PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.6
Filter Everything — WordPress & WooCommerce Filters v1.9.6
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 1.6.3 All 51 releases
filter-everything / src / Admin / FilterSet.php

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

1,555 lines 60.2 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 'rangeListValueToRemove' => esc_html__('Remove', 'filter-everything'),
318 'limitForRangeList' => (int)(defined('FLRT_RANGE_LIST_LIMIT') && FLRT_RANGE_LIST_LIMIT) ? FLRT_RANGE_LIST_LIMIT : 20,
319 'rangeListTexts' => [
320 'min_value' => esc_html__('Min Value', 'filter-everything'),
321 'max_value' => esc_html__('Max Value', 'filter-everything'),
322 'label' => esc_html__('Label', 'filter-everything'),
323 'up_to' => esc_html__('Up to', 'filter-everything'),
324 'and_up' => esc_html__('And up', 'filter-everything'),
325 'error' => esc_html__('Error: No fields are filled in. Please fill in at least one field.', 'filter-everything'),
326 'value_error' => esc_html__('Error: The minimum value cannot be greater than or equal to the maximum value.', 'filter-everything'),
327 ],
328 );
329
330 wp_localize_script( 'wpc-filters-admin-filter-set', 'wpcSetVars', $l10n );
331 }
332 }
333
334 public function addCustomLabel( $html, $attributes )
335 {
336 if( isset( $attributes['id'] ) ){
337 if( $attributes['id'] == $this->generateFieldId('post_name') ){
338
339 $spinner = '<span class="spinner"></span>'."\r\n";
340 $openContainer = '<div id="wpc-field-location-container"><span class="wpc-full-width">&nbsp;</span><div class="wpc-field-location-wrapper">'."\r\n";
341 $closeContainer = '</div></div>'."\r\n";
342 $link = '';
343
344 $current_index = isset( $attributes['value'] ) ? $attributes['value'] : '';
345 $options = ! empty( $attributes['options'] ) ? $attributes['options'] : [];
346
347 $data_link = '';
348 $hidden_link_class = ' display-none';
349 if( isset( $options[ $current_index ]['data-link'] ) ){
350 $data_link = esc_attr( $options[ $current_index ]['data-link'] );
351 $hidden_link_class = '';
352 }
353 $link = '<div class="wpc-location-preview-hidden"><a class="wpc-location-preview ' . $hidden_link_class . '" href="'.$data_link.'" ';
354 $link .= 'title="'.esc_attr( esc_html__('Preview the selected location in a new tab', 'filter-everything') ).'" ';
355 $link .= 'target="_blank">';
356 $link .= '<span class="dashicons dashicons-visibility"></span></a></div>';
357 $html = $spinner . $openContainer . $html . $link . $closeContainer;
358 }
359
360 if ( $attributes['id'] == $this->generateFieldId('apply_button_post_name') ) {
361
362 $spinner = '<span class="spinner"></span>'."\r\n";
363 $openContainer = '<div id="wpc-field-apply-button-location-container"><span class="wpc-full-width">&nbsp;</span>'."\r\n";
364 $closeContainer = '</div>'."\r\n";
365 $link = '';
366
367 $html = $spinner . $openContainer . $html . $link . $closeContainer;
368 }
369
370 if( $attributes['id'] == $this->generateFieldId('wp_filter_query') ){
371
372 $spinner = '<span class="spinner"></span>'."\n";
373 $openContainer = '<div id="wpc-field-wp-query-container">&nbsp;'."\n";
374 $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";
375 $closeContainer = '<div id="wpc_query_vars"></div></div>'."\n";
376
377 $html = $spinner . $openContainer . $html . $description . $closeContainer;
378 }
379
380 if( $attributes['id'] == $this->generateFieldId('wp_page_type') ){
381 $label = '<label class="wpc-location-label" for="'.esc_attr($attributes['id']).'">'.esc_html__( 'Apply filtering if the page is:', 'filter-everything' ).'</label>'."\r\n";
382 $html = $label . $html;
383 }
384
385 if( $attributes['id'] == $this->generateFieldId('apply_button_page_type') ){
386 $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";
387 $html = $label . $html;
388 }
389 }
390
391 return $html;
392 }
393
394
395 private function getSpecificFields( $type, $exclude_type = '' )
396 {
397 $particular = [];
398
399 foreach( $this->getFieldsMapping() as $key => $field ){
400 if(!empty($exclude_type) && ! empty( $field[$exclude_type])){
401 continue;
402 }
403 if( isset( $field[$type] ) && ! empty( $field[$type] ) ){
404 $particular[ $key ] = $field;
405 }
406 }
407
408 return $particular;
409 }
410
411 public function getPostTypes()
412 {
413 $allowed_types = [];
414 $post_types = get_post_types( array( 'public' => true ), 'objects' );
415 $exclude = apply_filters( 'wpc_filter_post_types', [] );
416
417 foreach ( $post_types as $type ){
418 if( in_array( $type->name, $exclude ) ){
419 continue;
420 }
421 $allowed_types[$type->name] = isset( $type->labels->name ) ? $type->labels->name : $type->labels->singular_name;
422 }
423
424 return $allowed_types;
425
426 }
427
428 public function getFieldsMapping()
429 {
430 return $this->defaultFields;
431 }
432
433 /**
434 * @var $queriedObject object
435 * @return array (empty array if there is no related set id)
436 */
437 public function findRelevantSets( $queriedObject )
438 {
439 // We need to search all relevantSetS
440 $filterSets = [];
441
442 $filterSets = apply_filters( 'wpc_relevant_set_ids', $filterSets, $queriedObject);
443
444 if( ! empty( $filterSets ) ){
445 foreach ( $filterSets as $set ){
446 if( isset( $set['show_on_the_page'] ) && $set['show_on_the_page'] === true ){
447 $this->addFilterSetsToJsonData($filterSets);
448 return $filterSets;
449 }
450 }
451 }
452
453 // Get main filter set for post type
454 if( isset( $queriedObject['post_types'] ) && ! isset( $queriedObject['post_id'] ) ){
455 foreach( $queriedObject['post_types'] as $post_type ){
456 $sets = $this->getSetIdForPostType( $post_type );
457 if( $sets !== false ){
458 $filterSets = array_merge( $filterSets, $sets );
459 }
460 }
461 }
462
463 $filterSets = apply_filters( 'wpc_return_relevant_set_ids', $filterSets, $queriedObject );
464
465 $this->addFilterSetsToJsonData($filterSets);
466
467 return $filterSets;
468 }
469
470 /**
471 * @var $post_type string - post_type post|product|page|...
472 * @return int|false
473 */
474 public function getSetIdForPostType( $post_type )
475 {
476 if( ! $post_type ){
477 return false;
478 }
479
480 $container = Container::instance();
481 $sets = [];
482 $pll_lang_id = false;
483
484 if ( is_array( $post_type ) ) {
485 $post_type = implode( "_", $post_type );
486 }
487
488 $key = 'set_' . $post_type;
489
490 if( ! $sets = $container->getParam( $key ) ){
491 global $wpdb;
492 $is_fitler_set_translatable = false;
493
494 if( flrt_wpml_active() ){
495 $wpml_settings = get_option( 'icl_sitepress_settings' );
496 if( isset( $wpml_settings['custom_posts_sync_option'][FLRT_FILTERS_SET_POST_TYPE] ) ){
497 if( $wpml_settings['custom_posts_sync_option'][FLRT_FILTERS_SET_POST_TYPE] === '1' ){
498 $is_fitler_set_translatable = true;
499 }
500 }
501 }
502
503 $sql[] = "SELECT {$wpdb->posts}.ID,{$wpdb->posts}.post_content,{$wpdb->posts}.post_excerpt,{$wpdb->posts}.post_name";
504 $sql[] = "FROM {$wpdb->posts}";
505
506 if ( flrt_wpml_active() && defined('ICL_LANGUAGE_CODE') && $is_fitler_set_translatable ) {
507 $sql[] = "LEFT JOIN {$wpdb->prefix}icl_translations AS wpml_translations";
508 $sql[] = "ON {$wpdb->posts}.ID = wpml_translations.element_id";
509 $sql[] = "AND wpml_translations.element_type IN(";
510 $sql[] = $wpdb->prepare( "CONCAT('post_', '%s')", FLRT_FILTERS_SET_POST_TYPE );
511 $sql[] = ")";
512 }
513
514 // Check common if Polylang PRO is active and Filter Set is translatable post type
515 if( flrt_pll_pro_active() && defined('FLRT_ALLOW_PLL_TRANSLATIONS') && FLRT_ALLOW_PLL_TRANSLATIONS ){
516 if( function_exists('pll_current_language') && function_exists('pll_the_languages') ) {
517 $pll_current_language = pll_current_language();
518 $pll_languages = pll_the_languages( array('raw' => 1 ) );
519 if( $pll_current_language && isset( $pll_languages[ $pll_current_language ]['id'] ) ){
520 $pll_lang_id = $pll_languages[ $pll_current_language ]['id'];
521
522 $sql[] = "LEFT JOIN {$wpdb->term_relationships}";
523 $sql[] = "ON ({$wpdb->posts}.ID = {$wpdb->term_relationships}.object_id)";
524 }
525 }
526 }
527
528 $sql[] = "WHERE 1=1";
529 $sql[] = "AND ( {$wpdb->posts}.post_name IN ( '1' )";
530
531 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
532 $sql[] = "OR {$wpdb->posts}.ID IN ( ";
533 $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
534 $sql[] = "LEFT JOIN {$wpdb->postmeta} ON ( {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id )";
535 $sql[] = "WHERE 1=1";
536 $sql[] = $wpdb->prepare( "AND {$wpdb->postmeta}.meta_key = %s", FLRT_APPLY_BUTTON_META_KEY);
537 $sql[] = "AND {$wpdb->postmeta}.meta_value IN ( '1' )";
538 $sql[] = ")";
539 }
540
541 $sql[] = ")";
542 $sql[] = $wpdb->prepare("AND {$wpdb->posts}.post_type = '%s'", FLRT_FILTERS_SET_POST_TYPE );
543 $sql[] = $wpdb->prepare("AND {$wpdb->posts}.post_excerpt = '%s'", $post_type );
544
545 $sql[] = "AND ( ({$wpdb->posts}.post_status = 'publish') )";
546
547 if( flrt_wpml_active() && defined( 'ICL_LANGUAGE_CODE' ) && $is_fitler_set_translatable ){
548 $sql[] = $wpdb->prepare("AND wpml_translations.language_code = '%s'", ICL_LANGUAGE_CODE );
549 }
550
551 if( flrt_pll_pro_active() && defined('FLRT_ALLOW_PLL_TRANSLATIONS') && FLRT_ALLOW_PLL_TRANSLATIONS ){
552 if( $pll_lang_id ){
553 $sql[] = $wpdb->prepare("AND {$wpdb->term_relationships}.term_taxonomy_id IN (%d)", $pll_lang_id );
554 }
555 }
556
557 $sql[] = "ORDER BY {$wpdb->posts}.menu_order DESC, {$wpdb->posts}.ID ASC";
558
559 if( ! defined('FLRT_FILTERS_PRO') ) {
560 $sql[] = "LIMIT 0, 1";
561 }
562
563 $sql = implode(' ', $sql );
564
565 $setPosts = $wpdb->get_results( $sql, OBJECT );
566
567 if( ! empty( $setPosts ) ){
568 $sets = flrt_is_query_on_page( $setPosts, array( '1' ) );
569 }else{
570 return false;
571 }
572
573 $container->storeParam( $key, $sets );
574
575 }
576
577 return $sets;
578 }
579
580 public function validateSets( $sets )
581 {
582 if( ! is_array( $sets ) || empty( $sets ) ){
583 return false;
584 }
585
586 foreach ( $sets as $i => $set ){
587 if( ! isset( $set['ID'] ) || ! $set['ID'] ){
588 return false;
589 }
590 }
591
592 return true;
593 }
594
595 public function preSaveSet( $post_id, $data )
596 {
597 $postData = Container::instance()->getThePost();
598 $postData = prepare_wpc_filter_set_json_data($postData);
599
600 if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
601 return $post_id;
602 }
603
604 if( wp_is_post_revision( $post_id ) ) {
605 return $post_id;
606 }
607
608 if( $data['post_type'] !== FLRT_FILTERS_SET_POST_TYPE ) {
609 return $post_id;
610 }
611
612 $nonce = filter_input( INPUT_POST, '_flrt_nonce' );
613
614 if( ! $this->verifyNonce( $nonce ) ) {
615 return $post_id;
616 }
617
618 if( ! current_user_can( flrt_plugin_user_caps() ) ) {
619 return $post_id;
620 }
621
622 // Do not fire this function twice, on saving Set Fields action
623 remove_action( 'pre_post_update', [$this, 'preSaveSet'], 10 );
624
625 $set_fields_key = self::FIELD_NAME_PREFIX;
626
627 if( isset( $postData[$set_fields_key] ) && ! empty( $postData[$set_fields_key] ) ){
628 $setFields = $postData[$set_fields_key];
629 $setFields['ID'] = $post_id;
630 $setFields['title'] = isset( $data['post_title'] ) ? $data['post_title'] : '';
631
632 $setFields = apply_filters( 'wpc_pre_save_set_fields', $setFields );
633
634 $setFields = $this->sanitizeSetFields( $setFields );
635
636 if( ! $this->validateSetFields( $setFields ) ){
637 flrt_redirect_to_error( $post_id, $this->errors );
638 }
639 }
640
641
642 return $post_id;
643 }
644
645 public function saveSet( $post_id, $post )
646 {
647 $postData = Container::instance()->getThePost();
648 $postData = prepare_wpc_filter_set_json_data($postData);
649
650 if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
651 return $post_id;
652 }
653
654 if( wp_is_post_revision( $post_id ) ) {
655 return $post_id;
656 }
657
658 if( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
659 return $post_id;
660 }
661
662 $nonce = filter_input( INPUT_POST, '_flrt_nonce' );
663
664 if( ! $this->verifyNonce( $nonce ) ) {
665 return $post_id;
666 }
667
668 if( ! current_user_can( flrt_plugin_user_caps() ) ) {
669 return $post_id;
670 }
671
672 remove_action( 'save_post', array( $this, 'saveSet' ), 10, 2 );
673
674 $filterFields = $this->getFilterFieldService();
675 $saveFiltersTrigger = true;
676 $allFiltersValid = true;
677
678 // Save filter fields
679 if( isset( $postData['wpc_filter_fields'] ) && ! empty( $postData['wpc_filter_fields'] ) ) {
680
681 // Validate filters
682 if( ! $filterFields->validateFilters( $postData['wpc_filter_fields'] ) ) {
683 $saveFiltersTrigger = false;
684 }
685
686 if( $saveFiltersTrigger ) {
687 $filtersToSave = [];
688
689 // loop
690 $filterConfiguredFields = $filterFields->getFieldsMapping();
691 foreach ( $postData['wpc_filter_fields'] as $filterId => $filter ) {
692
693 // Set up checkbox fields if they are empty
694 $filter = $filterFields->prepareFilterCheckboxFields( $filter, $filterFields->getFieldsByType( 'checkbox', $filterConfiguredFields ) );
695
696 // set parent
697 if ( ! $filter['parent'] ) {
698 $filter['parent'] = $post_id;
699 }
700
701 $filter = $filterFields->sanitizeFilterFields( $filter );
702 $filtersToSave[ $filterId ] = $filter;
703
704 if ( ! $filterFields->validateTheFilter( $filter, $filterId ) ) {
705 $allFiltersValid = false;
706 break;
707 }
708
709 }
710
711 // Loop to save
712 if( $allFiltersValid ){
713
714 $update_after_save = [];
715 $old_new_ids = [];
716
717 foreach ( $filtersToSave as $filterId => $filter ){
718 // save filter
719 $saved_filter = $filterFields->saveFilter($filter);
720
721 if( isset( $saved_filter['parent_filter'] ) && isset( $saved_filter['ID'] ) ){
722 if( strpos( $saved_filter['parent_filter'], 'filter_', 0 ) !== false ){
723 $update_after_save[ $saved_filter['ID'] ][] = [
724 'key' => 'parent_filter',
725 'value' => $saved_filter['parent_filter']
726 ];
727 }
728 $old_new_ids[$filterId] = $saved_filter['ID'];
729 }
730 }
731
732 // Update data after saving Filters and getting IDs of new ones.
733 if( ! empty( $update_after_save ) ){
734
735 foreach ( $update_after_save as $filter_post_id => $fields_to_update ){
736 $filter_post_data = get_post( $filter_post_id );
737 $filter_data = maybe_unserialize( $filter_post_data->post_content );
738
739 if( ! $filter_data ){
740 continue;
741 }
742
743 foreach ( $fields_to_update as $field_attr ){
744 if( $field_attr['key'] === 'parent_filter' ){
745 if( isset( $filter_data[ 'parent_filter' ] ) ){
746 if( isset( $old_new_ids[ $field_attr['value'] ] ) ){
747 $filter_data[ $field_attr['key'] ] = $old_new_ids[ $field_attr['value'] ];
748 }
749 }
750 }
751 }
752
753 $to_update = array(
754 'ID' => $filter_post_id,
755 'post_content' => maybe_serialize( $filter_data )
756 );
757
758 // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data.
759 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
760 add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 );
761
762 // Slash data.
763 // WP expects all data to be slashed and will unslash it (fixes '\' character issues)
764 $to_update = wp_slash( $to_update );
765
766 wp_update_post( $to_update );
767
768 remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 );
769 }
770 }
771 }
772 }
773 }
774
775 // Save Set fields
776 $set_fields_key = self::FIELD_NAME_PREFIX;
777
778 if( isset( $postData[$set_fields_key] ) && ! empty( $postData[$set_fields_key] ) ){
779 $setFields = $postData[$set_fields_key];
780 $setFields['ID'] = $post_id;
781 $setFields['title'] = isset( $post->post_title ) ? $post->post_title : '';
782
783 $this->saveSetFields( $setFields );
784 }
785
786 if( ! $saveFiltersTrigger || ! $allFiltersValid ){
787 flrt_redirect_to_error( $post_id, $filterFields->getErrorCodes() );
788 }
789
790 add_action( 'save_post', array( $this, 'saveSet' ), 10, 2 );
791
792 return $post_id;
793 }
794
795
796
797 protected function saveSetFields($setFields ){
798 $post_id = $setFields['ID'];
799
800 $setFields = apply_filters( 'wpc_pre_save_set_fields', $setFields );
801
802 $setFields = $this->sanitizeSetFields( $setFields );
803
804 $setFields = wp_unslash( $setFields );
805
806 // Set up checkbox fields if they are empty
807 $filterFields = $this->getFilterFieldService();
808 /**
809 * @feature It seems we need to move methods 'prepareFilterCheckboxFields' and 'getFieldsByType'
810 * to one level above to parent class
811 */
812 $this->setupDefaultFields();
813 $setFields = $filterFields->prepareFilterCheckboxFields( $setFields, $filterFields->getFieldsByType( 'checkbox', $this->getFieldsMapping()) );
814 $_setFields = $setFields;
815
816 // Remove elements, that shouldn't be serialized
817 flrt_extract_vars( $_setFields, array( 'ID', 'title', 'post_type', 'menu_order', 'post_name', 'wp_filter_query_vars' ) );
818 $menu_order = isset( $setFields['menu_order'] ) ? $setFields['menu_order'] : 0;
819
820 // Create array of data to save.
821 $to_save = array(
822 'ID' => $setFields['ID'],
823 'post_status' => 'publish',
824 'post_type' => FLRT_FILTERS_SET_POST_TYPE,
825 'post_title' => $setFields['title'],
826 'post_content' => maybe_serialize( $_setFields ),
827 'post_excerpt' => $setFields['post_type'],
828 'menu_order' => $menu_order,
829 'post_name' => $setFields['post_name']
830 );
831
832 // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data.
833 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
834
835 $to_save = wp_slash( $to_save );
836
837 add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 );
838
839 // Update or Insert.
840 if( $setFields['ID'] ) {
841 wp_update_post( $to_save );
842 } else {
843 $setFields['ID'] = wp_insert_post( $to_save );
844 }
845
846 remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 );
847 // Update meta_fields
848
849 update_post_meta( $setFields['ID'], 'wpc_filter_set_post_type', $setFields['post_type'] );
850
851 $set_query_vars = NULL;
852 // Save selected wp_query->query_vars
853 if( isset( $setFields['wp_filter_query'] ) ){
854 $filterQueryHash = $setFields['wp_filter_query'];
855 if( isset( $setFields['wp_filter_query_vars'][$filterQueryHash] ) ){
856 $set_query_vars = $setFields['wp_filter_query_vars'][$filterQueryHash];
857 }
858 }
859
860 update_post_meta( $setFields['ID'], 'wpc_filter_set_query_vars', $set_query_vars );
861
862 if( isset( $setFields['apply_button_post_name'] ) ){
863 update_post_meta( $setFields['ID'], FLRT_APPLY_BUTTON_META_KEY, $setFields['apply_button_post_name'] );
864 }
865
866 return $setFields['ID'];
867 }
868
869 private function sanitizeSetFields( $setFields )
870 {
871 if( is_array( $setFields ) ){
872 $sanitizedFields = [];
873
874 foreach ( $setFields as $key => $setField ) {
875 if( is_array( $setField ) ){
876 $sanitizedValue = $setField;
877 }else{
878 $sanitizedValue = esc_html( $setField );
879 }
880
881 $sanitizedFields[ $key ] = $sanitizedValue;
882 }
883
884 if( isset( $sanitizedFields['menu_order'] ) ){
885 $sanitizedFields['menu_order'] = flrt_sanitize_int( $sanitizedFields['menu_order'] );
886 $sanitizedFields['menu_order'] = $sanitizedFields['menu_order'] ? $sanitizedFields['menu_order'] : 0;
887 }
888
889 return $sanitizedFields;
890 }
891
892 return $setFields;
893 }
894
895 private function prepareSetParameters( $set_post )
896 {
897 /**
898 * @feature this should be not so complex. I'm ashamed of this.
899 */
900 if( ! isset( $set_post->ID ) ){
901 return false;
902 }
903
904 $this->setupDefaultFields();
905 $defaults = $this->getFieldsMapping();
906
907 $defaults = apply_filters( 'wpc_prepare_filter_set_parameters', $defaults, $set_post );
908
909 $unserialized = maybe_unserialize( $set_post->post_content );
910
911 // For backward compatibility. From v.1.1.24
912 if( isset( $unserialized['wp_page_type'] ) ){
913 $unserialized['wp_page_type'] = str_replace(":", "___", $unserialized['wp_page_type']);
914 }
915
916 if( empty( $unserialized ) ){
917 $unserialized = [];
918 }
919
920 foreach( $this->getSpecificFields( 'particular' ) as $key => $field ){
921 $unserialized[$key] = $set_post->{$field['particular']};
922 }
923
924 $populated = $this->populateValues( $unserialized, $defaults );
925 $parsed = $this->parseValues( $populated, $defaults );
926
927 // In case if some settings field was missing
928 $parsed = wp_parse_args( $parsed, $defaults );
929 $parsed = apply_filters( 'wpc_filter_before_make_default_set_values', $parsed );
930
931 // Set default values, if there is no saved
932 foreach( $parsed as $field => $params ){
933
934 if( ! isset( $params['value'] ) && isset( $params['default'] )){
935 $parsed[$field]['value'] = $params['default'];
936 }
937 }
938
939 $parsed['ID'] = $set_post->ID;
940
941 return apply_filters( 'wpc_filter_set_prepared_values', $parsed );
942 }
943
944 private function parseValues( $populated, $defaults )
945 {
946 $parsed = [];
947
948 foreach ( $populated as $field_key => $values_array ){
949 // In case if we have saved field, that is absent in fieldsMapping
950 if( ! isset( $defaults[$field_key] ) ){
951 continue;
952 }
953
954 if( isset( $values_array['value'] ) ){
955 $parsed[$field_key] = wp_parse_args( $values_array, $defaults[$field_key] );
956 }else{
957 $parsed[$field_key] = $this->parseValues( $values_array, $defaults[$field_key] );
958 }
959 }
960
961 return $parsed;
962 }
963
964 private function populateValues( $saved_values )
965 {
966 $transformed = [];
967
968 foreach ( $saved_values as $field_key => $field_value ) {
969 if( is_array( $field_value ) ){
970 $transformed[ $field_key ] = $this->populateValues( $field_value );
971 }else{
972 $transformed[ $field_key ] = array( 'value' => $field_value );
973 }
974 }
975
976 return $transformed;
977 }
978
979 public function getSet( $ID )
980 {
981 $parameters = [];
982
983 if( ! $ID || empty( $ID ) ){
984 return $parameters;
985 }
986
987 $container = Container::instance();
988 $key = 'wpc_set_' . $ID;
989
990 if( ! $set = $container->getParam( $key ) ){
991 $set_post = get_post( $ID );
992 /**
993 * @feature add this post to cache.
994 */
995 $set = $this->prepareSetParameters( $set_post );
996 $container->storeParam( $key, $set );
997 }
998
999 return $set;
1000 }
1001
1002 public function validateSetFields( $setFields ){
1003
1004 // Validate post_type
1005 if( isset( $setFields['post_type'] ) ){
1006 $postTypes = array_keys( $this->getPostTypes() );
1007 if( ! in_array( $setFields['post_type'], $postTypes, true ) ){
1008 $this->errors[] = 21; // Invalid post type
1009 return false;
1010 }
1011 } else {
1012 $this->errors[] = 21; // Invalid post type
1013 return false;
1014 }
1015
1016 if (!empty($setFields['post_type'])) {
1017 if($this->under_limit_filter_set($setFields['ID'], $setFields['post_type'], true)){
1018 $this->errors[] = 92;
1019 return false;
1020 }
1021 }
1022
1023 // We have to validate wp_page_type before locations field
1024 // because the last one expects valid wp_page_type
1025 if( isset( $setFields['wp_page_type'] ) ){
1026 $possibleWpPageType = apply_filters( 'wpc_validation_wp_page_type_entities', array('common___common') );
1027
1028 if( ! in_array( $setFields['wp_page_type'], $possibleWpPageType ) ){
1029 $this->errors[] = 211; // Invalid WP Page Type
1030 return false;
1031 }
1032 }else{
1033 $this->errors[] = 211; // Invalid WP Page Type
1034 return false;
1035 }
1036
1037 // Validate post_name aka location
1038 // We can not forbid to save "No WP Queries..." option
1039 // Because All archive pages for selected post type may not contain relevant query.
1040 if( isset( $setFields['post_name'] ) ){
1041 $flatEntities = apply_filters( 'wpc_validation_location_entities', array('1'), $setFields );
1042
1043 if( ! in_array( $setFields['post_name'], $flatEntities ) ){
1044 $this->errors[] = 22; // Invalid location
1045 return false;
1046 }
1047
1048 } else {
1049 $this->errors[] = 22; // Invalid location
1050 return false;
1051 }
1052
1053 //Validate wp_filter_query
1054 if( isset( $setFields['wp_filter_query'] ) ){
1055 if(! preg_match('/^[a-f0-9]{32}$/', $setFields['wp_filter_query'] ) && $setFields['wp_filter_query'] !== '-1'){
1056 $this->errors[] = 221; // Invalid WP Filter Query
1057 return false;
1058 }
1059 }else{
1060 $this->errors[] = 221; // Invalid WP Filter Query
1061 return false;
1062 }
1063
1064 // Validate hide_empty
1065 if( isset( $setFields['hide_empty'] ) ){
1066 if( ! in_array( $setFields['hide_empty'], array( 'yes', 'no', 'initial' ), true ) ){
1067 $this->errors[] = 23; // Invalid empty field
1068 return false;
1069 }
1070 }
1071
1072 // Validate show_count
1073 if( isset( $setFields['show_count'] ) ){
1074 if( ! in_array( $setFields['show_count'], array( 'yes', 'no' ), true ) ){
1075 $this->errors[] = 24; // Invalid show count
1076 return false;
1077 }
1078 }
1079
1080 if( isset( $setFields['horizontal_view'] ) ){
1081 if( ! in_array( $setFields['horizontal_view'], array( 'yes', 'no' ), true ) ){
1082 $this->errors[] = 25; // Invalid horizontal view
1083 return false;
1084 }
1085 }
1086
1087 if( isset( $setFields['wp_filter_query_vars'] ) ){
1088 if( ! empty( $setFields['wp_filter_query_vars'] ) ){
1089
1090 foreach ( $setFields['wp_filter_query_vars'] as $query_vars_serialized ){
1091 if( ! is_serialized( $query_vars_serialized ) ){
1092 $this->errors[] = 20; // Common Error
1093 return false;
1094 }
1095 }
1096 }
1097 }
1098
1099 if( isset( $setFields['use_apply_button'] ) ){
1100 if( ! in_array( $setFields['use_apply_button'], array( 'yes', 'no' ), true ) ){
1101 $this->errors[] = 242; // Invalid show count
1102 return false;
1103 }
1104 }
1105
1106 return $setFields;
1107 }
1108
1109 protected function getFilterFieldService()
1110 {
1111 return Container::instance()->getFilterFieldsService();
1112 }
1113
1114 public function getPostTypeField( $post_id )
1115 {
1116 $set = $this->getSet( $post_id );
1117 $field['post_type'] = ( $set['post_type'] ) ? $set['post_type'] : NULL;
1118 return $field;
1119 }
1120
1121 public function getSettingsTypeFields( $post_id )
1122 {
1123 $set = $this->getSet( $post_id );
1124 $settings_fields_map = $this->getSpecificFields('settings', 'location');
1125
1126 return flrt_extract_vars($set, array_keys( $settings_fields_map ) );
1127 }
1128
1129 public function getSettingsLocationTypeFields( $post_id )
1130 {
1131 $set = $this->getSet( $post_id );
1132 $settings_fields_map = $this->getSpecificFields('location');
1133
1134 return flrt_extract_vars($set, array_keys( $settings_fields_map ) );
1135 }
1136
1137 public function generateFieldName( $field_name, $sub_name = '', $index = 0 ){
1138 $attr = self::FIELD_NAME_PREFIX . '['.$field_name.']';
1139 if( $sub_name ){
1140 $attr .= '['.$index.']['.$sub_name.']';
1141 }
1142 return $attr;
1143 }
1144
1145 public function generateFieldId( $field_name, $sub_name = '', $index = 0 ){
1146 $attr = self::FIELD_NAME_PREFIX . '-' . $field_name;
1147 if( $sub_name ){
1148 $attr .= '-' . $sub_name . '-' . $index;
1149 }
1150 return $attr;
1151 }
1152
1153 public static function createNonce()
1154 {
1155 return wp_create_nonce( self::NONCE_ACTION );
1156 }
1157
1158 protected function verifyNonce($nonce )
1159 {
1160 return wp_verify_nonce( $nonce, self::NONCE_ACTION );
1161 }
1162
1163 public function addDuplicateLink(array $actions, \WP_Post $post): array
1164 {
1165 if ($post->post_type !== FLRT_FILTERS_SET_POST_TYPE || !current_user_can('edit_posts')) {
1166 return $actions;
1167 }
1168
1169 $is_pro = defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO;
1170 $new_actions = [];
1171
1172 foreach ($actions as $key => $action) {
1173 $new_actions[$key] = $action;
1174
1175 if ($key === 'edit') {
1176 $new_actions['flrt_duplicate'] = $this->generateDuplicateLinkHtml($post->ID, $is_pro);
1177 }
1178 }
1179
1180 return $new_actions;
1181 }
1182
1183 private function generateDuplicateLinkHtml(int $post_id, bool $is_pro): string
1184 {
1185 $label = esc_html__('Duplicate', 'filter-everything');
1186 $icon = '<span class="flrt-duplicate-filter-set">+</span> ';
1187
1188 if ($is_pro) {
1189 $url = wp_nonce_url(
1190 admin_url('admin.php?action=flrt_duplicate_filter_set&post=' . $post_id),
1191 'flrt_duplicate_filter_set'
1192 );
1193 return sprintf('<a href="%s">%s%s</a>', esc_url($url), $icon, $label);
1194 }
1195
1196 $pro_label = ' (' . esc_html__('PRO', 'filter-everything') . ')';
1197 return sprintf(
1198 '<a href="%s" class="wpc-vailable-in-pro-link">%s%s%s</a>',
1199 esc_url(flrt_vailable_in_pro_attr_link()),
1200 $icon,
1201 $label,
1202 $pro_label
1203 );
1204 }
1205
1206
1207 public function display_auto_filter_set_create_links($which)
1208 {
1209 if ($which !== 'top') {
1210 return;
1211 }
1212
1213 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
1214 if (!$screen || $screen->base !== 'edit' || $screen->post_type !== FLRT_FILTERS_SET_POST_TYPE) {
1215 return;
1216 }
1217
1218 $posts = get_posts([
1219 'post_type' => FLRT_FILTERS_SET_POST_TYPE,
1220 'post_status' => array('publish', 'pending', 'draft', 'future', 'private', 'trash'),
1221 'numberposts' => 1,
1222 'fields' => 'ids',
1223 'no_found_rows' => true,
1224 ]);
1225
1226 if (current_user_can('manage_options')) {
1227 $has_filter_set = true;
1228 if (empty($posts)) {
1229 $has_filter_set = false;
1230 }
1231 flrt_include_admin_view('auto-create-filter', ['nonce_action' => self::NONCE_ACTION, 'has_filter_set' => $has_filter_set]);
1232 }
1233 }
1234
1235 public function handle_create_auto_filter_set()
1236 {
1237 if (!current_user_can('manage_options')) {
1238 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'filter-everything'));
1239 }
1240
1241 if (!isset($_GET['_flrt_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_flrt_nonce'])), self::NONCE_ACTION)) {
1242 wp_die(esc_html__('The request could not be verified. Please try again.', 'filter-everything'));
1243 }
1244 $redirect_url = admin_url( 'edit.php?post_type=filter-set');
1245 $autoFilterSet = new \FilterEverything\Filter\AutoFilterSet($redirect_url);
1246 }
1247
1248 public function adminErrorNotice()
1249 {
1250 if(false !== ($result = get_transient('wpc_auto_filters_error'))){
1251 flrt_view_admin_error($result);
1252 delete_transient('wpc_auto_filters_error');
1253 }
1254 }
1255
1256 public function removePermalinksFromSettings($post_id){
1257 $post = get_post($post_id);
1258 if ( ! $post || $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
1259 return;
1260 }
1261
1262 $existingSlugs = get_option('wpc_filter_permalinks', []);
1263 if ( ! is_array($existingSlugs) ) {
1264 $existingSlugs = [];
1265 }
1266
1267 $child_posts = $this->getAllFilterSetPosts($post_id);
1268
1269 if(!empty($child_posts)){
1270 foreach ($child_posts as $child_post_obj) {
1271 $entityKey = $this->buildEntityKeyFromPostContent($child_post_obj->post_content);
1272 if ($entityKey !== null && isset($existingSlugs[$entityKey])) {
1273 unset($existingSlugs[$entityKey]);
1274 }
1275 }
1276 }
1277
1278 $other_filter_posts = $this->getAllFilterPosts($post_id);
1279 if (!empty($other_filter_posts)){
1280 foreach ($other_filter_posts as $filter_post_obj) {
1281 $entityKey = $this->buildEntityKeyFromPostContent($filter_post_obj->post_content);
1282 if ($entityKey !== null) {
1283 $existingSlugs[$entityKey] = $filter_post_obj->post_name;
1284 }
1285 }
1286 }
1287
1288 update_option('wpc_filter_permalinks', $existingSlugs, true);
1289 }
1290
1291 private function getAllFilterSetPosts($post_id)
1292 {
1293 return get_posts([
1294 'post_type' => FLRT_FILTERS_POST_TYPE,
1295 'posts_per_page' => -1,
1296 'post_status' => 'any',
1297 'post_parent' => $post_id
1298 ]);
1299 }
1300
1301 private function getAllFilterPosts($post_id)
1302 {
1303 $posts = get_posts([
1304 'post_type' => FLRT_FILTERS_POST_TYPE,
1305 'posts_per_page' => -1,
1306 'post_status' => 'publish',
1307 'post_parent__not_in' => [ $post_id ]
1308 ]);
1309
1310 $published_posts = [];
1311
1312 foreach ($posts as $post){
1313 $parent_post = get_post($post->post_parent);
1314 if($parent_post->post_status === 'publish'){
1315 $published_posts[] = $post;
1316 }
1317 }
1318
1319 return $published_posts;
1320 }
1321
1322 public function changeTrashSlug($new_status, $old_status, $post){
1323 if ( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
1324 return;
1325 }
1326 if ( $this->changingTrashSlug ) {
1327 return;
1328 }
1329 if ($old_status === 'trash' && $new_status === 'draft') {
1330 $existingSlugs = get_option('wpc_filter_permalinks', []);
1331 if ( ! is_array($existingSlugs) ) {
1332 $existingSlugs = [];
1333 }
1334
1335 $child_posts = $this->getAllFilterSetPosts($post->ID);
1336 foreach ($child_posts as $child_post_obj) {
1337 $entityKey = $this->buildEntityKeyFromPostContent($child_post_obj->post_content);
1338 if ($entityKey === null) {
1339 continue;
1340 }
1341 if ( empty($existingSlugs[$entityKey]) ) {
1342 continue;
1343 }
1344
1345 $desired_slug = $existingSlugs[$entityKey];
1346 if ( $desired_slug === $child_post_obj->post_name ) {
1347 continue;
1348 }
1349 if ( wp_is_post_revision($child_post_obj->ID) ) {
1350 continue;
1351 }
1352
1353 $update = array(
1354 'ID' => $child_post_obj->ID,
1355 'post_name' => $desired_slug,
1356 );
1357
1358 $this->changingTrashSlug = true;
1359 try {
1360 $result = wp_update_post($update, true);
1361 } finally {
1362 $this->changingTrashSlug = false;
1363 }
1364
1365 if ( is_wp_error($result) || 0 === $result ) {
1366 continue;
1367 }
1368 }
1369 }
1370 }
1371
1372 private function buildEntityKeyFromPostContent($rawPostContent)
1373 {
1374 $content = $this->safelyUnserializePostContent($rawPostContent);
1375 if (empty($content)) {
1376 return null;
1377 }
1378 if (empty($content['entity']) || empty($content['e_name'])) {
1379 return null;
1380 }
1381 return $content['entity'] . '#' . $content['e_name'];
1382 }
1383
1384 private function safelyUnserializePostContent($raw)
1385 {
1386 $content = maybe_unserialize($raw);
1387 return is_array($content) ? $content : [];
1388 }
1389
1390 /**
1391 * Returns the effective free-version Filter Set limit for a given post type.
1392 *
1393 * The historical free limit was {@see self::FREE_LIMIT_LEGACY} (3) Filter Sets per post type.
1394 * From {@see self::FREE_LIMIT_CUTOFF_DATE} onwards the new free limit is {@see self::FREE_LIMIT_NEW} (2).
1395 * To avoid breaking existing free-version installs that already relied on the old limit, any post type
1396 * that had at least one published Filter Set created before the cutoff is grandfathered at the legacy
1397 * limit; only post types whose Filter Sets are all post-cutoff (or have none yet) get the new lower limit.
1398 *
1399 * @param string $post_type Target post type (stored in wp_posts.post_excerpt for Filter Sets).
1400 * @return int Effective max number of published Filter Sets allowed for this post type.
1401 */
1402 public function getFreeLimitForPostType($post_type) : int
1403 {
1404 if (empty($post_type)) {
1405 return self::FREE_LIMIT_NEW;
1406 }
1407
1408 global $wpdb;
1409
1410 $legacy_count = (int) $wpdb->get_var(
1411 $wpdb->prepare(
1412 "SELECT COUNT(ID)
1413 FROM {$wpdb->posts}
1414 WHERE post_type = %s
1415 AND post_excerpt = %s
1416 AND post_status = %s
1417 AND post_date < %s",
1418 FLRT_FILTERS_SET_POST_TYPE,
1419 $post_type,
1420 'publish',
1421 self::FREE_LIMIT_CUTOFF_DATE
1422 )
1423 );
1424
1425 return $legacy_count > 0 ? self::FREE_LIMIT_LEGACY : self::FREE_LIMIT_NEW;
1426 }
1427
1428 /**
1429 * Checks whether the free-version limit of published Filter Sets per post type has been reached.
1430 *
1431 * In the PRO version (FLRT_FILTERS_PRO is defined and true) the limit does not apply and the method
1432 * always returns false. In the free version the effective limit comes from {@see self::getFreeLimitForPostType()}
1433 * — {@see self::FREE_LIMIT_LEGACY} (3) for post types grandfathered by Filter Sets created before
1434 * {@see self::FREE_LIMIT_CUTOFF_DATE}, and {@see self::FREE_LIMIT_NEW} (2) otherwise. The method then:
1435 * - resolves the post type from $post_id (or $default_post_type if provided),
1436 * - returns true (over the limit) when there are already enough Filter Sets for that post type and
1437 * the current $post_id is not among the first allowed ones, when switching an existing set to a
1438 * post type that already has enough sets, or when no Filter Set context exists yet and the
1439 * post type already has enough sets,
1440 * - returns false otherwise, i.e. the user is still under the limit and may proceed.
1441 *
1442 * @param int|string $post_id ID of the Filter Set being edited, or empty when creating a new one.
1443 * @param string $default_post_type Post type explicitly chosen in the UI; overrides the value stored on the set.
1444 * @param bool $update Reserved flag for update flows (currently unused inside the method).
1445 * @return bool True when the free-version limit is reached and the action must be blocked, false otherwise.
1446 */
1447 public function under_limit_filter_set($post_id = '', $default_post_type = '', $update = false) : bool
1448 {
1449
1450 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
1451 return false;
1452 }
1453
1454 if (empty($post_id) && empty($default_post_type)) {
1455 return true;
1456 }
1457
1458 global $wpdb;
1459
1460 $get_post_type = [];
1461
1462 if(!empty($post_id)){
1463 $get_post_type = $this->getPostTypeField($post_id);
1464 }
1465
1466
1467 if(empty($default_post_type) && !empty($get_post_type)){
1468 $post_type = (!empty($get_post_type['post_type']['value']) ? $get_post_type['post_type']['value'] : 'post');
1469 }else{
1470 $post_type = $default_post_type;
1471 }
1472
1473 if (!empty($post_type)) {
1474 $limit = $this->getFreeLimitForPostType($post_type);
1475
1476 $count = $wpdb->get_var(
1477 $wpdb->prepare(
1478 "SELECT COUNT(ID)
1479 FROM {$wpdb->posts}
1480 WHERE post_type = %s
1481 AND post_excerpt = %s
1482 AND post_status = %s",
1483 FLRT_FILTERS_SET_POST_TYPE,
1484 $post_type,
1485 'publish',
1486 )
1487 );
1488
1489 $post_ids = $wpdb->get_col(
1490 $wpdb->prepare(
1491 "SELECT ID
1492 FROM {$wpdb->posts}
1493 WHERE post_type = %s
1494 AND post_excerpt = %s
1495 AND post_status = %s
1496 ORDER BY ID ASC
1497 LIMIT %d",
1498 FLRT_FILTERS_SET_POST_TYPE,
1499 $post_type,
1500 'publish',
1501 $limit
1502 )
1503 );
1504
1505 if (!empty($post_id)) {
1506 if ($count >= $limit && !in_array($post_id, $post_ids)) {
1507 return true;
1508 }
1509 }
1510
1511 if(!empty($default_post_type) && !empty($get_post_type['post_type']['value'])){
1512 if($default_post_type === $get_post_type['post_type']['value']){
1513 return false;
1514 }
1515 }
1516
1517 if(!empty($default_post_type) && !empty($get_post_type['post_type']['value'])){
1518 if($default_post_type !== $get_post_type['post_type']['value']){
1519 if($count >= $limit){
1520 return true;
1521 }
1522 }
1523 }
1524
1525 if(empty($get_post_type['post_type']['value']) && $count >= $limit){
1526 return true;
1527 }
1528
1529 }
1530
1531 return false;
1532 }
1533
1534 public function getErrors() {
1535 return $this->errors;
1536 }
1537
1538 private function addFilterSetsToJsonData($filterSets)
1539 {
1540 global $flrt_json_data;
1541 foreach ($filterSets as $set) {
1542 $setId = $set['ID'];
1543 $setSettings = $this->getSet($setId);
1544
1545 if(!isset($flrt_json_data[$setId])){
1546 $flrt_json_data[$setId] = [];
1547 }
1548
1549 $flrt_json_data[$setId]['settings'] = array_map(
1550 fn($item) => is_array($item) ? $item['value'] : $item,
1551 $setSettings
1552 );
1553 }
1554 }
1555 }