PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.2.2
Filter Everything — WordPress & WooCommerce Filters v1.9.2.2
1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 All 52 releases
filter-everything / src / Admin / FilterSet.php

FilterSet.php in Filter Everything — WordPress & WooCommerce Filters 1.9.2.2, at src/Admin/FilterSet.php

1,520 lines 58.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace FilterEverything\Filter;
5
6 if ( ! defined('ABSPATH') ) {
7 exit;
8 }
9
10 class FilterSet
11 {
12 const NONCE_ACTION = 'wpc-f-set-nonce';
13
14 const FIELD_NAME_PREFIX = 'wpc_set_fields';
15
16 const FREE_LIMIT_NEW = 2;
17
18 const FREE_LIMIT_LEGACY = 3;
19
20 const FREE_LIMIT_CUTOFF_DATE = '2026-06-15 00:00:00';
21
22 private $defaultFields = [];
23
24 private $hooksRegistered = false;
25
26 private $errors;
27
28 private $changingTrashSlug = false;
29
30 public function __construct()
31 {
32 $this->registerHooks();
33 }
34
35 private function setupDefaultFields()
36 {
37 // maybe add filter in future to allow change default fields
38 $defaultFields = array(
39 'post_type' => array(
40 'type' => 'Select',
41 'label' => esc_html__('Post Type to filter', 'filter-everything'),
42 'name' => $this->generateFieldName('post_type'),
43 'id' => $this->generateFieldId('post_type'),
44 'class' => 'wpc-field-post-type',
45 'options' => $this->getPostTypes(),
46 'default' => 'post',
47 'instructions' => esc_html__('Select the Post Type you need to filter', 'filter-everything'),
48 'particular' => 'post_excerpt' // Determine that this is specific field should be stored in wp_post column
49 ),
50 'hide_empty' => array(
51 'type' => 'Select',
52 'label' => esc_html__('Empty Terms', 'filter-everything'),
53 'name' => $this->generateFieldName('hide_empty'),
54 'id' => $this->generateFieldId('hide_empty'),
55 'class' => 'wpc-field-hide-empty',
56 'options' => array(
57 'no' => esc_html__('Never hide', 'filter-everything'),
58 'yes' => esc_html__('Always hide', 'filter-everything'),
59 'initial' => esc_html__('Hide in the initial Filter only', 'filter-everything')
60 ),
61 'default' => 'no',
62 'instructions' => esc_html__('To hide or not Filter terms that do not contain posts', 'filter-everything'),
63 'settings' => true
64 ),
65 'show_count' => array(
66 'type' => 'Checkbox',
67 'label' => esc_html__('Show counters', 'filter-everything'),
68 'name' => $this->generateFieldName('show_count'),
69 'id' => $this->generateFieldId('show_count'),
70 'class' => 'wpc-field-show-count',
71 'default' => 'yes',
72 'instructions' => esc_html__('Displays the number of posts in a term', 'filter-everything'),
73 'settings' => true
74 ),
75 'wp_page_type' => array(
76 'type' => 'Select',
77 'label' => esc_html__('Where to filter?', 'filter-everything'),
78 'class' => 'wpc-field-wp-page-type',
79 'id' => $this->generateFieldId('wp_page_type'),
80 'name' => $this->generateFieldName('wp_page_type'),
81 'options' => flrt_get_set_location_groups(),
82 'default' => 'common___common',
83 'instructions' => esc_html__('Specify page(s) where the Posts list should be filtered is located', 'filter-everything'),
84 'settings' => true,
85 'location' => true,
86 ),
87 'post_name' => array(
88 'type' => 'Select',
89 'label' => '',
90 'class' => 'wpc-field-location',
91 'id' => $this->generateFieldId('post_name'),
92 'name' => $this->generateFieldName('post_name'),
93 'options' => flrt_get_set_location_terms(),
94 'default' => '1',
95 'instructions' => '',
96 'particular' => 'post_name', // Determine that this is specific field should be stored in wp_post column
97 'settings' => true,
98 'location' => true,
99 ),
100
101 'wp_filter_query' => array(
102 'type' => 'Select',
103 'label' => esc_html__('What to filter?', 'filter-everything'),
104 'class' => 'wpc-field-wp-filter-query',
105 'id' => $this->generateFieldId('wp_filter_query'),
106 'name' => $this->generateFieldName('wp_filter_query'),
107 'options' => array( '-1' => esc_html__('— Select Query —', 'filter-everything') ),
108 'default' => '-1',
109 'instructions' => esc_html__('Determines what exactly the Posts list (WP_Query) on a page should be filtered', 'filter-everything'),
110 'tooltip' => wp_kses ( __( 'Every Posts list, like "Popular products" or "Recent posts" on a page, is related to some WP_Query. This field allows you to set desired Posts list by choosing its WP_Query.<br /><br />If the filtering process does not change the Posts you need, it means you selected the wrong WP_Query. Please, try to experiment with different ones until it starts to filter.', 'filter-everything' )
111 ,
112 array(
113 'br' => array()
114 )
115 ),
116 'settings' => true,
117 'location' => true,
118 ),
119
120 );
121
122 $this->defaultFields = apply_filters( 'wpc_filter_set_default_fields', $defaultFields, $this );
123 }
124
125 private function registerHooks()
126 {
127 if ( ! $this->hooksRegistered ) {
128 add_filter( 'wpc_input_type_select', [ $this, 'addCustomLabel' ], 10, 2 );
129 add_action( 'admin_print_scripts', [ $this, 'includeAdminJs' ], 9999 );
130
131 add_filter( 'post_updated_messages', [ $this, 'filterSetActionsMessages' ] );
132 add_filter( 'bulk_post_updated_messages', [ $this, 'filterSetBulkActionsMessages' ], 10, 2 );
133
134 add_filter( 'page_row_actions', [ $this, 'filterSetRowActions' ], 10, 2 );
135
136 add_filter( 'page_row_actions', [ $this, 'addDuplicateLink' ], 11, 2 );
137
138
139 add_action( 'restrict_manage_posts', [ $this, 'restrictManagePosts' ], 999 );
140
141 add_action('manage_posts_extra_tablenav', [ $this, 'display_auto_filter_set_create_links'], 21, 1);
142
143 add_action('admin_post_wpc_create_auto_filter_set', [ $this, 'handle_create_auto_filter_set']);
144
145 add_action('admin_notices', array($this, 'adminErrorNotice'));
146
147 add_action('trashed_post', array($this, 'removePermalinksFromSettings'));
148
149 add_action('before_delete_post', array($this, 'removePermalinksFromSettings'));
150 add_action('transition_post_status', array($this, 'changeTrashSlug'), 10, 3);
151
152 $this->hooksRegistered = true;
153 }
154 }
155
156 public function restrictManagePosts( $post_type )
157 {
158 if( $post_type === FLRT_FILTERS_SET_POST_TYPE ){
159 $output = ob_get_clean();
160 ob_start();
161 }
162 }
163
164 public function filterSetRowActions( $actions, $post )
165 {
166 if( isset( $post->post_type ) && $post->post_type === FLRT_FILTERS_SET_POST_TYPE ){
167 $new_actions = [];
168 foreach( $actions as $key => $action ){
169 if( in_array( $key, array( 'edit', 'trash', 'untrash', 'delete', 'flrt_duplicate' ) ) ){
170 $new_actions[$key] = $action;
171 }
172 }
173 return $new_actions;
174 }
175 return $actions;
176 }
177
178 public function filterSetBulkActionsMessages( $messages, $bulk_counts )
179 {
180 if( ! isset( $messages[ FLRT_FILTERS_SET_POST_TYPE ] ) ){
181 $messages[ FLRT_FILTERS_SET_POST_TYPE ] = array(
182 /* translators: %s: Number of posts. */
183 'updated' => esc_html( _n( '%s filter set has been updated.', '%s filter sets have been updated.', $bulk_counts['updated'], 'filter-everything' ) ),
184 'locked' => ( 1 === $bulk_counts['locked'] ) ? esc_html__( '1 The filter set has not been updated. Someone is editing it.', 'filter-everything' ) :
185 /* translators: %s: Number of posts. */
186 esc_html( _n( '%s filter set has not been updated. Someone is editing it.', '%s filter sets have not been updated. Someone is editing them.', $bulk_counts['locked'], 'filter-everything' ) ),
187 /* translators: %s: Number of posts. */
188 'deleted' => esc_html( _n( '%s filter set has been permanently deleted.', '%s filter sets have been permanently deleted.', $bulk_counts['deleted'], 'filter-everything' ) ),
189 /* translators: %s: Number of posts. */
190 'trashed' => esc_html( _n( '%s filter set has been moved to the Trash.', '%s filter sets have been moved to the Trash.', $bulk_counts['trashed'], 'filter-everything' ) ),
191 /* translators: %s: Number of posts. */
192 'untrashed' => esc_html( _n( '%s filter set has been restored from the Trash.', '%s filter sets have been restored from the Trash.', $bulk_counts['untrashed'], 'filter-everything' ) ),
193 );
194 }
195 return $messages;
196 }
197
198 public function filterSetActionsMessages( $messages )
199 {
200 if( ! isset( $messages[ FLRT_FILTERS_SET_POST_TYPE ] ) ){
201 // No need to escape
202 $messages[ FLRT_FILTERS_SET_POST_TYPE ] = array(
203 0 => '',
204 1 => esc_html__( 'The Filter set has been updated.', 'filter-everything' ),
205 2 => esc_html__( 'The Custom field has been updated.', 'filter-everything' ),
206 3 => esc_html__( 'The Custom field has been deleted.', 'filter-everything' ),
207 4 => esc_html__( 'The Filter set has been updated.', 'filter-everything' ),
208 5 => false,
209 6 => esc_html__( 'The Filter set has been created.', 'filter-everything' ),
210 7 => esc_html__( 'The Filter set has been saved.', 'filter-everything' ),
211 8 => esc_html__( 'The Filter set has been submitted.', 'filter-everything' ),
212 9 => esc_html__( 'The Filter set has been scheduled for', 'filter-everything' ),
213 10 => esc_html__( 'The Filter set draft has been updated.', 'filter-everything' ),
214 // Errors
215 11 => esc_html__('The Filter set has not been updated.', 'filter-everything')
216 );
217 }
218
219 return $messages;
220 }
221
222 /**
223 * @return array
224 */
225 protected function getExistingFilterSlugs()
226 {
227 $existingSlugs = get_option('wpc_filter_permalinks', []);
228 $convertedExistingSlugs = [];
229
230 foreach( $existingSlugs as $entityKey => $slug ){
231 $parts = explode( '#', $entityKey, 2 );
232 $newEntityKey = implode('_', $parts);
233 $convertedExistingSlugs[$newEntityKey] = $slug;
234 }
235
236 if( isset( $convertedExistingSlugs['post_date_post_date'] ) ){
237 $convertedExistingSlugs['post_date'] = $convertedExistingSlugs['post_date_post_date'];
238 unset($convertedExistingSlugs['post_date_post_date']);
239 }
240
241 return $convertedExistingSlugs;
242 }
243
244 /**
245 * @return array
246 */
247 private function getPostTypesTaxList()
248 {
249 $postTypesTaxList = [];
250
251 $taxonomies = EntityManager::getTaxonomies();
252 $postTypes = array_keys( $this->getPostTypes() );
253
254 foreach ( $postTypes as $postType ){
255 foreach ( $taxonomies as $taxonomy) {
256
257 if( in_array( $postType, $taxonomy->object_type ) ){
258 $postTypesTaxList[$postType][] = array(
259 'name' => 'taxonomy_' . $taxonomy->name,
260 'hierarchical' => $taxonomy->hierarchical,
261 'label' => ucwords( flrt_ucfirst( mb_strtolower( $taxonomy->label ) ) ),
262 );
263 }
264 }
265 }
266
267 return $postTypesTaxList;
268 }
269
270 public function includeAdminJs()
271 {
272 $screen = get_current_screen();
273
274 if( isset( $screen->id ) && $screen->id === FLRT_FILTERS_SET_POST_TYPE ){
275 global $post_id;
276
277 // Disable autosavings
278 wp_dequeue_script( 'autosave' );
279
280 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
281 $ver = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? rand(0, 1000) : FLRT_PLUGIN_VER;
282 // $select2ver = '4.1.0';
283
284 // Filter Set script
285 wp_enqueue_script('wpc-filters-admin-filter-set', FLRT_PLUGIN_DIR_URL . 'assets/js/wpc-filter-set-admin'.$suffix.'.js', array('jquery', 'wp-util', 'jquery-ui-sortable', 'select2', 'wpc-filters-admin'), $ver );
286
287 $l10n = array(
288 'filterSlugs' => $this->getExistingFilterSlugs(),
289 'postTypesTaxList' => $this->getPostTypesTaxList(),
290 'swatchesTaxonomies' => flrt_get_experimental_option( 'color_swatches_taxonomies', [] ),
291 'brandEntities' => flrt_brand_filter_entities(),
292 'ratingTaxonomies' => array('product_visibility'),
293 'moreOptions' => esc_html__( 'More options', 'filter-everything' ),
294 'lessOptions' => esc_html__( 'Less options', 'filter-everything' ),
295 'filtersPro' => defined( 'FLRT_FILTERS_PRO' ),
296 'wPQuerySelectId' => $this->generateFieldId('wp_filter_query'),
297 'excludePlaceholder' => esc_html__( 'Select terms and', 'filter-everything' ),
298 'newFilter' => esc_html__( 'New Filter', 'filter-everything' ),
299 'addFilter' => esc_html__( 'Please, add filters first', 'filter-everything' ),
300 'selectFilter' => esc_html__( '— Select Filter —', 'filter-everything' ),
301 'numFieldNoTaxes' => esc_html__( 'No taxonomies are associated with the selected post type.', 'filter-everything' ),
302 'numFieldAttrs' => [
303 'post_meta_num' => [
304 'label' => esc_html__( 'Meta Key', 'filter-everything' ),
305 'description' => esc_html__( 'Name of the Custom Field', 'filter-everything' ),
306 ],
307 'tax_numeric' => [
308 'label' => esc_html__( 'Taxonomy', 'filter-everything' ),
309 'description' => esc_html__( 'Taxonomy with numeric values you need to filter by', 'filter-everything' ),
310 'notice' => '',
311 ]
312 ],
313 'defaultCustomMetaKeys' => wpc_default_custom_meta_keys_filter(),
314 'selectMetaKeyPlaceholder' => esc_html__( 'Type to search for a custom field key', 'filter-everything' ),
315 'metaKeySearchText' => esc_html__( 'Searching', 'filter-everything' ),
316 'isLimitFilterSet' => $this->under_limit_filter_set($post_id),
317 );
318
319 wp_localize_script( 'wpc-filters-admin-filter-set', 'wpcSetVars', $l10n );
320 }
321 }
322
323 public function addCustomLabel( $html, $attributes )
324 {
325 if( isset( $attributes['id'] ) ){
326 if( $attributes['id'] == $this->generateFieldId('post_name') ){
327
328 $spinner = '<span class="spinner"></span>'."\r\n";
329 $openContainer = '<div id="wpc-field-location-container"><span class="wpc-full-width">&nbsp;</span><div class="wpc-field-location-wrapper">'."\r\n";
330 $closeContainer = '</div></div>'."\r\n";
331 $link = '';
332
333 $current_index = isset( $attributes['value'] ) ? $attributes['value'] : '';
334 $options = ! empty( $attributes['options'] ) ? $attributes['options'] : [];
335
336 $data_link = '';
337 $hidden_link_class = ' display-none';
338 if( isset( $options[ $current_index ]['data-link'] ) ){
339 $data_link = esc_attr( $options[ $current_index ]['data-link'] );
340 $hidden_link_class = '';
341 }
342 $link = '<div class="wpc-location-preview-hidden"><a class="wpc-location-preview ' . $hidden_link_class . '" href="'.$data_link.'" ';
343 $link .= 'title="'.esc_attr( esc_html__('Preview the selected location in a new tab', 'filter-everything') ).'" ';
344 $link .= 'target="_blank">';
345 $link .= '<span class="dashicons dashicons-visibility"></span></a></div>';
346 $html = $spinner . $openContainer . $html . $link . $closeContainer;
347 }
348
349 if ( $attributes['id'] == $this->generateFieldId('apply_button_post_name') ) {
350
351 $spinner = '<span class="spinner"></span>'."\r\n";
352 $openContainer = '<div id="wpc-field-apply-button-location-container"><span class="wpc-full-width">&nbsp;</span>'."\r\n";
353 $closeContainer = '</div>'."\r\n";
354 $link = '';
355
356 $html = $spinner . $openContainer . $html . $link . $closeContainer;
357 }
358
359 if( $attributes['id'] == $this->generateFieldId('wp_filter_query') ){
360
361 $spinner = '<span class="spinner"></span>'."\n";
362 $openContainer = '<div id="wpc-field-wp-query-container">&nbsp;'."\n";
363 $description = '<p class="description">'.esc_html__( 'Note: if you modify the selected Posts list on the page, please update this Filter Set', 'filter-everything' ).'</p>'."\n";
364 $closeContainer = '<div id="wpc_query_vars"></div></div>'."\n";
365
366 $html = $spinner . $openContainer . $html . $description . $closeContainer;
367 }
368
369 if( $attributes['id'] == $this->generateFieldId('wp_page_type') ){
370 $label = '<label class="wpc-location-label" for="'.esc_attr($attributes['id']).'">'.esc_html__( 'Apply filtering if the page is:', 'filter-everything' ).'</label>'."\r\n";
371 $html = $label . $html;
372 }
373
374 if( $attributes['id'] == $this->generateFieldId('apply_button_page_type') ){
375 $label = '<label class="wpc-location-label" for="'.esc_attr($attributes['id']).'">'.esc_html__( 'Show this Filter Set if the page is:', 'filter-everything' ).'</label>'."\r\n";
376 $html = $label . $html;
377 }
378 }
379
380 return $html;
381 }
382
383
384 private function getSpecificFields( $type, $exclude_type = '' )
385 {
386 $particular = [];
387
388 foreach( $this->getFieldsMapping() as $key => $field ){
389 if(!empty($exclude_type) && ! empty( $field[$exclude_type])){
390 continue;
391 }
392 if( isset( $field[$type] ) && ! empty( $field[$type] ) ){
393 $particular[ $key ] = $field;
394 }
395 }
396
397 return $particular;
398 }
399
400 public function getPostTypes()
401 {
402 $allowed_types = [];
403 $post_types = get_post_types( array( 'public' => true ), 'objects' );
404 $exclude = apply_filters( 'wpc_filter_post_types', [] );
405
406 foreach ( $post_types as $type ){
407 if( in_array( $type->name, $exclude ) ){
408 continue;
409 }
410 $allowed_types[$type->name] = isset( $type->labels->name ) ? $type->labels->name : $type->labels->singular_name;
411 }
412
413 return $allowed_types;
414
415 }
416
417 public function getFieldsMapping()
418 {
419 return $this->defaultFields;
420 }
421
422 /**
423 * @var $queriedObject object
424 * @return array (empty array if there is no related set id)
425 */
426 public function findRelevantSets( $queriedObject )
427 {
428 // We need to search all relevantSetS
429 $filterSets = [];
430
431 $filterSets = apply_filters( 'wpc_relevant_set_ids', $filterSets, $queriedObject);
432
433 if( ! empty( $filterSets ) ){
434 foreach ( $filterSets as $set ){
435 if( isset( $set['show_on_the_page'] ) && $set['show_on_the_page'] === true ){
436 return $filterSets;
437 }
438 }
439 }
440
441 // Get main filter set for post type
442 if( isset( $queriedObject['post_types'] ) && ! isset( $queriedObject['post_id'] ) ){
443 foreach( $queriedObject['post_types'] as $post_type ){
444 $sets = $this->getSetIdForPostType( $post_type );
445 if( $sets !== false ){
446 $filterSets = array_merge( $filterSets, $sets );
447 }
448 }
449 }
450
451 $filterSets = apply_filters( 'wpc_return_relevant_set_ids', $filterSets, $queriedObject );
452
453 return $filterSets;
454 }
455
456 /**
457 * @var $post_type string - post_type post|product|page|...
458 * @return int|false
459 */
460 public function getSetIdForPostType( $post_type )
461 {
462 if( ! $post_type ){
463 return false;
464 }
465
466 $container = Container::instance();
467 $sets = [];
468 $pll_lang_id = false;
469
470 if ( is_array( $post_type ) ) {
471 $post_type = implode( "_", $post_type );
472 }
473
474 $key = 'set_' . $post_type;
475
476 if( ! $sets = $container->getParam( $key ) ){
477 global $wpdb;
478 $is_fitler_set_translatable = false;
479
480 if( flrt_wpml_active() ){
481 $wpml_settings = get_option( 'icl_sitepress_settings' );
482 if( isset( $wpml_settings['custom_posts_sync_option'][FLRT_FILTERS_SET_POST_TYPE] ) ){
483 if( $wpml_settings['custom_posts_sync_option'][FLRT_FILTERS_SET_POST_TYPE] === '1' ){
484 $is_fitler_set_translatable = true;
485 }
486 }
487 }
488
489 $sql[] = "SELECT {$wpdb->posts}.ID,{$wpdb->posts}.post_content,{$wpdb->posts}.post_excerpt,{$wpdb->posts}.post_name";
490 $sql[] = "FROM {$wpdb->posts}";
491
492 if ( flrt_wpml_active() && defined('ICL_LANGUAGE_CODE') && $is_fitler_set_translatable ) {
493 $sql[] = "LEFT JOIN {$wpdb->prefix}icl_translations AS wpml_translations";
494 $sql[] = "ON {$wpdb->posts}.ID = wpml_translations.element_id";
495 $sql[] = "AND wpml_translations.element_type IN(";
496 $sql[] = $wpdb->prepare( "CONCAT('post_', '%s')", FLRT_FILTERS_SET_POST_TYPE );
497 $sql[] = ")";
498 }
499
500 // Check common if Polylang PRO is active and Filter Set is translatable post type
501 if( flrt_pll_pro_active() && defined('FLRT_ALLOW_PLL_TRANSLATIONS') && FLRT_ALLOW_PLL_TRANSLATIONS ){
502 if( function_exists('pll_current_language') && function_exists('pll_the_languages') ) {
503 $pll_current_language = pll_current_language();
504 $pll_languages = pll_the_languages( array('raw' => 1 ) );
505 if( $pll_current_language && isset( $pll_languages[ $pll_current_language ]['id'] ) ){
506 $pll_lang_id = $pll_languages[ $pll_current_language ]['id'];
507
508 $sql[] = "LEFT JOIN {$wpdb->term_relationships}";
509 $sql[] = "ON ({$wpdb->posts}.ID = {$wpdb->term_relationships}.object_id)";
510 }
511 }
512 }
513
514 $sql[] = "WHERE 1=1";
515 $sql[] = "AND ( {$wpdb->posts}.post_name IN ( '1' )";
516
517 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
518 $sql[] = "OR {$wpdb->posts}.ID IN ( ";
519 $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
520 $sql[] = "LEFT JOIN {$wpdb->postmeta} ON ( {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id )";
521 $sql[] = "WHERE 1=1";
522 $sql[] = $wpdb->prepare( "AND {$wpdb->postmeta}.meta_key = %s", FLRT_APPLY_BUTTON_META_KEY);
523 $sql[] = "AND {$wpdb->postmeta}.meta_value IN ( '1' )";
524 $sql[] = ")";
525 }
526
527 $sql[] = ")";
528 $sql[] = $wpdb->prepare("AND {$wpdb->posts}.post_type = '%s'", FLRT_FILTERS_SET_POST_TYPE );
529 $sql[] = $wpdb->prepare("AND {$wpdb->posts}.post_excerpt = '%s'", $post_type );
530
531 $sql[] = "AND ( ({$wpdb->posts}.post_status = 'publish') )";
532
533 if( flrt_wpml_active() && defined( 'ICL_LANGUAGE_CODE' ) && $is_fitler_set_translatable ){
534 $sql[] = $wpdb->prepare("AND wpml_translations.language_code = '%s'", ICL_LANGUAGE_CODE );
535 }
536
537 if( flrt_pll_pro_active() && defined('FLRT_ALLOW_PLL_TRANSLATIONS') && FLRT_ALLOW_PLL_TRANSLATIONS ){
538 if( $pll_lang_id ){
539 $sql[] = $wpdb->prepare("AND {$wpdb->term_relationships}.term_taxonomy_id IN (%d)", $pll_lang_id );
540 }
541 }
542
543 $sql[] = "ORDER BY {$wpdb->posts}.menu_order DESC, {$wpdb->posts}.ID ASC";
544
545 if( ! defined('FLRT_FILTERS_PRO') ) {
546 $sql[] = "LIMIT 0, 1";
547 }
548
549 $sql = implode(' ', $sql );
550
551 $setPosts = $wpdb->get_results( $sql, OBJECT );
552
553 if( ! empty( $setPosts ) ){
554 $sets = flrt_is_query_on_page( $setPosts, array( '1' ) );
555 }else{
556 return false;
557 }
558
559 $container->storeParam( $key, $sets );
560
561 }
562
563 return $sets;
564 }
565
566 public function validateSets( $sets )
567 {
568 if( ! is_array( $sets ) || empty( $sets ) ){
569 return false;
570 }
571
572 foreach ( $sets as $i => $set ){
573 if( ! isset( $set['ID'] ) || ! $set['ID'] ){
574 return false;
575 }
576 }
577
578 return true;
579 }
580
581 public function preSaveSet( $post_id, $data )
582 {
583 $postData = Container::instance()->getThePost();
584
585 if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
586 return $post_id;
587 }
588
589 if( wp_is_post_revision( $post_id ) ) {
590 return $post_id;
591 }
592
593 if( $data['post_type'] !== FLRT_FILTERS_SET_POST_TYPE ) {
594 return $post_id;
595 }
596
597 $nonce = filter_input( INPUT_POST, '_flrt_nonce' );
598
599 if( ! $this->verifyNonce( $nonce ) ) {
600 return $post_id;
601 }
602
603 if( ! current_user_can( flrt_plugin_user_caps() ) ) {
604 return $post_id;
605 }
606
607 // Do not fire this function twice, on saving Set Fields action
608 remove_action( 'pre_post_update', [$this, 'preSaveSet'], 10 );
609
610 $set_fields_key = self::FIELD_NAME_PREFIX;
611
612 if( isset( $postData[$set_fields_key] ) && ! empty( $postData[$set_fields_key] ) ){
613 $setFields = $postData[$set_fields_key];
614 $setFields['ID'] = $post_id;
615 $setFields['title'] = isset( $data['post_title'] ) ? $data['post_title'] : '';
616
617 $setFields = apply_filters( 'wpc_pre_save_set_fields', $setFields );
618
619 $setFields = $this->sanitizeSetFields( $setFields );
620
621 if( ! $this->validateSetFields( $setFields ) ){
622 flrt_redirect_to_error( $post_id, $this->errors );
623 }
624 }
625
626
627 return $post_id;
628 }
629
630 public function saveSet( $post_id, $post )
631 {
632 $postData = Container::instance()->getThePost();
633 if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
634 return $post_id;
635 }
636
637 if( wp_is_post_revision( $post_id ) ) {
638 return $post_id;
639 }
640
641 if( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
642 return $post_id;
643 }
644
645 $nonce = filter_input( INPUT_POST, '_flrt_nonce' );
646
647 if( ! $this->verifyNonce( $nonce ) ) {
648 return $post_id;
649 }
650
651 if( ! current_user_can( flrt_plugin_user_caps() ) ) {
652 return $post_id;
653 }
654
655 remove_action( 'save_post', array( $this, 'saveSet' ), 10, 2 );
656
657 $filterFields = $this->getFilterFieldService();
658 $saveFiltersTrigger = true;
659 $allFiltersValid = true;
660
661 // Save filter fields
662 if( isset( $postData['wpc_filter_fields'] ) && ! empty( $postData['wpc_filter_fields'] ) ) {
663
664 // Validate filters
665 if( ! $filterFields->validateFilters( $postData['wpc_filter_fields'] ) ) {
666 $saveFiltersTrigger = false;
667 }
668
669 if( $saveFiltersTrigger ) {
670 $filtersToSave = [];
671
672 // loop
673 $filterConfiguredFields = $filterFields->getFieldsMapping();
674 foreach ( $postData['wpc_filter_fields'] as $filterId => $filter ) {
675
676 // Set up checkbox fields if they are empty
677 $filter = $filterFields->prepareFilterCheckboxFields( $filter, $filterFields->getFieldsByType( 'checkbox', $filterConfiguredFields ) );
678
679 // set parent
680 if ( ! $filter['parent'] ) {
681 $filter['parent'] = $post_id;
682 }
683
684 $filter = $filterFields->sanitizeFilterFields( $filter );
685 $filtersToSave[ $filterId ] = $filter;
686
687 if ( ! $filterFields->validateTheFilter( $filter, $filterId ) ) {
688 $allFiltersValid = false;
689 break;
690 }
691
692 }
693
694 // Loop to save
695 if( $allFiltersValid ){
696
697 $update_after_save = [];
698 $old_new_ids = [];
699
700 foreach ( $filtersToSave as $filterId => $filter ){
701 // save filter
702 $saved_filter = $filterFields->saveFilter($filter);
703
704 if( isset( $saved_filter['parent_filter'] ) && isset( $saved_filter['ID'] ) ){
705 if( strpos( $saved_filter['parent_filter'], 'filter_', 0 ) !== false ){
706 $update_after_save[ $saved_filter['ID'] ][] = [
707 'key' => 'parent_filter',
708 'value' => $saved_filter['parent_filter']
709 ];
710 }
711 $old_new_ids[$filterId] = $saved_filter['ID'];
712 }
713 }
714
715 // Update data after saving Filters and getting IDs of new ones.
716 if( ! empty( $update_after_save ) ){
717
718 foreach ( $update_after_save as $filter_post_id => $fields_to_update ){
719 $filter_post_data = get_post( $filter_post_id );
720 $filter_data = maybe_unserialize( $filter_post_data->post_content );
721
722 if( ! $filter_data ){
723 continue;
724 }
725
726 foreach ( $fields_to_update as $field_attr ){
727 if( $field_attr['key'] === 'parent_filter' ){
728 if( isset( $filter_data[ 'parent_filter' ] ) ){
729 if( isset( $old_new_ids[ $field_attr['value'] ] ) ){
730 $filter_data[ $field_attr['key'] ] = $old_new_ids[ $field_attr['value'] ];
731 }
732 }
733 }
734 }
735
736 $to_update = array(
737 'ID' => $filter_post_id,
738 'post_content' => maybe_serialize( $filter_data )
739 );
740
741 // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data.
742 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
743 add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 );
744
745 // Slash data.
746 // WP expects all data to be slashed and will unslash it (fixes '\' character issues)
747 $to_update = wp_slash( $to_update );
748
749 wp_update_post( $to_update );
750
751 remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 );
752 }
753 }
754 }
755 }
756 }
757
758 // Save Set fields
759 $set_fields_key = self::FIELD_NAME_PREFIX;
760
761 if( isset( $postData[$set_fields_key] ) && ! empty( $postData[$set_fields_key] ) ){
762 $setFields = $postData[$set_fields_key];
763 $setFields['ID'] = $post_id;
764 $setFields['title'] = isset( $post->post_title ) ? $post->post_title : '';
765
766 $this->saveSetFields( $setFields );
767 }
768
769 if( ! $saveFiltersTrigger || ! $allFiltersValid ){
770 flrt_redirect_to_error( $post_id, $filterFields->getErrorCodes() );
771 }
772
773 add_action( 'save_post', array( $this, 'saveSet' ), 10, 2 );
774
775 return $post_id;
776 }
777
778
779
780 protected function saveSetFields($setFields ){
781 $post_id = $setFields['ID'];
782
783 $setFields = apply_filters( 'wpc_pre_save_set_fields', $setFields );
784
785 $setFields = $this->sanitizeSetFields( $setFields );
786
787 $setFields = wp_unslash( $setFields );
788
789 // Set up checkbox fields if they are empty
790 $filterFields = $this->getFilterFieldService();
791 /**
792 * @feature It seems we need to move methods 'prepareFilterCheckboxFields' and 'getFieldsByType'
793 * to one level above to parent class
794 */
795 $this->setupDefaultFields();
796 $setFields = $filterFields->prepareFilterCheckboxFields( $setFields, $filterFields->getFieldsByType( 'checkbox', $this->getFieldsMapping()) );
797 $_setFields = $setFields;
798
799 // Remove elements, that shouldn't be serialized
800 flrt_extract_vars( $_setFields, array( 'ID', 'title', 'post_type', 'menu_order', 'post_name', 'wp_filter_query_vars' ) );
801 $menu_order = isset( $setFields['menu_order'] ) ? $setFields['menu_order'] : 0;
802
803 // Create array of data to save.
804 $to_save = array(
805 'ID' => $setFields['ID'],
806 'post_status' => 'publish',
807 'post_type' => FLRT_FILTERS_SET_POST_TYPE,
808 'post_title' => $setFields['title'],
809 'post_content' => maybe_serialize( $_setFields ),
810 'post_excerpt' => $setFields['post_type'],
811 'menu_order' => $menu_order,
812 'post_name' => $setFields['post_name']
813 );
814
815 // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data.
816 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
817
818 $to_save = wp_slash( $to_save );
819
820 add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 );
821
822 // Update or Insert.
823 if( $setFields['ID'] ) {
824 wp_update_post( $to_save );
825 } else {
826 $setFields['ID'] = wp_insert_post( $to_save );
827 }
828
829 remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 );
830 // Update meta_fields
831
832 update_post_meta( $setFields['ID'], 'wpc_filter_set_post_type', $setFields['post_type'] );
833
834 $set_query_vars = NULL;
835 // Save selected wp_query->query_vars
836 if( isset( $setFields['wp_filter_query'] ) ){
837 $filterQueryHash = $setFields['wp_filter_query'];
838 if( isset( $setFields['wp_filter_query_vars'][$filterQueryHash] ) ){
839 $set_query_vars = $setFields['wp_filter_query_vars'][$filterQueryHash];
840 }
841 }
842
843 update_post_meta( $setFields['ID'], 'wpc_filter_set_query_vars', $set_query_vars );
844
845 if( isset( $setFields['apply_button_post_name'] ) ){
846 update_post_meta( $setFields['ID'], FLRT_APPLY_BUTTON_META_KEY, $setFields['apply_button_post_name'] );
847 }
848
849 return $setFields['ID'];
850 }
851
852 private function sanitizeSetFields( $setFields )
853 {
854 if( is_array( $setFields ) ){
855 $sanitizedFields = [];
856
857 foreach ( $setFields as $key => $setField ) {
858 if( is_array( $setField ) ){
859 $sanitizedValue = $setField;
860 }else{
861 $sanitizedValue = esc_html( $setField );
862 }
863
864 $sanitizedFields[ $key ] = $sanitizedValue;
865 }
866
867 if( isset( $sanitizedFields['menu_order'] ) ){
868 $sanitizedFields['menu_order'] = flrt_sanitize_int( $sanitizedFields['menu_order'] );
869 $sanitizedFields['menu_order'] = $sanitizedFields['menu_order'] ? $sanitizedFields['menu_order'] : 0;
870 }
871
872 return $sanitizedFields;
873 }
874
875 return $setFields;
876 }
877
878 private function prepareSetParameters( $set_post )
879 {
880 /**
881 * @feature this should be not so complex. I'm ashamed of this.
882 */
883 if( ! isset( $set_post->ID ) ){
884 return false;
885 }
886
887 $this->setupDefaultFields();
888 $defaults = $this->getFieldsMapping();
889
890 $defaults = apply_filters( 'wpc_prepare_filter_set_parameters', $defaults, $set_post );
891
892 $unserialized = maybe_unserialize( $set_post->post_content );
893
894 // For backward compatibility. From v.1.1.24
895 if( isset( $unserialized['wp_page_type'] ) ){
896 $unserialized['wp_page_type'] = str_replace(":", "___", $unserialized['wp_page_type']);
897 }
898
899 if( empty( $unserialized ) ){
900 $unserialized = [];
901 }
902
903 foreach( $this->getSpecificFields( 'particular' ) as $key => $field ){
904 $unserialized[$key] = $set_post->{$field['particular']};
905 }
906
907 $populated = $this->populateValues( $unserialized, $defaults );
908 $parsed = $this->parseValues( $populated, $defaults );
909
910 // In case if some settings field was missing
911 $parsed = wp_parse_args( $parsed, $defaults );
912 $parsed = apply_filters( 'wpc_filter_before_make_default_set_values', $parsed );
913
914 // Set default values, if there is no saved
915 foreach( $parsed as $field => $params ){
916
917 if( ! isset( $params['value'] ) && isset( $params['default'] )){
918 $parsed[$field]['value'] = $params['default'];
919 }
920 }
921
922 $parsed['ID'] = $set_post->ID;
923
924 return apply_filters( 'wpc_filter_set_prepared_values', $parsed );
925 }
926
927 private function parseValues( $populated, $defaults )
928 {
929 $parsed = [];
930
931 foreach ( $populated as $field_key => $values_array ){
932 // In case if we have saved field, that is absent in fieldsMapping
933 if( ! isset( $defaults[$field_key] ) ){
934 continue;
935 }
936
937 if( isset( $values_array['value'] ) ){
938 $parsed[$field_key] = wp_parse_args( $values_array, $defaults[$field_key] );
939 }else{
940 $parsed[$field_key] = $this->parseValues( $values_array, $defaults[$field_key] );
941 }
942 }
943
944 return $parsed;
945 }
946
947 private function populateValues( $saved_values )
948 {
949 $transformed = [];
950
951 foreach ( $saved_values as $field_key => $field_value ) {
952 if( is_array( $field_value ) ){
953 $transformed[ $field_key ] = $this->populateValues( $field_value );
954 }else{
955 $transformed[ $field_key ] = array( 'value' => $field_value );
956 }
957 }
958
959 return $transformed;
960 }
961
962 public function getSet( $ID )
963 {
964 $parameters = [];
965
966 if( ! $ID || empty( $ID ) ){
967 return $parameters;
968 }
969
970 $container = Container::instance();
971 $key = 'wpc_set_' . $ID;
972
973 if( ! $set = $container->getParam( $key ) ){
974 $set_post = get_post( $ID );
975 /**
976 * @feature add this post to cache.
977 */
978 $set = $this->prepareSetParameters( $set_post );
979 $container->storeParam( $key, $set );
980 }
981
982 return $set;
983 }
984
985 public function validateSetFields( $setFields ){
986
987 // Validate post_type
988 if( isset( $setFields['post_type'] ) ){
989 $postTypes = array_keys( $this->getPostTypes() );
990 if( ! in_array( $setFields['post_type'], $postTypes, true ) ){
991 $this->errors[] = 21; // Invalid post type
992 return false;
993 }
994 } else {
995 $this->errors[] = 21; // Invalid post type
996 return false;
997 }
998
999 if (!empty($setFields['post_type'])) {
1000 if($this->under_limit_filter_set($setFields['ID'], $setFields['post_type'], true)){
1001 $this->errors[] = 92;
1002 return false;
1003 }
1004 }
1005
1006 // We have to validate wp_page_type before locations field
1007 // because the last one expects valid wp_page_type
1008 if( isset( $setFields['wp_page_type'] ) ){
1009 $possibleWpPageType = apply_filters( 'wpc_validation_wp_page_type_entities', array('common___common') );
1010
1011 if( ! in_array( $setFields['wp_page_type'], $possibleWpPageType ) ){
1012 $this->errors[] = 211; // Invalid WP Page Type
1013 return false;
1014 }
1015 }else{
1016 $this->errors[] = 211; // Invalid WP Page Type
1017 return false;
1018 }
1019
1020 // Validate post_name aka location
1021 // We can not forbid to save "No WP Queries..." option
1022 // Because All archive pages for selected post type may not contain relevant query.
1023 if( isset( $setFields['post_name'] ) ){
1024 $flatEntities = apply_filters( 'wpc_validation_location_entities', array('1'), $setFields );
1025
1026 if( ! in_array( $setFields['post_name'], $flatEntities ) ){
1027 $this->errors[] = 22; // Invalid location
1028 return false;
1029 }
1030
1031 } else {
1032 $this->errors[] = 22; // Invalid location
1033 return false;
1034 }
1035
1036 //Validate wp_filter_query
1037 if( isset( $setFields['wp_filter_query'] ) ){
1038 if(! preg_match('/^[a-f0-9]{32}$/', $setFields['wp_filter_query'] ) && $setFields['wp_filter_query'] !== '-1'){
1039 $this->errors[] = 221; // Invalid WP Filter Query
1040 return false;
1041 }
1042 }else{
1043 $this->errors[] = 221; // Invalid WP Filter Query
1044 return false;
1045 }
1046
1047 // Validate hide_empty
1048 if( isset( $setFields['hide_empty'] ) ){
1049 if( ! in_array( $setFields['hide_empty'], array( 'yes', 'no', 'initial' ), true ) ){
1050 $this->errors[] = 23; // Invalid empty field
1051 return false;
1052 }
1053 }
1054
1055 // Validate show_count
1056 if( isset( $setFields['show_count'] ) ){
1057 if( ! in_array( $setFields['show_count'], array( 'yes', 'no' ), true ) ){
1058 $this->errors[] = 24; // Invalid show count
1059 return false;
1060 }
1061 }
1062
1063 if( isset( $setFields['horizontal_view'] ) ){
1064 if( ! in_array( $setFields['horizontal_view'], array( 'yes', 'no' ), true ) ){
1065 $this->errors[] = 25; // Invalid horizontal view
1066 return false;
1067 }
1068 }
1069
1070 if( isset( $setFields['wp_filter_query_vars'] ) ){
1071 if( ! empty( $setFields['wp_filter_query_vars'] ) ){
1072
1073 foreach ( $setFields['wp_filter_query_vars'] as $query_vars_serialized ){
1074 if( ! is_serialized( $query_vars_serialized ) ){
1075 $this->errors[] = 20; // Common Error
1076 return false;
1077 }
1078 }
1079 }
1080 }
1081
1082 if( isset( $setFields['use_apply_button'] ) ){
1083 if( ! in_array( $setFields['use_apply_button'], array( 'yes', 'no' ), true ) ){
1084 $this->errors[] = 242; // Invalid show count
1085 return false;
1086 }
1087 }
1088
1089 return $setFields;
1090 }
1091
1092 protected function getFilterFieldService()
1093 {
1094 return Container::instance()->getFilterFieldsService();
1095 }
1096
1097 public function getPostTypeField( $post_id )
1098 {
1099 $set = $this->getSet( $post_id );
1100 $field['post_type'] = ( $set['post_type'] ) ? $set['post_type'] : NULL;
1101 return $field;
1102 }
1103
1104 public function getSettingsTypeFields( $post_id )
1105 {
1106 $set = $this->getSet( $post_id );
1107 $settings_fields_map = $this->getSpecificFields('settings', 'location');
1108
1109 return flrt_extract_vars($set, array_keys( $settings_fields_map ) );
1110 }
1111
1112 public function getSettingsLocationTypeFields( $post_id )
1113 {
1114 $set = $this->getSet( $post_id );
1115 $settings_fields_map = $this->getSpecificFields('location');
1116
1117 return flrt_extract_vars($set, array_keys( $settings_fields_map ) );
1118 }
1119
1120 public function generateFieldName( $field_name, $sub_name = '', $index = 0 ){
1121 $attr = self::FIELD_NAME_PREFIX . '['.$field_name.']';
1122 if( $sub_name ){
1123 $attr .= '['.$index.']['.$sub_name.']';
1124 }
1125 return $attr;
1126 }
1127
1128 public function generateFieldId( $field_name, $sub_name = '', $index = 0 ){
1129 $attr = self::FIELD_NAME_PREFIX . '-' . $field_name;
1130 if( $sub_name ){
1131 $attr .= '-' . $sub_name . '-' . $index;
1132 }
1133 return $attr;
1134 }
1135
1136 public static function createNonce()
1137 {
1138 return wp_create_nonce( self::NONCE_ACTION );
1139 }
1140
1141 protected function verifyNonce($nonce )
1142 {
1143 return wp_verify_nonce( $nonce, self::NONCE_ACTION );
1144 }
1145
1146 public function addDuplicateLink(array $actions, \WP_Post $post): array
1147 {
1148 if ($post->post_type !== FLRT_FILTERS_SET_POST_TYPE || !current_user_can('edit_posts')) {
1149 return $actions;
1150 }
1151
1152 $is_pro = defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO;
1153 $new_actions = [];
1154
1155 foreach ($actions as $key => $action) {
1156 $new_actions[$key] = $action;
1157
1158 if ($key === 'edit') {
1159 $new_actions['flrt_duplicate'] = $this->generateDuplicateLinkHtml($post->ID, $is_pro);
1160 }
1161 }
1162
1163 return $new_actions;
1164 }
1165
1166 private function generateDuplicateLinkHtml(int $post_id, bool $is_pro): string
1167 {
1168 $label = esc_html__('Duplicate', 'filter-everything');
1169 $icon = '<span class="flrt-duplicate-filter-set">+</span> ';
1170
1171 if ($is_pro) {
1172 $url = wp_nonce_url(
1173 admin_url('admin.php?action=flrt_duplicate_filter_set&post=' . $post_id),
1174 'flrt_duplicate_filter_set'
1175 );
1176 return sprintf('<a href="%s">%s%s</a>', esc_url($url), $icon, $label);
1177 }
1178
1179 $pro_label = ' (' . esc_html__('PRO', 'filter-everything') . ')';
1180 return sprintf(
1181 '<a href="%s" class="wpc-vailable-in-pro-link">%s%s%s</a>',
1182 esc_url(flrt_vailable_in_pro_attr_link()),
1183 $icon,
1184 $label,
1185 $pro_label
1186 );
1187 }
1188
1189
1190 public function display_auto_filter_set_create_links($which)
1191 {
1192 if ($which !== 'top') {
1193 return;
1194 }
1195
1196 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
1197 if (!$screen || $screen->base !== 'edit' || $screen->post_type !== FLRT_FILTERS_SET_POST_TYPE) {
1198 return;
1199 }
1200
1201 $posts = get_posts([
1202 'post_type' => FLRT_FILTERS_SET_POST_TYPE,
1203 'post_status' => array('publish', 'pending', 'draft', 'future', 'private', 'trash'),
1204 'numberposts' => 1,
1205 'fields' => 'ids',
1206 'no_found_rows' => true,
1207 ]);
1208
1209 if (current_user_can('manage_options')) {
1210 $has_filter_set = true;
1211 if (empty($posts)) {
1212 $has_filter_set = false;
1213 }
1214 flrt_include_admin_view('auto-create-filter', ['nonce_action' => self::NONCE_ACTION, 'has_filter_set' => $has_filter_set]);
1215 }
1216 }
1217
1218 public function handle_create_auto_filter_set()
1219 {
1220 if (!current_user_can('manage_options')) {
1221 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'filter-everything'));
1222 }
1223
1224 if (!isset($_GET['_flrt_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_flrt_nonce'])), self::NONCE_ACTION)) {
1225 wp_die(esc_html__('The request could not be verified. Please try again.', 'filter-everything'));
1226 }
1227 $redirect_url = admin_url( 'edit.php?post_type=filter-set');
1228 $autoFilterSet = new \FilterEverything\Filter\AutoFilterSet($redirect_url);
1229 }
1230
1231 public function adminErrorNotice()
1232 {
1233 if(false !== ($result = get_transient('wpc_auto_filters_error'))){
1234 flrt_view_admin_error($result);
1235 delete_transient('wpc_auto_filters_error');
1236 }
1237 }
1238
1239 public function removePermalinksFromSettings($post_id){
1240 $post = get_post($post_id);
1241 if ( ! $post || $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
1242 return;
1243 }
1244
1245 $existingSlugs = get_option('wpc_filter_permalinks', []);
1246 if ( ! is_array($existingSlugs) ) {
1247 $existingSlugs = [];
1248 }
1249
1250 $child_posts = $this->getAllFilterSetPosts($post_id);
1251
1252 if(!empty($child_posts)){
1253 foreach ($child_posts as $child_post_obj) {
1254 $entityKey = $this->buildEntityKeyFromPostContent($child_post_obj->post_content);
1255 if ($entityKey !== null && isset($existingSlugs[$entityKey])) {
1256 unset($existingSlugs[$entityKey]);
1257 }
1258 }
1259 }
1260
1261 $other_filter_posts = $this->getAllFilterPosts($post_id);
1262 if (!empty($other_filter_posts)){
1263 foreach ($other_filter_posts as $filter_post_obj) {
1264 $entityKey = $this->buildEntityKeyFromPostContent($filter_post_obj->post_content);
1265 if ($entityKey !== null) {
1266 $existingSlugs[$entityKey] = $filter_post_obj->post_name;
1267 }
1268 }
1269 }
1270
1271 update_option('wpc_filter_permalinks', $existingSlugs, true);
1272 }
1273
1274 private function getAllFilterSetPosts($post_id)
1275 {
1276 return get_posts([
1277 'post_type' => FLRT_FILTERS_POST_TYPE,
1278 'posts_per_page' => -1,
1279 'post_status' => 'any',
1280 'post_parent' => $post_id
1281 ]);
1282 }
1283
1284 private function getAllFilterPosts($post_id)
1285 {
1286 $posts = get_posts([
1287 'post_type' => FLRT_FILTERS_POST_TYPE,
1288 'posts_per_page' => -1,
1289 'post_status' => 'publish',
1290 'post_parent__not_in' => [ $post_id ]
1291 ]);
1292
1293 $published_posts = [];
1294
1295 foreach ($posts as $post){
1296 $parent_post = get_post($post->post_parent);
1297 if($parent_post->post_status === 'publish'){
1298 $published_posts[] = $post;
1299 }
1300 }
1301
1302 return $published_posts;
1303 }
1304
1305 public function changeTrashSlug($new_status, $old_status, $post){
1306 if ( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
1307 return;
1308 }
1309 if ( $this->changingTrashSlug ) {
1310 return;
1311 }
1312 if ($old_status === 'trash' && $new_status === 'draft') {
1313 $existingSlugs = get_option('wpc_filter_permalinks', []);
1314 if ( ! is_array($existingSlugs) ) {
1315 $existingSlugs = [];
1316 }
1317
1318 $child_posts = $this->getAllFilterSetPosts($post->ID);
1319 foreach ($child_posts as $child_post_obj) {
1320 $entityKey = $this->buildEntityKeyFromPostContent($child_post_obj->post_content);
1321 if ($entityKey === null) {
1322 continue;
1323 }
1324 if ( empty($existingSlugs[$entityKey]) ) {
1325 continue;
1326 }
1327
1328 $desired_slug = $existingSlugs[$entityKey];
1329 if ( $desired_slug === $child_post_obj->post_name ) {
1330 continue;
1331 }
1332 if ( wp_is_post_revision($child_post_obj->ID) ) {
1333 continue;
1334 }
1335
1336 $update = array(
1337 'ID' => $child_post_obj->ID,
1338 'post_name' => $desired_slug,
1339 );
1340
1341 $this->changingTrashSlug = true;
1342 try {
1343 $result = wp_update_post($update, true);
1344 } finally {
1345 $this->changingTrashSlug = false;
1346 }
1347
1348 if ( is_wp_error($result) || 0 === $result ) {
1349 continue;
1350 }
1351 }
1352 }
1353 }
1354
1355 private function buildEntityKeyFromPostContent($rawPostContent)
1356 {
1357 $content = $this->safelyUnserializePostContent($rawPostContent);
1358 if (empty($content)) {
1359 return null;
1360 }
1361 if (empty($content['entity']) || empty($content['e_name'])) {
1362 return null;
1363 }
1364 return $content['entity'] . '#' . $content['e_name'];
1365 }
1366
1367 private function safelyUnserializePostContent($raw)
1368 {
1369 $content = maybe_unserialize($raw);
1370 return is_array($content) ? $content : [];
1371 }
1372
1373 /**
1374 * Returns the effective free-version Filter Set limit for a given post type.
1375 *
1376 * The historical free limit was {@see self::FREE_LIMIT_LEGACY} (3) Filter Sets per post type.
1377 * From {@see self::FREE_LIMIT_CUTOFF_DATE} onwards the new free limit is {@see self::FREE_LIMIT_NEW} (2).
1378 * To avoid breaking existing free-version installs that already relied on the old limit, any post type
1379 * that had at least one published Filter Set created before the cutoff is grandfathered at the legacy
1380 * limit; only post types whose Filter Sets are all post-cutoff (or have none yet) get the new lower limit.
1381 *
1382 * @param string $post_type Target post type (stored in wp_posts.post_excerpt for Filter Sets).
1383 * @return int Effective max number of published Filter Sets allowed for this post type.
1384 */
1385 public function getFreeLimitForPostType($post_type) : int
1386 {
1387 if (empty($post_type)) {
1388 return self::FREE_LIMIT_NEW;
1389 }
1390
1391 global $wpdb;
1392
1393 $legacy_count = (int) $wpdb->get_var(
1394 $wpdb->prepare(
1395 "SELECT COUNT(ID)
1396 FROM {$wpdb->posts}
1397 WHERE post_type = %s
1398 AND post_excerpt = %s
1399 AND post_status = %s
1400 AND post_date < %s",
1401 FLRT_FILTERS_SET_POST_TYPE,
1402 $post_type,
1403 'publish',
1404 self::FREE_LIMIT_CUTOFF_DATE
1405 )
1406 );
1407
1408 return $legacy_count > 0 ? self::FREE_LIMIT_LEGACY : self::FREE_LIMIT_NEW;
1409 }
1410
1411 /**
1412 * Checks whether the free-version limit of published Filter Sets per post type has been reached.
1413 *
1414 * In the PRO version (FLRT_FILTERS_PRO is defined and true) the limit does not apply and the method
1415 * always returns false. In the free version the effective limit comes from {@see self::getFreeLimitForPostType()}
1416 * — {@see self::FREE_LIMIT_LEGACY} (3) for post types grandfathered by Filter Sets created before
1417 * {@see self::FREE_LIMIT_CUTOFF_DATE}, and {@see self::FREE_LIMIT_NEW} (2) otherwise. The method then:
1418 * - resolves the post type from $post_id (or $default_post_type if provided),
1419 * - returns true (over the limit) when there are already enough Filter Sets for that post type and
1420 * the current $post_id is not among the first allowed ones, when switching an existing set to a
1421 * post type that already has enough sets, or when no Filter Set context exists yet and the
1422 * post type already has enough sets,
1423 * - returns false otherwise, i.e. the user is still under the limit and may proceed.
1424 *
1425 * @param int|string $post_id ID of the Filter Set being edited, or empty when creating a new one.
1426 * @param string $default_post_type Post type explicitly chosen in the UI; overrides the value stored on the set.
1427 * @param bool $update Reserved flag for update flows (currently unused inside the method).
1428 * @return bool True when the free-version limit is reached and the action must be blocked, false otherwise.
1429 */
1430 public function under_limit_filter_set($post_id = '', $default_post_type = '', $update = false) : bool
1431 {
1432
1433 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
1434 return false;
1435 }
1436
1437 if (empty($post_id) && empty($default_post_type)) {
1438 return true;
1439 }
1440
1441 global $wpdb;
1442
1443 $get_post_type = [];
1444
1445 if(!empty($post_id)){
1446 $get_post_type = $this->getPostTypeField($post_id);
1447 }
1448
1449
1450 if(empty($default_post_type) && !empty($get_post_type)){
1451 $post_type = (!empty($get_post_type['post_type']['value']) ? $get_post_type['post_type']['value'] : 'post');
1452 }else{
1453 $post_type = $default_post_type;
1454 }
1455
1456 if (!empty($post_type)) {
1457 $limit = $this->getFreeLimitForPostType($post_type);
1458
1459 $count = $wpdb->get_var(
1460 $wpdb->prepare(
1461 "SELECT COUNT(ID)
1462 FROM {$wpdb->posts}
1463 WHERE post_type = %s
1464 AND post_excerpt = %s
1465 AND post_status = %s",
1466 FLRT_FILTERS_SET_POST_TYPE,
1467 $post_type,
1468 'publish',
1469 )
1470 );
1471
1472 $post_ids = $wpdb->get_col(
1473 $wpdb->prepare(
1474 "SELECT ID
1475 FROM {$wpdb->posts}
1476 WHERE post_type = %s
1477 AND post_excerpt = %s
1478 AND post_status = %s
1479 ORDER BY ID ASC
1480 LIMIT %d",
1481 FLRT_FILTERS_SET_POST_TYPE,
1482 $post_type,
1483 'publish',
1484 $limit
1485 )
1486 );
1487
1488 if (!empty($post_id)) {
1489 if ($count >= $limit && !in_array($post_id, $post_ids)) {
1490 return true;
1491 }
1492 }
1493
1494 if(!empty($default_post_type) && !empty($get_post_type['post_type']['value'])){
1495 if($default_post_type === $get_post_type['post_type']['value']){
1496 return false;
1497 }
1498 }
1499
1500 if(!empty($default_post_type) && !empty($get_post_type['post_type']['value'])){
1501 if($default_post_type !== $get_post_type['post_type']['value']){
1502 if($count >= $limit){
1503 return true;
1504 }
1505 }
1506 }
1507
1508 if(empty($get_post_type['post_type']['value']) && $count >= $limit){
1509 return true;
1510 }
1511
1512 }
1513
1514 return false;
1515 }
1516
1517 public function getErrors() {
1518 return $this->errors;
1519 }
1520 }