PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.7
Filter Everything — WordPress & WooCommerce Filters v1.9.7
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 / Plugin.php

Plugin.php in Filter Everything — WordPress & WooCommerce Filters 1.9.7, at src/Plugin.php

1,558 lines 70.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 use FilterEverything\Filter\Pro\Api\ApiRequests;
10 use FilterEverything\Filter\Shortcodes;
11 use FilterEverything\Filter\Pro\PluginPro;
12 use FilterEverything\Filter\DefaultSettings;
13 use FilterEverything\Filter\PluginHelpers;
14
15 class Plugin
16 {
17 use PluginHelpers;
18 private $wpManager;
19
20 public function __construct()
21 {
22 $this->wpManager = Container::instance()->getWpManager();
23 $this->wpManager->init();
24 $this->register_hooks();
25
26 new Shortcodes();
27
28 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
29 new PluginPro();
30 }
31
32 if( is_admin() ){
33 new Admin();
34 }
35
36 // «Plugin notifications» channel. Not admin-only: its daily fetch runs in WP-Cron.
37 new Messages();
38 }
39
40 public function register_hooks(){
41 /**
42 * string
43 */
44 $getData = Container::instance()->getTheGet();
45
46 if( ! is_admin() ){
47 // Priority 1000: run AFTER third-party routers (e.g. Brain\Cortex inside
48 // WP User Manager, priority 100). When Cortex ran after us, it received
49 // our "false", found no route of its own and re-ran $wp->query_posts(),
50 // wiping the filtered main query with an unfiltered one.
51 add_filter( 'do_parse_request', array( $this->wpManager, 'customParseRequest' ), 1000, 3 );
52
53 add_action( 'parse_request', array( $this->wpManager, 'parseRequest' ) );
54 add_action( 'pre_get_posts', array( $this->wpManager, 'addFilterQueryToWpQuery' ), 9999 );
55
56 add_filter( 'posts_where', [ $this->wpManager, 'fixPostsWhereForSearch' ], 10, 2 );
57 add_filter( 'post_limits_request', [ $this->wpManager, 'addSQlComment' ], 10, 2 );
58 add_action( 'pre_get_posts', array( $this->wpManager, 'fixSearchPostType' ), 9999 );
59
60 add_action( 'template_redirect', [ $this, 'prepareEntities' ] );
61
62 add_action( 'wpc_filtered_query_end', [ $this, 'addSearchArgsToWpQuery' ] );
63 add_action( 'wpc_all_set_wp_queried_posts', [ $this, 'addSearchArgsToWpQuery' ] );
64
65 add_filter( 'posts_where', [ $this, 'postDateWhere' ], 10000, 2 );
66
67 if ( flrt_is_woocommerce() ){
68 add_action( 'woocommerce_product_query', 'flrt_remove_product_query_post_clauses', 10, 2 );
69 add_filter( 'posts_search', [$this, 'addSkuSearchSql'], 10000, 2 );
70 }
71
72 $sorting = new Sorting();
73 $sorting->registerHooks();
74 }
75
76 add_action( 'body_class', array( $this, 'bodyClass' ) );
77
78 add_action( 'admin_print_styles', array( $this, 'includeAdminCss' ) );
79 add_action( 'admin_print_scripts', array( $this, 'includeAdminJs' ) );
80
81 // Do not include JS, if this page is admin or can't contain filters
82 if( ! is_admin() && ! is_login() ){
83 add_action( 'wp_head', [ $this, 'inlineFrontCss' ] );
84 add_action( 'wp_print_styles', array( $this, 'includeFrontCss' ) );
85 add_action( 'wp_print_scripts', array( $this, 'includeFrontJs' ) );
86 add_action( 'wp_print_styles', array( $this, 'dynamicFrontCss' ) );
87 }
88
89 add_action( 'wp_footer', [$this, 'footerHtml'] );
90
91 if( ! defined('FLRT_FILTERS_PRO') ) {
92 add_action( 'wp_head', array($this, 'noIndexFilterPages'), 1 );
93 add_filter( 'wpc_filter_set_default_fields', [ $this, 'addAvailableInProFields' ], 10, 2 );
94 add_filter( 'wpc_pre_save_set_fields', [ $this, 'unsetAvailableInProFields' ] );
95 }
96
97 add_filter( 'wpc_filter_set_default_fields', [ $this, 'addSetTailFields' ], 20, 2 );
98
99 // Disable single search result redirect
100 add_filter( 'woocommerce_redirect_single_search_result', '__return_false' );
101
102 add_action( 'save_post', [$this, 'resetTransitions'] );
103 add_action( 'delete_post', [$this, 'resetTransitions'] );
104 add_action( 'woocommerce_ajax_save_product_variations', [$this, 'resetTransitions'] );
105 // Term changes bypass save_post but alter the filter data (term names,
106 // slugs, product assignments via wp-cli/imports) — same invalidation
107 add_action( 'created_term', [$this, 'resetTransitions'] );
108 add_action( 'edited_term', [$this, 'resetTransitions'] );
109 add_action( 'delete_term', [$this, 'resetTransitions'] );
110
111 if( isset( $getData['reset_filters_cache'] ) && $getData['reset_filters_cache'] == true ){
112 $this->resetTransitions();
113 }
114
115 add_action( 'wpc_before_filter_set_settings_fields', [$this, 'removeApplyButtonOrderField'] );
116 add_filter( 'wpc_filter_set_prepared_values', [$this, 'handleFilterSetFieldsVisibility'] );
117
118 add_action( 'wpc_cycle_filter_fields', [$this, 'showCombinedFields'], 10, 2 );
119
120 add_action( 'pre_get_posts', [$this, 'burpOutAllWpQueries'], 9999 );
121
122 add_action('wp_ajax_wpc-get-set-location-terms', [$this, 'sendSetLocationTerms']);
123
124 add_filter('wpc_relevant_set_ids', [$this, 'findRelevantSets'], 10, 2);
125 add_filter('wpc_is_filtered_query_free', [$this, 'isFilteredQuery'], 10, 2);
126
127
128 add_filter('wpc_prepare_filter_set_parameters', [$this, 'prepareSetParameters'], 10, 2);
129
130 add_filter('wpc_filter_before_make_default_set_values', [$this, 'legacyPrepareWpPageTypeValue'] );
131
132 add_filter('wpc_validation_wp_page_type_entities', [$this, 'validationWpPageTypeEntities'] );
133
134 add_filter('wpc_validation_location_entities', [$this, 'validationLocationEntities'], 10, 2);
135
136 add_action( 'wpc_before_filter_set_settings_location_fields', [$this, 'showLocationFields'] );
137
138 add_filter('manage_edit-' . FLRT_FILTERS_SET_POST_TYPE . '_columns', array($this, 'filterSetPostTypeCol'));
139 add_action('manage_' . FLRT_FILTERS_SET_POST_TYPE . '_posts_custom_column', array($this, 'filterSetPostTypeColContent'), 10, 2);
140
141 $woo_shortcodes = array(
142 'products',
143 'featured_products',
144 'sale_products',
145 'best_selling_products',
146 'recent_products',
147 'product_attribute',
148 'top_rated_products'
149 );
150
151 // Set first install timestamp
152 $first_install = get_option( 'wpc_first_install' );
153 if ( ! $first_install ){
154 $first_install = [];
155 $first_install['install_time'] = time();
156 $first_install['rate_disabled'] = false;
157 $first_install['rate_delayed'] = false;
158 update_option( 'wpc_first_install', $first_install );
159 }
160
161 // Fix caching problem for products queried by shortcode
162 foreach ( $woo_shortcodes as $woo_shortcode ){
163 add_filter( "shortcode_atts_{$woo_shortcode}", [$this, 'disableCacheProductsShortcode'] );
164 }
165 }
166
167 public function resetTransitions()
168 {
169 $em = Container::instance()->getEntityManager();
170 $all_filters = $em->getGlobalConfiguredSlugs();
171
172 if( is_array( $all_filters ) ){
173 foreach ( $all_filters as $entityEname => $slug ){
174 // For terms it should be entity name
175 $parts = explode( '#', $entityEname, 2 );
176 $e_name = isset( $parts[1] ) ? $parts[1] : '';
177 $type = isset( $parts[0] ) ? $parts[0] : '';
178
179 if ( in_array( $type, [ 'post_meta_num', 'tax_numeric' ] ) ) {
180 // Entity code appends the queried post types between the e_name and
181 // the format suffix (wpc_terms_post_meta_num__price_product_product_variation_v2),
182 // so the exact key can not be rebuilt here - match on the unsuffixed base
183 $this->deleteTermsTransientsLike( 'wpc_terms_' . $type . '_' . $e_name );
184 }
185
186 if ( in_array( $type, [ 'post_date' ] ) ) {
187 $this->deleteTermsTransientsLike( 'wpc_terms_post_date_' );
188 }
189
190 if ( in_array( $type, [ 'post_meta_date' ] ) ) {
191 $this->deleteTermsTransientsLike( 'wpc_terms_post_meta_date_' );
192 }
193
194 if ( $type === 'post_meta_exists' ) {
195 delete_transient( flrt_get_post_ids_transient_key( $e_name .'_yes' ) );
196 delete_transient( flrt_get_post_ids_transient_key( $e_name .'_no' ) );
197 }
198
199 // With an external object cache transients are not in the options
200 // table and the LIKE lookups above find nothing, so always delete
201 // the post-type-less key variant directly as well
202 $terms_transient_key = flrt_get_terms_transient_key( $type . '_'. $e_name );
203 $post_ids_transient_key = flrt_get_post_ids_transient_key( $slug );
204 $var_meta_transient_key = flrt_get_variations_transient_key( 'attribute_'. $e_name );
205
206 delete_transient( $terms_transient_key );
207 delete_transient( $post_ids_transient_key );
208 delete_transient( $var_meta_transient_key );
209 }
210 }
211
212 $variations_key = 'wpc_posts_variations' . FLRT_CACHE_FORMAT_SUFFIX;
213 $filter_key = 'wpc_filters_query';
214
215 delete_transient($variations_key);
216 delete_transient($filter_key);
217
218 // Static filter-data files (see maybeWriteJsonBlobFile): bump the
219 // version so new URLs are minted, and remove the now-unused files
220 update_option( 'flrt_json_blob_ver', time(), false );
221 $uploads = wp_upload_dir();
222 if ( empty( $uploads['error'] ) ) {
223 $blob_files = glob( trailingslashit( $uploads['basedir'] ) . 'flrt-cache/filters-*.json' );
224 if ( is_array( $blob_files ) ) {
225 foreach ( $blob_files as $blob_file ) {
226 @unlink( $blob_file );
227 }
228 }
229 }
230
231 unset( $terms_transient_key, $post_ids_transient_key, $var_meta_transient_key, $all_filters, $em );
232 }
233
234 /**
235 * Deletes all DB-stored transients whose name contains the given base key.
236 * Used by resetTransitions() for term caches whose full key includes parts
237 * unknown at reset time (queried post types, language code, format suffix).
238 */
239 private function deleteTermsTransientsLike( $base_key )
240 {
241 global $wpdb;
242
243 $like = '%' . $wpdb->esc_like( $base_key ) . '%';
244 $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $like ) );
245
246 if ( ! is_array( $names ) ) {
247 return;
248 }
249
250 $keys = [];
251 foreach ( $names as $name ) {
252 $keys[] = str_replace( [ '_transient_timeout_', '_transient_' ], '', $name );
253 }
254
255 foreach ( array_unique( $keys ) as $key ) {
256 delete_transient( $key );
257 }
258 }
259
260 public function prepareEntities()
261 {
262 $wpManager = Container::instance()->getWpManager();
263 $em = Container::instance()->getEntityManager();
264 $sets = $wpManager->getQueryVar('wpc_page_related_set_ids');
265
266 if( $sets ){
267 foreach( $sets as $set ){
268 $em->prepareEntitiesToDisplay( array( $set ) );
269 }
270 }
271 }
272
273
274 public function addAvailableInProFields( $fields, $filterSet )
275 {
276 foreach ( $fields as $key => $attributes ){
277 // Always insert regular 'old' field
278 $new_fields[$key] = $attributes;
279
280 if( $key === 'show_count' ){
281
282 $new_fields['instead_custom_posts_container'] = array(
283 'type' => 'inProButton',
284 'label' => esc_html__('Results container', 'filter-everything'),
285 'pro_label' => flrt_pro_promo_label(),
286 'name' => $filterSet->generateFieldName('instead_custom_posts_container'),
287 'id' => $filterSet->generateFieldId('instead_custom_posts_container'),
288 'default' => '',
289 'settings' => true
290 );
291
292 }
293
294 if( $key === 'hide_empty' ){
295 $new_fields['instead_hide_empty_filter_container'] = array(
296 'type' => 'inProButton',
297 'label' => esc_html__('Hide empty Filters', 'filter-everything'),
298 'pro_label' => flrt_pro_promo_label(),
299 'name' => $filterSet->generateFieldName('instead_custom_posts_container'),
300 'id' => $filterSet->generateFieldId('instead_custom_posts_container'),
301 'default' => '',
302 'instructions' => esc_html__('Hide the Entire Filter if no one term contains posts', 'filter-everything'),
303 'settings' => true
304 );
305
306 }
307
308 }
309
310 return $new_fields;
311 }
312
313 public function addSetTailFields( $fields, $filterSet )
314 {
315 $new_fields = [];
316 // In PRO the tail block goes right after hide_empty_filter, which pushes
317 // custom_posts_container below the horizontal filters — the same position
318 // its free-mode teaser stub has
319 $insert_after = defined('FLRT_FILTERS_PRO') ? 'hide_empty_filter' : 'show_count';
320
321 foreach ( $fields as $key => $attributes ){
322 // Always insert regular 'old' field
323 $new_fields[$key] = $attributes;
324
325 if( $key === $insert_after ){
326
327 $new_fields['use_search_field'] = array(
328 'type' => 'Checkbox',
329 'label' => esc_html__('Enable Search Field', 'filter-everything'),
330 'name' => $filterSet->generateFieldName('use_search_field'),
331 'id' => $filterSet->generateFieldId('use_search_field'),
332 'class' => 'wpc-field-use-search-field',
333 'default' => 'no',
334 'instructions' => esc_html__('Allows you to search by text among filtered posts', 'filter-everything'),
335 'settings' => true
336 );
337
338 $new_fields['search_field_menu_order'] = array(
339 'type' => 'Hidden',
340 'label' => '',
341 'class' => 'wpc-menu-order-field',
342 'id' => $filterSet->generateFieldId('search_field_menu_order'),
343 'name' => $filterSet->generateFieldName('search_field_menu_order'),
344 'default' => -1,
345 'settings' => true
346 );
347
348 $new_fields['search_field_label'] = array(
349 'type' => 'Text',
350 'label' => esc_html__('Search Field Title', 'filter-everything'),
351 'name' => $filterSet->generateFieldName('search_field_label'),
352 'id' => $filterSet->generateFieldId('search_field_label'),
353 'class' => 'wpc-search-field-label',
354 'default' => esc_html__('Search', 'filter-everything'),
355 'instructions' => esc_html__('Specify if needed or leave empty', 'filter-everything'),
356 'settings' => true
357 );
358
359 $new_fields['search_field_placeholder'] = array(
360 'type' => 'Text',
361 'label' => esc_html__('Search Field Placeholder', 'filter-everything'),
362 'name' => $filterSet->generateFieldName('search_field_placeholder'),
363 'id' => $filterSet->generateFieldId('search_field_placeholder'),
364 'class' => 'wpc-search-field-placeholder',
365 'placeholder' => esc_html__( 'e.g. Search products, Search posts', 'filter-everything' ),
366 'default' => '',
367 'settings' => true
368 );
369
370 $new_fields['use_apply_button'] = array(
371 'type' => 'Checkbox',
372 'label' => esc_html__('«Apply Button» mode', 'filter-everything'),
373 'name' => $filterSet->generateFieldName('use_apply_button'),
374 'id' => $filterSet->generateFieldId('use_apply_button'),
375 'class' => 'wpc-field-use-apply-button',
376 'default' => 'no',
377 'instructions' => esc_html__('Enables filtering by clicking the Apply button', 'filter-everything'),
378 'settings' => true
379 );
380
381 $new_fields['apply_button_menu_order'] = array(
382 'type' => 'Hidden',
383 'label' => '',
384 'class' => 'wpc-menu-order-field',
385 'id' => $filterSet->generateFieldId('apply_button_menu_order'),
386 'name' => $filterSet->generateFieldName('apply_button_menu_order'),
387 'default' => -1,
388 'settings' => true
389 );
390
391 $new_fields['apply_button_text'] = array(
392 'type' => 'Text',
393 'label' => esc_html__('Apply Button label', 'filter-everything'),
394 'name' => $filterSet->generateFieldName('apply_button_text'),
395 'id' => $filterSet->generateFieldId('apply_button_text'),
396 'class' => 'wpc-field-apply-button-text',
397 'default' => esc_html__('Apply', 'filter-everything'),
398 'settings' => true
399 );
400
401 $new_fields['reset_button_text'] = array(
402 'type' => 'Text',
403 'label' => esc_html__('Reset Button label', 'filter-everything'),
404 'name' => $filterSet->generateFieldName('reset_button_text'),
405 'id' => $filterSet->generateFieldId('reset_button_text'),
406 'class' => 'wpc-field-reset-button-text',
407 'default' => esc_html__('Reset', 'filter-everything'),
408 'settings' => true
409 );
410
411 $new_fields['horizontal_view'] = array(
412 'type' => 'Checkbox',
413 'label' => esc_html__('Horizontal filters', 'filter-everything'),
414 'name' => $filterSet->generateFieldName('horizontal_view'),
415 'id' => $filterSet->generateFieldId('horizontal_view'),
416 'class' => 'wpc-field-horizontal-view-text',
417 'default' => 'no',
418 'instructions' => esc_html__('Display filters side by side instead of one per row', 'filter-everything'),
419 'settings' => true
420 );
421 $columns_options = [];
422 for ( $i = 2; $i <= 5; $i++ ){
423 $columns_options[(string)$i] = (string)$i;
424 }
425 $new_fields['horizontal_view_column'] = array(
426 'type' => 'Select',
427 'label' => esc_html__('Number of columns', 'filter-everything'),
428 'class' => 'wpc-field-horizontal-view-column',
429 'id' => $filterSet->generateFieldId('horizontal_view_column'),
430 'name' => $filterSet->generateFieldName('horizontal_view_column'),
431 'options' => $columns_options,
432 'default' => '3',
433 'instructions' => esc_html__('How many columns to use in horizontal mode', 'filter-everything'),
434 'settings' => true
435 );
436
437 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
438 $new_fields['horizontal_view_priority'] = array(
439 'type' => 'Select',
440 'label' => esc_html__('Horizontal priority', 'filter-everything'),
441 'class' => 'wpc-field-horizontal-view-priority',
442 'id' => $filterSet->generateFieldId('horizontal_view_priority'),
443 'name' => $filterSet->generateFieldName('horizontal_view_priority'),
444 'options' => ['widget' => 'widget', 'filter_set' => 'filter_set'],
445 'default' => ($screen && $screen->base === 'post' && $screen->action === 'add') ? 'filter_set' : 'widget',
446 'instructions' => esc_html__('How many columns to use in horizontal mode', 'filter-everything'),
447 'settings' => true,
448 );
449 }
450 }
451
452 return $new_fields;
453 }
454
455 public function unsetAvailableInProFields( $setFields )
456 {
457 unset( $setFields['instead_post_name'], $setFields['instead_custom_posts_container'] );
458
459 return $setFields;
460 }
461
462 public function noIndexFilterPages()
463 {
464 if( flrt_is_filter_request() ){
465 $robots['index'] = 'noindex';
466 $robots['follow'] = 'nofollow';
467 $content = implode(', ', $robots );
468 echo sprintf('<meta name="robots" content="%s">', $content)."\r\n";
469 }
470 }
471
472 /**
473 * @see DynamicCss::printInlineCss() (kept as the wp_head callback name)
474 */
475 public function inlineFrontCss()
476 {
477 ( new DynamicCss() )->printInlineCss();
478 }
479
480 /**
481 * @see DynamicCss::enqueueDynamicCss() (kept as the wp_print_styles callback name)
482 */
483 public function dynamicFrontCss()
484 {
485 ( new DynamicCss() )->enqueueDynamicCss();
486 }
487
488 public function bodyClass( $classes )
489 {
490 if( flrt_get_option('mobile_filter_settings') === 'show_open_close_button' ){
491 $classes[] = 'wpc_show_open_close_button';
492 }
493
494 if( flrt_is_filter_request() ){
495 $classes[] = 'wpc_is_filter_request';
496 }
497
498 return $classes;
499 }
500
501 public static function activate()
502 {
503 $defaultSettings = new DefaultSettings();
504 if ( ! get_option('wpc_filter_settings') ) {
505 add_option('wpc_filter_settings', $defaultSettings->wpc_filter_settings() );
506 }
507
508 if( ! get_option( 'wpc_filter_experimental' ) ){
509 add_option('wpc_filter_experimental', $defaultSettings->wpc_filter_experimental() );
510 }
511
512 // Stamp the current version on fresh installs so 'update'-triggered
513 // admin notices (see AdminNotices) fire only when an existing install is
514 // later updated, never on a brand-new installation.
515 if ( ! get_option( 'flrt_version' ) ) {
516 add_option( 'flrt_version', FLRT_PLUGIN_VER );
517 }
518 // When the plugin arrived — Messages keeps promos quiet for a day after it.
519 if ( ! get_option( Messages::INSTALLED_AT_OPTION ) ) {
520 add_option( Messages::INSTALLED_AT_OPTION, time(), '', false );
521 }
522
523 // PRO: put the managed block back into a physical robots.txt right away
524 // (deactivate() removed it) instead of waiting for the next trigger.
525 if ( function_exists( 'flrt_robots_file_sync' ) ) {
526 flrt_robots_file_sync( 'activate' );
527 }
528 }
529
530 /**
531 * Deactivation: PRO takes its managed block out of the physical robots.txt
532 * and stops the daily sync cron. Options and posts are kept for the
533 * next activation — that is uninstall()'s job.
534 */
535 public static function deactivate()
536 {
537 if ( function_exists( 'flrt_robots_file_teardown' ) ) {
538 flrt_robots_file_teardown();
539 }
540
541 Messages::teardown();
542 }
543
544 /**
545 * Clears all plugin data: options and posts
546 */
547 /**
548 * Removes the static filter-data cache (uploads/flrt-cache) with its
549 * folder. Defensive on purpose: the path must look exactly like our own
550 * folder, only known file patterns are deleted, every step is
551 * error-suppressed and any unexpected failure is swallowed — the cleanup
552 * is best-effort and must never break the uninstall process.
553 */
554 private static function deleteJsonBlobCache()
555 {
556 try {
557 $uploads = wp_upload_dir();
558 if ( ! empty( $uploads['error'] ) || empty( $uploads['basedir'] ) ) {
559 return;
560 }
561
562 $dir = trailingslashit( $uploads['basedir'] ) . 'flrt-cache';
563
564 if ( substr( $dir, -strlen( '/flrt-cache' ) ) !== '/flrt-cache' || ! is_dir( $dir ) ) {
565 return;
566 }
567
568 $blob_files = glob( $dir . '/filters-*.json' );
569 $tmp_files = glob( $dir . '/filters-*.json.*.tmp' );
570
571 $to_delete = array_merge(
572 is_array( $blob_files ) ? $blob_files : [],
573 is_array( $tmp_files ) ? $tmp_files : []
574 );
575 $to_delete[] = $dir . '/index.php';
576
577 foreach ( $to_delete as $file ) {
578 if ( is_file( $file ) ) {
579 @unlink( $file );
580 }
581 }
582
583 // Removed only when empty — anything unexpected inside stays put
584 @rmdir( $dir );
585 } catch ( \Throwable $e ) {
586 // Best-effort cleanup: never break the uninstall
587 }
588 }
589
590 public static function uninstall( $force = false )
591 {
592 $allow_to_delete = true;
593
594 if( is_multisite() ){
595 $active_plugins = get_site_option('active_sitewide_plugins');
596 if( is_array( $active_plugins ) ){
597 $active_plugins = array_keys( $active_plugins );
598 }
599 }else{
600 $active_plugins = apply_filters('active_plugins', get_option('active_plugins'));
601 }
602
603 $fe_active = [];
604 $to_compare = [
605 'filter-everything-pro/filter-everything.php',
606 'filter-everything/filter-everything.php'
607 ];
608
609 if( ! empty( $active_plugins ) ){
610 foreach ( $active_plugins as $plugin_path ){
611 if( in_array( $plugin_path, $to_compare ) ){
612 $fe_active[] = $plugin_path;
613 }
614 }
615 }
616
617 if( count( $fe_active ) > 0 ){
618 $allow_to_delete = false;
619 }
620
621 if ( $force == true ) {
622 $allow_to_delete = true;
623 }
624
625 if( $allow_to_delete ){
626
627 if ( function_exists( 'flrt_robots_file_teardown' ) ) {
628 flrt_robots_file_teardown();
629 }
630
631 $options = [
632 'wpc_robots_file_state',
633 'wpc_filter_settings',
634 'wpc_indexing_deep_settings',
635 'wpc_filter_permalinks',
636 'wpc_seo_rules_settings',
637 'wpc_xml_write_date',
638 'wpc_filter_experimental',
639 'flrt_json_blob_ver',
640 'widget_wpc_filters_widget',
641 'widget_wpc_sorting_widget',
642 'widget_wpc_chips_widget',
643 ];
644
645 foreach ( $options as $option_name ){
646 delete_option( $option_name );
647 }
648
649 self::deleteJsonBlobCache();
650
651 // «Plugin notifications»: cron event, cached feed, consent, per-user dismissals
652 Messages::uninstall();
653 if ( class_exists( __NAMESPACE__ . '\\ReviewRequest' ) ) {
654 ReviewRequest::uninstall();
655 }
656
657 // Deactivate and erase license if exists
658 if ( defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) {
659
660 wpc_clear_folder(FLRT_XML_PATH);
661
662 $to_send = false;
663 $saved_value = get_option( FLRT_LICENSE_KEY );
664
665 if( isset( $saved_value['license_key'] ) ) {
666 $saved_value_arr = maybe_unserialize( base64_decode( $saved_value['license_key'] ) );
667 $to_send = $saved_value_arr;
668 }
669
670 if ( is_array( $to_send ) ){
671 $to_send['home_url'] = home_url();
672
673 // Make data suitable to send as GET variables
674 $to_send = array_map( 'urlencode', $to_send );
675
676 if ( isset( $saved_value_arr['id'] ) && $saved_value_arr['id'] ) {
677 $apiRequest = new ApiRequests();
678 $result = $apiRequest->sendRequest('DELETE', 'license', $to_send );
679
680 // If license was deactivated, we have to refresh updates info
681 delete_transient(FLRT_VERSION_TRANSIENT );
682 delete_option( FLRT_LICENSE_KEY );
683 }
684 }
685
686 }
687
688 $postTypes = array(
689 FLRT_FILTERS_SET_POST_TYPE,
690 FLRT_FILTERS_POST_TYPE
691 );
692
693 if( defined( 'FLRT_SEO_RULES_POST_TYPE' ) ){
694 $postTypes[] = FLRT_SEO_RULES_POST_TYPE;
695 }
696
697 $filterPosts = new \WP_Query(
698 array(
699 'posts_per_page' => -1,
700 'post_status' => array('any'),
701 'post_type' => $postTypes,
702 'fields' => 'ids',
703 'suppress_filters' => true
704 )
705 );
706
707 $filterPostsIds = $filterPosts->get_posts();
708
709 if( ! empty( $filterPostsIds ) ){
710 foreach ($filterPostsIds as $post_id) {
711 wp_delete_post( $post_id, true );
712 }
713 }
714
715 $filterTrashPosts = new \WP_Query(
716 array(
717 'posts_per_page' => -1,
718 'post_status' => array('trash'),
719 'post_type' => $postTypes,
720 'fields' => 'ids',
721 'suppress_filters' => true
722 )
723 );
724
725 $filterTrashPostsIds = $filterTrashPosts->get_posts();
726
727 if( ! empty( $filterTrashPostsIds ) ){
728 foreach ($filterTrashPostsIds as $post_id) {
729 wp_delete_post( $post_id, true );
730 }
731 }
732 }
733 }
734
735 public static function switchTheme() {
736 flrt_remove_option('posts_container');
737 flrt_remove_option('primary_color');
738 }
739
740 public function includeAdminCss()
741 {
742 $screen = get_current_screen();
743 if ( ! is_null( $screen ) && property_exists( $screen, 'base' ) ) {
744 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
745 $ver = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? rand(0, 1000) : FLRT_PLUGIN_VER;
746 $is_about_pro_tab = isset($_GET['tab']) && $_GET['tab'] === 'aboutpro';
747
748 if ( in_array( $screen->base, [ 'edit', 'post', 'edit-tags', 'term' ] ) || ( strpos( $screen->base, 'filters-settings' ) !== false ) ) {
749 wp_enqueue_style( 'wpc-filter-everything-admin', FLRT_PLUGIN_DIR_URL . 'assets/css/filter-everything-admin'.$suffix.'.css', ['wp-color-picker'], $ver );
750 if($is_about_pro_tab){
751 wp_enqueue_style( 'wpc-filter-everything-pro-benefits', FLRT_PLUGIN_DIR_URL . 'assets/css/pro-benefits-page'.$suffix.'.css', ['wpc-filter-everything-admin'], $ver );
752 }
753 }
754
755 if ( $screen->base === 'widgets' ) {
756 wp_enqueue_style('wpc-widgets', FLRT_PLUGIN_DIR_URL . 'assets/css/wpc-widgets' . $suffix . '.css', [], $ver );
757 }
758
759 }
760 }
761
762 public function includeAdminJs()
763 {
764 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
765 $ver = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? rand(0, 1000) : FLRT_PLUGIN_VER;
766 $select2ver = '4.1.0';
767
768 wp_register_script( 'jquery-tiptip', FLRT_PLUGIN_DIR_URL . 'assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $ver, true );
769 wp_enqueue_script('jquery-tiptip');
770 wp_enqueue_script('wpc-filters-admin', FLRT_PLUGIN_DIR_URL . 'assets/js/wpc-filters-common-admin' . $suffix . '.js', array( 'jquery', 'jquery-ui-sortable', 'wp-color-picker', 'select2'), $ver, true );
771
772 $l10n = array(
773 'prefixesOrderAvailableInPro' => esc_html__( 'Editing the order of URL prefixes is available in the PRO version', 'filter-everything' ),
774 'chipsPlaceholder' => esc_html__( 'Select or enter hooks', 'filter-everything' ),
775 'colorSwatchesPlaceholder' => esc_html__( 'Click to select taxonomies', 'filter-everything' ),
776 'chooseElementHelpText' => esc_html__( 'Hover over the Results container and click to select it', 'filter-everything' ),
777 );
778 wp_localize_script( 'wpc-filters-admin', 'wpcFiltersAdminCommon', $l10n );
779
780 wp_enqueue_script( 'select2', FLRT_PLUGIN_DIR_URL . "assets/js/select2/select2".$suffix.".js", array('jquery'), $select2ver );
781 wp_enqueue_style('select2', FLRT_PLUGIN_DIR_URL . "assets/css/select2/select2".$suffix.".css", '', $select2ver );
782
783 $screen = get_current_screen();
784
785 if( ! is_null( $screen ) && property_exists( $screen, 'base' ) && $screen->base === 'widgets' ){
786 wp_enqueue_script('wpc-widgets', FLRT_PLUGIN_DIR_URL . 'assets/js/wpc-widgets' . $suffix . '.js', array('jquery'), $ver );
787 $l10n = array(
788 'wpcItemNum' => esc_html__( 'Item #', 'filter-everything')
789 );
790 wp_localize_script( 'wpc-widgets', 'wpcWidgets', $l10n );
791 }
792 }
793
794 public function includeFrontCss()
795 {
796 $suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min';
797 $ver = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? rand(0, 1000) : FLRT_PLUGIN_VER;
798 /**
799 * string
800 */
801 $getData = Container::instance()->getTheGet();
802 if( isset( $getData['fl_builder'] ) ){
803 wp_enqueue_style('wpc-widgets', FLRT_PLUGIN_DIR_URL . 'assets/css/wpc-widgets' . $suffix . '.css', [], $ver );
804 }
805 if ( isset( $getData['flrt_get_html_selector'] ) && $getData['flrt_get_html_selector'] === '1' && current_user_can('manage_options') ) {
806 wp_enqueue_style('wpc-element-picker', FLRT_PLUGIN_DIR_URL . 'assets/css/element-picker' . $suffix . '.css', [], $ver );
807 }
808
809 // Do not include plugin CSS if there are no Filter Sets on the page
810 $sets = $this->wpManager->getQueryVar('wpc_page_related_set_ids', []);
811 if( empty( $sets ) ) {
812 return false;
813 }
814
815 wp_enqueue_style('wpc-filter-everything', FLRT_PLUGIN_DIR_URL . 'assets/css/filter-everything' . $suffix . '.css', [], $ver);
816 }
817
818 public function footerHtml()
819 {
820 echo '<div class="wpc-filters-overlay"></div>'."\r\n";
821 }
822
823 public function includeFrontJs()
824 {
825 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
826 $ver = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? rand(0, 1000) : FLRT_PLUGIN_VER;
827 /**
828 * string
829 */
830 $getData = Container::instance()->getTheGet();
831 $em = Container::instance()->getEntityManager();
832
833 $wpcFrontJsVariables = [];
834
835 if ( isset( $getData['flrt_get_html_selector'] ) && $getData['flrt_get_html_selector'] === '1' && current_user_can('manage_options') ) {
836 wp_enqueue_script( 'wpc-element-picker-bundle', FLRT_PLUGIN_DIR_URL . 'assets/js/js-element-picker/element-picker.bundle' . $suffix . '.js', array( 'jquery' ), $ver, true );
837 wp_enqueue_script( 'wpc-element-picker', FLRT_PLUGIN_DIR_URL . 'assets/js/element-picker' . $suffix . '.js', array( 'jquery', 'wpc-element-picker-bundle' ), $ver, true );
838 add_action( 'wp_footer', [$this, 'showElementPickerPanel'] );
839 }
840
841 if( isset( $getData['fl_builder'] ) ){
842 wp_enqueue_script('wpc-widgets', FLRT_PLUGIN_DIR_URL . 'assets/js/wpc-widgets' . $suffix . '.js', array('jquery'), $ver );
843 $l10n = array(
844 'wpcItemNum' => esc_html__( 'Item #', 'filter-everything' )
845 );
846 wp_localize_script( 'wpc-widgets', 'wpcWidgets', $l10n );
847 }
848
849 // Do not include plugin JS if there are no Filter Sets on the page
850 $sets = $this->wpManager->getQueryVar( 'wpc_page_related_set_ids', [] );
851 $related_filters = $em->getSetsRelatedFilters( $sets );
852 $date_filters = [];
853 $include_timepicker = false;
854
855 if ( ! empty( $related_filters ) ) {
856 foreach ( $related_filters as $filter ) {
857 if ( in_array( $filter['entity'], [ 'post_date', 'post_meta_date' ] ) ) {
858
859 $date_time = flrt_split_date_time( $filter['date_format'] );
860
861 global $wp_locale;
862 $date_filters[ $filter['ID'] ] = [
863 'date_type' => $filter['date_type'],
864 'date_format' => flrt_convert_date_to_js( $date_time['date'] ),
865 'time_format' => flrt_convert_time_to_js( $date_time['time'] ),
866 ];
867
868 if ( in_array( $filter['date_type'], [ 'time', 'datetime' ] ) ) {
869 $include_timepicker = true;
870 }
871
872 }
873 }
874 }
875
876 /**
877 * Do not continue and do not include any assets
878 * if this page does not contain Filter Sets
879 */
880 if ( empty( $sets ) ) {
881 return false;
882 }
883
884 $showBottomWidget = 'no';
885 $ajaxEnabled = false;
886 $autoScroll = false;
887 $waitCursor = false;
888 $wpcUseSelect2 = false;
889 $wpcPopupCompatMode = false;
890 $autoScrollOffset = apply_filters( 'wpc_auto_scroll_offset', 150 );
891 $wpc_mobile_width = flrt_get_mobile_width();
892 $per_page = [];
893 $applyButtonSets = [];
894 $queryOnThePageSets = [];
895 $filterSetService = Container::instance()->getFilterSetService();
896
897 if ( flrt_get_option('mobile_filter_settings') === 'show_bottom_widget' ) {
898 $showBottomWidget = 'yes';
899 }
900
901 if ( flrt_get_option('enable_ajax') === 'on' ) {
902 $ajaxEnabled = true;
903 }
904
905 if ( flrt_get_experimental_option('auto_scroll') === 'on' ) {
906 $autoScroll = true;
907 }
908
909 if ( flrt_get_experimental_option( 'use_wait_cursor' ) === 'on' ) {
910 $waitCursor = true;
911 }
912
913 if ( flrt_get_option('bottom_widget_compatibility') ) {
914 $wpcPopupCompatMode = true;
915 }
916
917 //@todo This appears on login page and produce not an array error
918 foreach( $sets as $set ){
919 if( $set['filtered_post_type'] === 'product' && function_exists('wc_get_default_products_per_row') ){
920 $numberposts = apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() );
921 }else{
922 $numberposts = get_option( 'posts_per_page' );
923 }
924
925 $per_page[ $set['ID'] ] = intval($numberposts);
926 $theSet = $filterSetService->getSet( $set['ID'] );
927
928 if( isset( $set['query_on_the_page'] ) && $set['query_on_the_page'] ){
929 if( (int) $set['ID'] > 0 ) {
930 $queryOnThePageSets[] = (int) $set['ID'];
931 }
932 }
933
934 if( isset( $theSet['use_apply_button']['value'] ) && $theSet['use_apply_button']['value'] === 'yes' ){
935 if( (int) $set['ID'] > 0 ){
936 $applyButtonSets[] = (int) $set['ID'];
937 }
938 }
939 }
940
941 $per_page = apply_filters( 'wpc_filter_sets_posts_per_page', $per_page );
942
943 $wpcPostContainers = apply_filters( 'wpc_posts_containers', flrt_get_option( 'posts_container', flrt_default_posts_container() ) );
944
945 if ( is_array( $wpcPostContainers ) ) {
946 $wpcPostContainers = array_map( 'wp_specialchars_decode', $wpcPostContainers );
947 }
948
949 wp_register_script( 'wc-jquery-ui-touchpunch', FLRT_PLUGIN_DIR_URL . 'assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch'.$suffix.'.js', [], $ver, true );
950
951 $isPro = defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO;
952
953 $wpcFrontJsVariables = array(
954 'ajaxUrl' => admin_url('admin-ajax.php'),
955 'wpcAjaxEnabled' => $ajaxEnabled,
956 'wpcStatusCookieName' => FLRT_FOLDING_COOKIE_NAME,
957 'wpcMoreLessCookieName' => FLRT_MORELESS_COOKIE_NAME,
958 'wpcHierarchyListCookieName' => FLRT_HIERARCHY_LIST_COOKIE_NAME,
959 'wpcWidgetStatusCookieName' => FLRT_OPEN_CLOSE_BUTTON_COOKIE_NAME,
960 'wpcMobileWidth' => $wpc_mobile_width,
961 'showBottomWidget' => $showBottomWidget,
962 '_nonce' => wp_create_nonce('wpcNonceFront'),
963 'wpcPostContainers' => $wpcPostContainers,
964 'wpcAutoScroll' => $autoScroll,
965 'wpcAutoScrollOffset' => $autoScrollOffset,
966 'wpcWaitCursor' => $waitCursor,
967 'wpcPostsPerPage' => $per_page,
968 'wpcUseSelect2' => $wpcUseSelect2,
969 'wpcDateFilters' => false,
970 'wpcPopupCompatMode' => $wpcPopupCompatMode,
971 'wpcApplyButtonSets' => $applyButtonSets,
972 'wpcQueryOnThePageSets' => $queryOnThePageSets,
973 'wpcNoPostsContainerMsg' => esc_html__('It appears that this page does not contain the container specified in the «Results container» option. Try to specify the correct one in the Filter Set settings or the common plugin Settings.', 'filter-everything'),
974 'wpcIsPro' => $isPro,
975 'permalinksEnabled' => FLRT_PERMALINKS_ENABLED,
976 'wpcMoreLessCount' => flrt_more_less_count(),
977 'wpcSearchChipsText' => esc_html__('search: %s', 'filter-everything' ),
978 'chipsTitle' => esc_html__('Remove %s from results', 'filter-everything'),
979 'chipsReset' => esc_html__('Reset all', 'filter-everything'),
980 );
981
982 /**
983 * We includes Flatpickr.js only on pages, where date filter is used.
984 */
985 if ( ! empty( $date_filters ) ) {
986
987 wp_enqueue_script( 'jquery-ui-datepicker' );
988 wp_enqueue_style( 'wpc-datepicker', FLRT_PLUGIN_DIR_URL . 'assets/css/datepicker/jquery-ui'.$suffix.'.css', array(), '1.11.4' );
989
990 if ( $include_timepicker ) {
991 wp_enqueue_script( 'wpc-timepicker', FLRT_PLUGIN_DIR_URL . "assets/js/timepicker/jquery-ui-timepicker-addon".$suffix.".js", array( 'jquery-ui-datepicker' ), '1.6.3' );
992 wp_enqueue_style( 'wpc-timepicker', FLRT_PLUGIN_DIR_URL . "assets/css/timepicker/jquery-ui-timepicker-addon".$suffix.".css", array(), '1.6.3' );
993 }
994
995 $wpcFrontJsVariables['wpcDateFilters'] = $date_filters;
996 $wpcFrontJsVariables['wpcDateFiltersLocale'] = determine_locale();
997 $wpcFrontJsVariables['wpcDateFiltersL10n'] = array(
998 'closeText' => _x( 'Filter', 'Date Picker closeText', 'filter-everything' ),
999 'currentText' => _x( 'Today', 'Date Picker currentText', 'filter-everything' ),
1000 'nextText' => _x( 'Next', 'Date Picker nextText', 'filter-everything' ),
1001 'prevText' => _x( 'Prev', 'Date Picker prevText', 'filter-everything' ),
1002 'weekHeader' => _x( 'Wk', 'Date Picker weekHeader', 'filter-everything' ),
1003 'timeOnlyTitle' => _x( 'Choose Time', 'Date Time Picker timeOnlyTitle', 'filter-everything' ),
1004 'timeText' => _x( 'Time', 'Date Time Picker timeText', 'filter-everything' ),
1005 'hourText' => _x( 'Hour', 'Date Time Picker hourText', 'filter-everything' ),
1006 'minuteText' => _x( 'Minute', 'Date Time Picker minuteText', 'filter-everything' ),
1007 'secondText' => _x( 'Second', 'Date Time Picker secondText', 'filter-everything' ),
1008 'timezoneText' => _x( 'Time Zone', 'Date Time Picker timezoneText', 'filter-everything' ),
1009 'selectText' => _x( 'Select', 'Date Time Picker selectText', 'filter-everything' ),
1010 'amNames' => array(
1011 _x( 'AM', 'Date Time Picker amText', 'filter-everything' ),
1012 _x( 'A', 'Date Time Picker amTextShort', 'filter-everything' ),
1013 ),
1014 'pmNames' => array(
1015 _x( 'PM', 'Date Time Picker pmText', 'filter-everything' ),
1016 _x( 'P', 'Date Time Picker pmTextShort', 'filter-everything' ),
1017 ),
1018
1019 'monthNames' => array_values( $wp_locale->month ),
1020 'monthNamesShort' => array_values( $wp_locale->month_abbrev ),
1021 'dayNames' => array_values( $wp_locale->weekday ),
1022 'dayNamesMin' => array_values( $wp_locale->weekday_initial ),
1023 'dayNamesShort' => array_values( $wp_locale->weekday_abbrev ),
1024 'firstDay' => get_option( 'start_of_week' ),
1025 'applyText' => _x( 'Apply', 'Date Picker applyText', 'filter-everything' ),
1026 );
1027 }
1028
1029 /**
1030 * Include Main Front filters javascript
1031 */
1032 wp_enqueue_script('wpc-filter-everything', FLRT_PLUGIN_DIR_URL . 'assets/js/filter-everything'.$suffix.'.js', array('jquery', 'jquery-ui-slider', 'wc-jquery-ui-touchpunch'), $ver, true );
1033
1034 if( flrt_get_experimental_option('select2_dropdowns') === 'on' ){
1035 $select2ver = '4.1.0';
1036 $wpcUseSelect2 = 'yes';
1037 wp_enqueue_script( 'select2', FLRT_PLUGIN_DIR_URL . "assets/js/select2/select2".$suffix.".js", array('jquery'), $select2ver );
1038 wp_enqueue_style('select2', FLRT_PLUGIN_DIR_URL . "assets/css/select2/select2".$suffix.".css", '', $select2ver );
1039 }
1040
1041 $wpcFrontJsVariables['wpcUseSelect2'] = $wpcUseSelect2;
1042
1043 wp_localize_script( 'wpc-filter-everything', 'wpcFilterFront', $wpcFrontJsVariables );
1044
1045 if(!empty($applyButtonSets) && flrt_instant_recount()){
1046 $this->inlineScriptJsonData();
1047 }
1048
1049 unset( $filterSetService, $wpcFrontJsVariables );
1050 }
1051
1052 private function inlineScriptJsonData()
1053 {
1054 global $wp, $wp_rewrite;
1055 $flrt_json_data = Container::instance()->getFilterContext()->jsonData();
1056 if(!empty($flrt_json_data)){
1057 $wpc_filter_permalinks = get_option( 'wpc_filter_permalinks', [] );
1058 $permalinksTab = new PermalinksTab();
1059 $wpc_filter_permalinks_numbering = [];
1060 $wpc_filter_permalinks_entities = [];
1061 $permalinks_result = [];
1062 $includeEntities = ['post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date'];
1063 $i = 1;
1064 // Maps are keyed by the full "entity#e_name" pair: two filters may share
1065 // an e_name with different entities (post_meta_num#_sale_price "Sale Price"
1066 // vs post_meta_exists#_sale_price "On Sale") and keying by e_name alone
1067 // made the last one win, sending On Sale into the no-slug GET branch (?yes=on)
1068 foreach ($permalinksTab->movePostMetaNumInTheEnd($wpc_filter_permalinks) as $key => $value) {
1069 $parts = explode('#', $key);
1070 $permalinks_result[$key] = $value;
1071 if(in_array($parts[0], $includeEntities)){
1072 $wpc_filter_permalinks_entities[$key] = $parts[0];
1073 }
1074 $wpc_filter_permalinks_numbering[$i++] = $key;
1075 }
1076 $flrt_json_data['wpcFilterPermalinks'] = $permalinks_result;
1077 $flrt_json_data['wpcFilterPermalinksNum'] = $wpc_filter_permalinks_numbering;
1078 $flrt_json_data['wpcFilterEntitiesWithoutSlug'] = $wpc_filter_permalinks_entities;
1079 // Pagination must never leak into the Apply URL base: applying a new
1080 // selection restarts the results from page 1 (deeper pages may not exist)
1081 $request = (string) $wp->request;
1082 $pagination_base = ! empty( $wp_rewrite->pagination_base ) ? $wp_rewrite->pagination_base : 'page';
1083 $request = preg_replace( '#(^|/)' . preg_quote( $pagination_base, '#' ) . '/\d+$#', '', $request );
1084
1085 $flrt_json_data['domain'] = trailingslashit(home_url(add_query_arg([], $request)));
1086 // The site's permalink convention (mirrors user_trailingslashit) —
1087 // the client-side URL builder must terminate paths the same way
1088 $flrt_json_data['trailingSlash'] = ( user_trailingslashit( 'wpc' ) === 'wpc/' );
1089 // On shops that list variations as standalone catalog items (XStore's
1090 // variable_products_detach) the client-side recount must keep displayed
1091 // counts in variation space — mirrors the server-side
1092 // wpc_from_variations_to_products counting gate.
1093 $flrt_json_data['variationsAsProducts'] = function_exists( 'flrt_variations_listed_as_products' )
1094 ? (bool) flrt_variations_listed_as_products() : false;
1095
1096 /**
1097 * The selection-independent bulk of the data (term post lists,
1098 * meta values, the set universe — ~95% of the payload, tens of
1099 * MB on large catalogs) is served as a hash-versioned STATIC
1100 * FILE: cacheable across pages and visits, fetched off the
1101 * critical path. Only the page-scoped part stays inline. When
1102 * the file cannot be written, everything falls back inline —
1103 * still as an inert JSON block, never as a JS object literal
1104 * (the full JS parser needed ~28 s for 32 MB; JSON.parse of the
1105 * same payload takes ~60 ms).
1106 */
1107 $blobUrl = $this->maybeWriteJsonBlobFile( $flrt_json_data );
1108 $pagePart = [];
1109
1110 if ( $blobUrl ) {
1111 foreach ( [ 'wpcFilterPermalinks', 'wpcFilterPermalinksNum', 'wpcFilterEntitiesWithoutSlug', 'domain', 'trailingSlash', 'variationsAsProducts' ] as $rootKey ) {
1112 if ( array_key_exists( $rootKey, $flrt_json_data ) ) {
1113 $pagePart[ $rootKey ] = $flrt_json_data[ $rootKey ];
1114 }
1115 }
1116 foreach ( $flrt_json_data as $key => $setData ) {
1117 if ( ! is_numeric( $key ) ) {
1118 continue;
1119 }
1120 $pagePart[ $key ] = [
1121 'filteredAllPostsIds' => isset( $setData['filteredAllPostsIds'] ) ? $setData['filteredAllPostsIds'] : [],
1122 'totalFilteredCount' => isset( $setData['totalFilteredCount'] ) ? $setData['totalFilteredCount'] : 0,
1123 ];
1124 }
1125 $pagePart['blobUrl'] = $blobUrl;
1126 $json = wp_json_encode( $pagePart );
1127 } else {
1128 $json = wp_json_encode( $flrt_json_data );
1129 }
1130
1131 add_action( 'wp_print_footer_scripts', function () use ( $json ) {
1132 echo '<script type="application/json" id="wpc-filter-json-data">' . $json . '</script>' . "\n";
1133 }, 1 );
1134
1135 $bootstrap = 'try { var wpcFilterJsonDataEl = document.getElementById("wpc-filter-json-data");';
1136 $bootstrap .= ' if ( wpcFilterJsonDataEl ) { var wpcFilterPagePart = JSON.parse( wpcFilterJsonDataEl.textContent );';
1137 $bootstrap .= ' wpcFilterJsonDataEl.parentNode.removeChild( wpcFilterJsonDataEl );';
1138 $bootstrap .= ' if ( wpcFilterPagePart.blobUrl ) {';
1139 $bootstrap .= ' window.wpcFilterJsonBlobUrl = wpcFilterPagePart.blobUrl;';
1140 $bootstrap .= ' window.wpcFilterJsonDataPromise = fetch( wpcFilterPagePart.blobUrl, { credentials: "same-origin" } )';
1141 $bootstrap .= '.then( function (r) { if ( ! r.ok ) { throw new Error( "HTTP " + r.status ); } return r.json(); } )';
1142 $bootstrap .= '.then( function (blob) { for ( var k in wpcFilterPagePart ) { if ( k === "blobUrl" ) { continue; }';
1143 $bootstrap .= ' if ( blob[k] && typeof blob[k] === "object" && ! Array.isArray( blob[k] ) && wpcFilterPagePart[k] && typeof wpcFilterPagePart[k] === "object" && ! Array.isArray( wpcFilterPagePart[k] ) ) {';
1144 $bootstrap .= ' for ( var kk in wpcFilterPagePart[k] ) { blob[k][kk] = wpcFilterPagePart[k][kk]; } } else { blob[k] = wpcFilterPagePart[k]; } }';
1145 $bootstrap .= ' window.wpcFilterJsonData = blob; return blob; } )';
1146 $bootstrap .= '.catch( function (e) { if ( window.console ) { console.error( "Filter Everything: filter data fetch failed", e ); } } );';
1147 $bootstrap .= ' } else { window.wpcFilterJsonData = wpcFilterPagePart; } }';
1148 $bootstrap .= ' } catch (e) { if ( window.console ) { console.error( "Filter Everything: filter data JSON parse failed", e ); } }';
1149
1150 wp_add_inline_script( 'wpc-filter-everything', $bootstrap, 'before' );
1151 }
1152 }
1153
1154 /**
1155 * Writes the selection-independent part of $flrt_json_data to a static
1156 * JSON file in uploads/flrt-cache/ and returns its URL (false = keep
1157 * everything inline). The file name carries a hash of the inputs that
1158 * define the content (plugin/cache versions, blob version bumped in
1159 * resetTransitions(), set group, language, universe), so URLs are
1160 * immutable: a stale file is impossible, only unused ones — those are
1161 * garbage-collected in resetTransitions().
1162 */
1163 private function maybeWriteJsonBlobFile( $flrt_json_data )
1164 {
1165 if ( apply_filters( 'wpc_json_static_file', true ) === false ) {
1166 return false;
1167 }
1168
1169 $setIds = array_filter( array_keys( $flrt_json_data ), 'is_numeric' );
1170 if ( empty( $setIds ) ) {
1171 return false;
1172 }
1173
1174 $firstSet = $flrt_json_data[ reset( $setIds ) ];
1175 $group = isset( $firstSet['relatedSets'] ) && $firstSet['relatedSets'] !== '' ? $firstSet['relatedSets'] : implode( '_', $setIds );
1176 $group = preg_replace( '/[^0-9_]/', '', (string) $group );
1177 $universe = ( isset( $firstSet['allPostsIds'] ) && is_array( $firstSet['allPostsIds'] ) ) ? array_keys( $firstSet['allPostsIds'] ) : [];
1178 $lang = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : '';
1179
1180 // The filter CONFIGS (view, More/Less, parent, labels, ...) shape the
1181 // stored blob, but no hook is guaranteed to bump flrt_json_blob_ver for
1182 // every way a config can change — a missed bump used to freeze a stale
1183 // config into the immutable file forever (e.g. a filter switched to
1184 // More/Less kept more_less='no' in the blob and the recount hid every
1185 // term of it). Fingerprint the configs directly: a config change now
1186 // mints a new file name by construction. 'values' is page-scoped and
1187 // normalized out of the stored blob — it must not vary the name.
1188 $configFingerprint = [];
1189 foreach ( $setIds as $sid ) {
1190 if ( empty( $flrt_json_data[ $sid ]['allEntities'] ) ) {
1191 continue;
1192 }
1193 foreach ( $flrt_json_data[ $sid ]['allEntities'] as $eName => $entity ) {
1194 $filterConf = is_object( $entity )
1195 ? ( isset( $entity->filter ) ? (array) $entity->filter : [] )
1196 : ( isset( $entity['filter'] ) ? (array) $entity['filter'] : [] );
1197 unset( $filterConf['values'] );
1198 $configFingerprint[ $sid ][ $eName ] = $filterConf;
1199 }
1200 }
1201
1202 $hash = md5( implode( '|', [
1203 FLRT_PLUGIN_VER,
1204 FLRT_CACHE_FORMAT_SUFFIX,
1205 (string) get_option( 'flrt_json_blob_ver', 1 ),
1206 // The same DB serves different filter data in free and PRO (PRO-only
1207 // fields are gated at read time) — the modes must not share a file
1208 defined( 'FLRT_FILTERS_PRO' ) ? 'pro' : 'free',
1209 $group,
1210 $lang,
1211 count( $universe ),
1212 md5( implode( ',', $universe ) ),
1213 md5( (string) wp_json_encode( $configFingerprint ) ),
1214 ] ) );
1215
1216 $uploads = wp_upload_dir();
1217 if ( ! empty( $uploads['error'] ) ) {
1218 return false;
1219 }
1220
1221 $fileName = 'filters-' . $group . '-' . $hash . '.json';
1222 $dir = trailingslashit( $uploads['basedir'] ) . 'flrt-cache';
1223 $file = $dir . '/' . $fileName;
1224 $url = trailingslashit( $uploads['baseurl'] ) . 'flrt-cache/' . $fileName;
1225
1226 if ( file_exists( $file ) ) {
1227 return $url;
1228 }
1229
1230 if ( ! wp_mkdir_p( $dir ) ) {
1231 return false;
1232 }
1233
1234 if ( ! file_exists( $dir . '/index.php' ) ) {
1235 @file_put_contents( $dir . '/index.php', "<?php // Silence is golden.\n" );
1236 }
1237
1238 $encoded = wp_json_encode( $this->normalizeJsonBlob( $flrt_json_data ) );
1239 if ( ! $encoded ) {
1240 return false;
1241 }
1242
1243 // Write to a temp name first: a concurrent request must never fetch
1244 // a half-written multi-MB file
1245 $tmp = $file . '.' . wp_generate_password( 8, false ) . '.tmp';
1246 if ( @file_put_contents( $tmp, $encoded ) === false ) {
1247 return false;
1248 }
1249 if ( ! @rename( $tmp, $file ) ) {
1250 @unlink( $tmp );
1251 return false;
1252 }
1253
1254 return $url;
1255 }
1256
1257 /**
1258 * Strips/zeroes the page-scoped leaves so that every page of the set
1259 * group produces an identical blob: filteredAllPostsIds and the current
1260 * result counts live inline; cross_count, current range bounds and
1261 * per-bucket counts are recomputed client-side on the first recount and
1262 * are never read from the JSON before that.
1263 */
1264 private function normalizeJsonBlob( $flrt_json_data )
1265 {
1266 // Entities are objects — one encode/decode round-trip detaches a
1267 // plain array tree we can normalize without touching the originals
1268 $data = json_decode( wp_json_encode( $flrt_json_data ), true );
1269
1270 $rangeLeaves = [ 'min', 'max', 'from', 'to', 'time_from', 'time_to' ];
1271
1272 foreach ( $data as $key => &$setData ) {
1273 if ( ! is_numeric( $key ) ) {
1274 continue;
1275 }
1276
1277 unset( $setData['filteredAllPostsIds'], $setData['totalFilteredCount'] );
1278
1279 if ( empty( $setData['allEntities'] ) || ! is_array( $setData['allEntities'] ) ) {
1280 continue;
1281 }
1282
1283 foreach ( $setData['allEntities'] as &$entity ) {
1284 unset( $entity['filter']['values'] );
1285
1286 if ( array_key_exists( 'new_date_query', $entity ) ) {
1287 $entity['new_date_query'] = [];
1288 }
1289
1290 foreach ( $rangeLeaves as $leaf ) {
1291 if ( array_key_exists( $leaf, $entity ) ) {
1292 $entity[ $leaf ] = false;
1293 }
1294 }
1295
1296 if ( empty( $entity['items'] ) || ! is_array( $entity['items'] ) ) {
1297 continue;
1298 }
1299
1300 foreach ( $entity['items'] as &$item ) {
1301 if ( array_key_exists( 'cross_count', $item ) ) {
1302 $item['cross_count'] = 0;
1303 }
1304 if ( array_key_exists( 'wp_queried', $item ) ) {
1305 $item['wp_queried'] = false;
1306 }
1307 foreach ( $rangeLeaves as $leaf ) {
1308 if ( array_key_exists( $leaf, $item ) ) {
1309 $item[ $leaf ] = false;
1310 }
1311 }
1312 if ( ! empty( $item['range_list_input'] ) && is_array( $item['range_list_input'] ) ) {
1313 foreach ( $item['range_list_input'] as $bucket => $count ) {
1314 if ( is_numeric( $count ) ) {
1315 $item['range_list_input'][ $bucket ] = 0;
1316 }
1317 }
1318 }
1319 }
1320 unset( $item );
1321 }
1322 unset( $entity );
1323 }
1324 unset( $setData );
1325
1326 // These stay inline (page-scoped or tiny site config)
1327 unset( $data['domain'], $data['trailingSlash'], $data['variationsAsProducts'], $data['wpcFilterPermalinks'], $data['wpcFilterPermalinksNum'], $data['wpcFilterEntitiesWithoutSlug'] );
1328
1329 return $data;
1330 }
1331
1332 public function removeApplyButtonOrderField( &$set_settings_fields )
1333 {
1334 unset( $set_settings_fields['apply_button_menu_order'], $set_settings_fields['search_field_menu_order'] );
1335 }
1336
1337 public function handleFilterSetFieldsVisibility( $filterSetFields )
1338 {
1339 if( isset( $filterSetFields['use_apply_button']['value'] ) && $filterSetFields['use_apply_button']['value'] === 'yes' ) {
1340 $filterSetFields['apply_button_text']['additional_class'] = 'wpc-opened';
1341 $filterSetFields['reset_button_text']['additional_class'] = 'wpc-opened';
1342 }
1343
1344 if ( isset( $filterSetFields['use_search_field']['value'] ) && $filterSetFields['use_search_field']['value'] === 'yes' ) {
1345 $filterSetFields['search_field_placeholder']['additional_class'] = 'wpc-opened';
1346 $filterSetFields['search_field_label']['additional_class'] = 'wpc-opened';
1347 }
1348
1349 if (isset( $filterSetFields['horizontal_view']['value'] ) && $filterSetFields['horizontal_view']['value'] === 'yes'){
1350 $filterSetFields['horizontal_view_column']['additional_class'] = 'wpc-opened';
1351 }
1352
1353 return $filterSetFields;
1354 }
1355
1356 public function disableCacheProductsShortcode( $out )
1357 {
1358 $wpManager = Container::instance()->getWpManager();
1359 $is_filter_request = $wpManager->getQueryVar('wpc_is_filter_request');
1360 $thePost = Container::instance()->getThePost();
1361 $action = isset( $thePost['action'] ) ? $thePost['action'] : false;
1362
1363 // wpc_get_wp_queries - action to get WP_Queries on a page
1364 if( isset( $out['cache'] ) && ( $is_filter_request || $action === 'wpc_get_wp_queries' ) ){
1365 $out['cache'] = false;
1366 }
1367
1368 return $out;
1369 }
1370
1371 public function showCombinedFields( &$filter, $field_key )
1372 {
1373 $includeExclude = flrt_extract_vars( $filter, array('exclude', 'include') );
1374 if( $includeExclude ):
1375
1376 ?><tr class="<?php echo esc_attr( flrt_filter_row_class( $includeExclude['exclude'] ) ); ?>"<?php flrt_maybe_hide_row( $includeExclude['exclude'] ); ?>><?php
1377
1378 flrt_include_admin_view('filter-field-label', array(
1379 'field_key' => 'exclude',
1380 'attributes' => $includeExclude['exclude']
1381 )
1382 );
1383 ?>
1384 <td class="wpc-filter-field-td wpc-filter-field-include-exclude-td">
1385 <div class="wpc-filter-field-include-exclude-wrap">
1386 <div class="wpc-field-wrap wpc-field-exclude-wrap <?php if( isset( $includeExclude['exclude']['id'] ) ){ echo esc_attr( $includeExclude['exclude']['id'] ); } ?>-wrap">
1387 <?php echo flrt_render_input( $includeExclude['exclude'] ); // Already escaped in function ?>
1388 <?php do_action('wpc_after_filter_input', $includeExclude['exclude'] ); ?>
1389 </div>
1390 <div class="wpc-field-wrap wpc-field-include-wrap <?php if( isset( $includeExclude['include']['id'] ) ){ echo esc_attr( $includeExclude['include']['id'] ); } ?>-wrap">
1391 <?php echo flrt_render_input( $includeExclude['include'] ); // Already escaped in function ?>
1392 <?php do_action('wpc_after_filter_input', $includeExclude['include'] ); ?>
1393 </div>
1394 </div>
1395 </td>
1396 </tr><?php
1397
1398 endif;
1399
1400 if ( $field_key === 'min_num_label' ) {
1401 $min_max_num_labels = flrt_extract_vars( $filter, array( 'min_num_label', 'max_num_label' ) );
1402
1403 if( $min_max_num_labels ) :
1404 $min_field_id = $max_field_id = '';
1405
1406 if (isset($min_max_num_labels['min_num_label']['id'])) {
1407 $min_field_id = esc_attr($min_max_num_labels['min_num_label']['id']);
1408 }
1409 if( isset( $min_max_num_labels['max_num_label']['id'] ) ){
1410 $max_field_id = esc_attr( $min_max_num_labels['max_num_label']['id'] );
1411 }
1412
1413 if( isset($min_max_num_labels['min_num_label']['value']) ){
1414 $min_max_num_labels['min_num_label']['data-caret'] = mb_strlen( $min_max_num_labels['min_num_label']['value'] ) + 1;
1415 }
1416
1417 if( isset($min_max_num_labels['max_num_label']['value']) ){
1418 $min_max_num_labels['max_num_label']['data-caret'] = mb_strlen( $min_max_num_labels['max_num_label']['value'] ) + 1;
1419 }
1420
1421 ?><tr class="<?php echo esc_attr( flrt_filter_row_class( $min_max_num_labels['min_num_label'] ) ); ?>"<?php flrt_maybe_hide_row( $min_max_num_labels['min_num_label'] ); ?>><?php
1422
1423 flrt_include_admin_view('filter-field-label', array(
1424 'field_key' => 'min_num_label',
1425 'attributes' => $min_max_num_labels['min_num_label']
1426 )
1427 );
1428 ?>
1429 <td class="wpc-filter-field-td wpc-filter-field-min-max-labels-td">
1430 <div class="wpc-filter-field-min-max-labels-wrap">
1431 <div class="wpc-field-wrap wpc-field-min_num_label-wrap <?php echo $min_field_id; ?>-wrap">
1432 <?php echo flrt_render_input( $min_max_num_labels['min_num_label'] ); // Already escaped in function ?>
1433 <?php do_action('wpc_after_filter_input', $min_max_num_labels['min_num_label'] ); ?>
1434 </div>
1435 <div class="wpc-field-wrap wpc-field-max_num_label-wrap <?php echo $max_field_id; ?>-wrap">
1436 <?php echo flrt_render_input( $min_max_num_labels['max_num_label'] ); // Already escaped in function ?>
1437 <?php do_action('wpc_after_filter_input', $min_max_num_labels['max_num_label'] ); ?>
1438 </div>
1439 <p class="description"><?php echo wp_kses(
1440 sprintf( __('For example, "Price from <span class="wpc-variable-inserter" data-field="%s" title="Click to insert the variable">{value}</span> $" will be displayed as "Price from 150 $"', 'filter-everything'), $min_field_id ),
1441 array( 'span' => array(
1442 'class' => true,
1443 'data-field' => true,
1444 'title' => true,
1445 ) )
1446 ) ?></p>
1447 </div>
1448 </td>
1449 </tr><?php
1450
1451 endif;
1452 }
1453 }
1454
1455 public function addSearchArgsToWpQuery( $wp_query )
1456 {
1457 if ( isset( $_GET['srch'] ) && $_GET['srch'] ) {
1458 $keyword = filter_input( INPUT_GET, 'srch', FILTER_DEFAULT );
1459 $wp_query->set( 's', $keyword );
1460 }
1461
1462 return $wp_query;
1463 }
1464
1465 public function addSkuSearchSql( $search, $wp_query )
1466 {
1467 if( $wp_query->get('flrt_query_hash') || $wp_query->get('flrt_query_clone') ){
1468
1469 if ( $wp_query->get('wc_query') === 'product_query' || $wp_query->get('post_type') === 'product' /* || $wp_query->get('post_type') === 'product_variation' */ ) {
1470 global $wpdb;
1471
1472 $product_id = wc_get_product_id_by_sku( $wp_query->get('s') );
1473 if ( ! $product_id ) {
1474 return $search;
1475 }
1476
1477 $product = wc_get_product( $product_id );
1478 if ( $product->is_type( 'variation' ) ) {
1479 $product_id = $product->get_parent_id();
1480 }
1481
1482 $search = str_replace( 'AND (((', "AND (({$wpdb->posts}.ID IN (" . $product_id . ")) OR ((", $search );
1483 return $search;
1484 }
1485
1486 }
1487 return $search;
1488 }
1489
1490 public function postDateWhere( $where, $wp_query )
1491 { global $wpdb;
1492 $sql = [];
1493 $operator = '';
1494 $wpc_date_query = $wp_query->get( 'wpc_date_query' );
1495
1496 if( ! empty( $wpc_date_query ) && is_array( $wpc_date_query ) ) {
1497 foreach ( $wpc_date_query as $edge => $value ) {
1498 if( $edge === 'from' ) {
1499 $operator = '>=';
1500 } elseif ( $edge === 'to' ) {
1501 $operator = '<=';
1502 }
1503
1504 $sql[] = $wpdb->prepare( "AND {$wpdb->posts}.post_date {$operator} %s ", $value );
1505
1506 }
1507
1508 $where = implode( ' ', $sql ) . $where;
1509 }
1510
1511 return $where;
1512 }
1513
1514 public function showElementPickerPanel()
1515 {
1516 $getData = Container::instance()->getTheGet();
1517
1518 $html = sprintf(
1519 '<div id="wpc-choose-selector" data-set-id="%1$s">
1520 <div id="wpc-choose-id-css" class="wpc-choose-block">
1521 <span>%2$s</span>
1522 <input id="wpc-chooses-html-id">
1523 </div>
1524 <div id="wpc-choose-class-css" class="wpc-choose-block">
1525 <span>%3$s</span>
1526 <input id="wpc-chooses-html-css">
1527 </div>
1528 <div id="wpc-choose-empty" class="wpc-choose-block">
1529
1530 <span id="wpc-chooses-empty-element">%4$s</span>
1531 </div>
1532 <div id="wpc-choose-selector-buttons">
1533 <button id="wpc-choose-another-selector">%5$s</button>
1534 <button id="wpc-choose-save-btn">%6$s</button>
1535 </div>
1536 </div>',
1537 !(empty($getData['flrt_set_id'])) ? esc_attr($getData['flrt_set_id']) : 'global_posts_container',
1538 esc_html__( 'Chosen ID: ', 'filter-everything' ),
1539 esc_html__( 'Chosen Class: ', 'filter-everything' ),
1540 esc_html__( 'The element does not have any CSS selectors', 'filter-everything' ),
1541 esc_html__( 'Choose another', 'filter-everything' ),
1542 esc_html__( 'Save', 'filter-everything' )
1543 );
1544
1545 // Localize script with nonce and AJAX URL
1546 wp_localize_script('wpc-element-picker', 'wpcElementPickerData', array(
1547 'ajax_url' => admin_url('admin-ajax.php'),
1548 'error_saving_selector' => esc_html__('Error saving selector', 'filter-everything'),
1549 'no_element_selected' => esc_html__('Error: No element selected', 'filter-everything'),
1550 'failed_to_save_selector' => esc_html__('Error: Failed to save selector. Please try again.', 'filter-everything'),
1551 'saving' => esc_html__('Saving...', 'filter-everything'),
1552 'save' => esc_html__('Save', 'filter-everything'),
1553 ));
1554
1555 echo $html;
1556 }
1557 }
1558