PluginProbe
Filter Everything — WordPress & WooCommerce Filters / trunk
Filter Everything — WordPress & WooCommerce Filters vtrunk
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 / FilterFields.php

FilterFields.php in Filter Everything — WordPress & WooCommerce Filters trunk, at src/Admin/FilterFields.php

1,866 lines 76.1 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 FilterFields
11 {
12
13 const FLRT_NEW_FILTER_ID = 'wpc_new_id';
14
15 const FILTER_FIELD_KEY = 'wpc_filter_fields';
16
17 private $defaultFields = [];
18
19 private $fse;
20
21 private $em;
22
23 private $hooksRegistered = false;
24
25 private $errors;
26
27 public function __construct()
28 {
29 $this->fse = Container::instance()->getFilterService();
30 $this->em = Container::instance()->getEntityManager();
31 $this->setupDefaultFields();
32 }
33
34 public function registerHooks()
35 {
36 if ( ! $this->hooksRegistered ) {
37 add_filter( 'wpc_input_type_select', [ $this, 'addSpinnerToSelect' ], 10, 2 );
38 add_filter( 'wpc_input_type_radio', [ $this, 'addSpinnerToDateFormats' ], 10, 2 );
39
40 add_action( 'wp_ajax_wpc-delete-filter', [ $this, 'ajaxDeleteFilter' ] );
41 add_action( 'wp_ajax_wpc-load-exclude-terms', [ $this, 'sendExcludedTerms' ] );
42 add_action( 'wp_ajax_wpc_get_date_formats', [ $this, 'sendDateFormats' ] );
43 add_action( 'wp_ajax_wpc-validate-filters', [ $this, 'ajaxValidateFilters' ] );
44 add_action( 'after_delete_post', [ $this, 'deleteRelatedFilters' ], 10, 2 );
45
46 $this->hooksRegistered = true;
47 }
48 }
49
50 private function setupDefaultFields()
51 {
52 // maybe add filter in future to allow change default fields
53 do_action( 'wpc_before_setup_filter_fields' );
54 $entity = $this->em->getEntityByFilter( array('entity' => 'taxonomy', 'e_name' => 'category'), 'post' );
55 $entityTerms = $entity->getTermsForSelect();
56 asort( $entityTerms, SORT_NATURAL );
57
58 $defaultFields = array(
59 'ID' => array(
60 'type' => 'Hidden'
61 ),
62 'parent' => array(
63 'type' => 'Hidden'
64 ),
65 'menu_order' => array(
66 'type' => 'Hidden',
67 'class' => 'wpc-menu-order-field'
68 ),
69 'entity' => array(
70 'type' => 'Select',
71 'label' => esc_html__( 'Filter by', 'filter-everything' ),
72 'class' => 'wpc-field-entity',
73 'options' => $this->em->getPossibleEntities(),
74 'default' => 'taxonomy_category',
75 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything' ),
76 'required' => true
77 ),
78 'e_name' => array(
79 'type' => 'Text',
80 'label' => esc_html__( 'Meta Key', 'filter-everything' ),
81 'class' => 'wpc-field-ename',
82 'instructions' => esc_html__( 'Name of the Custom Field', 'filter-everything'),
83 'required' => true
84 ),
85 'label' => array(
86 'type' => 'Text',
87 'label' => esc_html__( 'Filter Title', 'filter-everything' ),
88 'class' => 'wpc-field-label',
89 'placeholder' => esc_html__( 'New Filter', 'filter-everything'),
90 'instructions' => esc_html__( 'Will appear in the Filters widget', 'filter-everything' )
91 ),
92 'slug' => array(
93 'type' => 'Text',
94 'label' => esc_html__( 'Var Name for URL', 'filter-everything' ),
95 'class' => 'wpc-field-slug',
96 'instructions' => esc_html__( 'Name of a part of the URL responsible for this filter', 'filter-everything'),
97 'tooltip' => wp_kses(
98 __( 'For example, in the URL path:<br />/?color=blue&size=large<br />"color" and "size" are the names of the URL vars.<br />In the PRO version of the plugin, the URL part after "?" becomes <br />/color-blue/size-large/', 'filter-everything'),
99 array( 'br' => array() )
100 ),
101 'required' => true
102 ), // Optional
103 'view' => array(
104 'type' => 'Select',
105 'label' => esc_html__( 'View in Widget', 'filter-everything' ),
106 'class' => 'wpc-field-view',
107 'options' => $this->getViewOptions(),
108 'default' => 'checkboxes',
109 'instructions' => '',
110 ),
111 'show_range_list' => array(
112 'type' => 'inProButton',
113 'pro_label' => flrt_pro_promo_label(),
114 'label' => esc_html__('Show Range List', 'filter-everything'),
115 'class' => 'wpc-field-show-range-list',
116 'default' => '',
117 'tooltip' => '',
118 ),
119 'dropdown_label' => array(
120 'type' => 'Text',
121 'label' => esc_html__( 'Dropdown Label', 'filter-everything' ),
122 'class' => 'wpc-field-dropdown-label',
123 'default' => '',
124 'placeholder' => esc_html__( 'e.g. - Select color -', 'filter-everything' ),
125 'instructions' => esc_html__( 'Label for the default dropdown option', 'filter-everything' ),
126 ),
127 'date_type' => array(
128 'type' => 'Select',
129 'label' => esc_html__( 'Date Type', 'filter-everything' ),
130 'class' => 'wpc-date-type',
131 'options' => array(
132 'date' => esc_html__( 'Date', 'filter-everything' ),
133 'datetime' => esc_html__( 'Date Time', 'filter-everything' ),
134 'time' => esc_html__( 'Time', 'filter-everything' ),
135 ),
136 'default' => 'date',
137 'instructions' => '',
138 ),
139 'date_format' => array(
140 'type' => 'Radio',
141 'label' => esc_html__( 'Date Format', 'filter-everything' ),
142 'class' => 'wpc-date-format',
143 'options' => $this->getDateFormatOptions(),
144 'default' => __( 'F j, Y' ),
145 'instructions' => esc_html__( 'How the date will be displayed in the Filters widget', 'filter-everything' ),
146 'tooltip' => wp_kses(
147 sprintf( __( 'More PHP date formats can be found on <a href="%1$s" target="_blank">this page</a>.', 'filter-everything' ), 'https://wordpress.org/documentation/article/customize-date-and-time-format/' ),
148 array(
149 'a' => array(
150 'href' => true,
151 ) )
152 )
153 ),
154 'show_term_names' => array(
155 'type' => 'Checkbox',
156 'label' => esc_html__( 'Show Term names', 'filter-everything' ),
157 'class' => 'wpc-field-show-term-names',
158 'default' => 'yes',
159 'instructions' => esc_html__( 'Term names for filters with swatches or brand images may be hidden', 'filter-everything' ),
160 'tooltip' => '',
161 ),
162 'selected_and_above' => array(
163 'type' => 'Checkbox',
164 'label' => esc_html__( 'Mode «All stars+»', 'filter-everything' ),
165 'class' => 'wpc-field-show-selected-and-above',
166 'default' => 'no',
167 'instructions' => esc_html__( 'E.g. if 3 stars are selected, all products with rating 3+ will be shown', 'filter-everything' ),
168 'tooltip' => '',
169 ),
170 'logic' => array(
171 'type' => 'Select',
172 'label' => esc_html__( 'Filter Logic', 'filter-everything' ),
173 'class' => 'wpc-field-logic',
174 'options' => array('or' => esc_html__('OR', 'filter-everything' ), 'and' => esc_html__('AND', 'filter-everything') ),
175 'default' => 'or',
176 'instructions' => esc_html__( 'Determines how to select posts when two or more terms of this filter are selected', 'filter-everything' ),
177 'tooltip' => wp_kses(
178 __( '«OR» means to show posts if they are at least in one of the selected terms. <br />«AND» means that posts should belong to all selected terms at the same time.', 'filter-everything' ),
179 array( 'br' => array() )
180 ),
181 ), // AND
182 'orderby' => array(
183 'type' => 'Select',
184 'label' => esc_html__( 'Sort Terms by', 'filter-everything' ),
185 'class' => 'wpc-field-orderby',
186 'options' => $this->getOrderByOptions(),
187 'default' => 'default',
188 'instructions' => esc_html__('The order in which terms appear in the widget', 'filter-everything'),
189 'tooltip' => wp_kses(
190 __( 'The "Default" option means that terms will be sorted in the order as they were received from the Database<br />The "Menu order" option is available for WooCommerce attributes only.', 'filter-everything' ),
191 array( 'br' => array() )
192 )
193 ),
194 'in_path' => array(
195 'type' => 'Checkbox',
196 'label' => esc_html__( 'In URL', 'filter-everything' ),
197 'class' => 'wpc-field-path',
198 'default' => 'yes',
199 'instructions' => ''
200 ),
201 'exclude' => array(
202 'type' => 'Select',
203 'label' => esc_html__( 'Include/Exclude Terms', 'filter-everything' ),
204 'class' => 'wpc-field-exclude',
205 'options' => $entityTerms, //$entity->getTermsForSelect(),
206 'multiple' => 'multiple',
207 'instructions' => '',
208 'skip_view' => true
209 ),
210 'include' => array(
211 'type' => 'Select',
212 'label' => '',
213 'class' => 'wpc-field-include',
214 'options' => [ 'no' => esc_html__( 'exclude', 'filter-everything' ), 'yes' => esc_html__('include', 'filter-everything' ) ],
215 'default' => 'no',
216 'instructions' => esc_html__( 'Include selected terms only', 'filter-everything' ),
217 'skip_view' => true
218 ),
219 'collapse' => array(
220 'type' => 'Checkbox',
221 'label' => esc_html__( 'Folding', 'filter-everything' ),
222 'class' => 'wpc-field-collapse',
223 'default' => 'no',
224 'instructions' => esc_html__( 'Makes filter collapsible in the widget', 'filter-everything'),
225 'tooltip' => esc_html__( 'Useful in situations when the filter is rarely applied but takes up some space in the widget. Collapsed by default.', 'filter-everything' )
226 ),
227 'hierarchy' => array(
228 'type' => 'Checkbox',
229 'label' => esc_html__( 'Show Hierarchy', 'filter-everything' ),
230 'class' => 'wpc-field-hierarchy',
231 'default' => 'no',
232 'instructions' => esc_html__( 'Display the term hierarchy in the filter widget. Child terms will be collapsed by default', 'filter-everything' )
233 ),
234 'range_slider' => array(
235 'type' => 'Checkbox',
236 'label' => esc_html__( 'Enable Range Slider?', 'filter-everything' ),
237 'class' => 'wpc-field-range-slider',
238 'default' => 'yes',
239 'instructions' => esc_html__( 'If disabled, visitors must type numeric values in text inputs', 'filter-everything' )
240 ),
241 'step' => array(
242 'type' => 'Text',
243 'label' => esc_html__( 'Slider Step', 'filter-everything' ),
244 'class' => 'wpc-field-value-step',
245 'instructions' => esc_html__( 'Determines how the numeric value will be changed when you move slider controls', 'filter-everything' ),
246 'tooltip' => wp_kses(
247 __('Step 1 means possible values are 1,2,3,4 ...<br />Step 0.1 means possible values are 5.1, 5.2, 5.3, 5.4 ...<br />Step 15 means possible values are 15, 30, 45, 60...', 'filter-everything'),
248 array( 'br' => array() )
249 ),
250 'default' => 1
251 ),
252 'search' => array(
253 'type' => 'Checkbox',
254 'label' => esc_html__( 'Terms Search', 'filter-everything' ),
255 'class' => 'wpc-field-search',
256 'default' => 'no',
257 'instructions' => esc_html__( 'Adds a search field that allows you to quickly find filter terms', 'filter-everything' )
258 ),
259 'parent_filter' => array(
260 'type' => 'Select',
261 'label' => esc_html__( 'Parent Filter', 'filter-everything' ),
262 'class' => 'wpc-field-parent-filter',
263 'options' => [ '-1' => esc_html__( 'Please, add filters first', 'filter-everything' ) ],
264 'instructions' => esc_html__( 'If specified, current Filter terms become available only after the parent Filter selected', 'filter-everything' )
265 ),
266 'hide_until_parent' => array(
267 'type' => 'Checkbox',
268 'label' => esc_html__( 'Hide until the Parent selected', 'filter-everything' ),
269 'class' => 'wpc-field-hide-until-parent',
270 'default' => 'no',
271 'instructions' => esc_html__( 'Do not show this Filter until a parent is selected. Useful for step-by-step filtering', 'filter-everything' )
272 ),
273 'min_num_label' => array(
274 'type' => 'Text',
275 'label' => esc_html__( 'Labels for Chips', 'filter-everything' ),
276 'class' => 'wpc-field-min-num-label',
277 'default' => '',
278 'placeholder' => esc_html__( 'e.g. From {value}', 'filter-everything' ),
279 'instructions' => '',
280 'skip_view' => true,
281 ),
282 'max_num_label' => array(
283 'type' => 'Text',
284 'label' => '',
285 'class' => 'wpc-field-max-num-label',
286 'default' => '',
287 'placeholder' => esc_html__( 'e.g. To {value}', 'filter-everything' ),
288 'instructions' => '',
289 'skip_view' => true,
290 ),
291 'tooltip' => array(
292 'type' => 'Text',
293 'label' => esc_html__( 'Tooltip', 'filter-everything' ),
294 'class' => 'wpc-field-tooltip',
295 'instructions' => esc_html__( 'Will appear next to the Filter Title', 'filter-everything' ),
296 'default' => ''
297 ),
298 'more_less' => array(
299 'type' => 'Checkbox',
300 'label' => esc_html__( 'More/Less', 'filter-everything' ),
301 'class' => 'wpc-field-more-less',
302 'default' => 'no',
303 'instructions' => sprintf( esc_html__( 'Show a toggle link after %s terms', 'filter-everything' ), flrt_more_less_count() )
304 ),
305 'show_chips' => array(
306 'type' => 'Checkbox',
307 'label' => esc_html__( 'Show in Chips', 'filter-everything' ),
308 'class' => 'wpc-field-show-chips',
309 'default' => 'yes',
310 'instructions' => esc_html__( 'Show filter selected terms in the list of all chosen items', 'filter-everything' )
311 ),
312 'acf_fields' => array(
313 'type' => 'Text',
314 'label' => esc_html__( 'ACF Fields', 'filter-everything' ),
315 'class' => 'wpc-field-acf',
316 'default' => '',
317 'instructions' => esc_html__( 'Contains the IDs of ACF fields that are related to the Custom Field', 'filter-everything' )
318 )
319 );
320
321 $this->defaultFields = apply_filters( 'wpc_filter_default_fields', $defaultFields, $this );
322 }
323
324 public static function getViewOptions()
325 {
326 $viewOptions = array(
327 'checkboxes' => esc_html__('Checkboxes', 'filter-everything'),
328 'radio' => esc_html__('Radio buttons', 'filter-everything'),
329 'labels' => esc_html__('Labels list', 'filter-everything'),
330 'dropdown' => esc_html__('Dropdown', 'filter-everything'),
331 'range' => esc_html__('Numeric range', 'filter-everything'),
332 'date' => esc_html__('Date range', 'filter-everything'),
333 );
334
335 if(flrt_is_woocommerce()){
336 $viewOptions['rating'] = esc_html__('Rating', 'filter-everything');
337 }
338
339 return $viewOptions;
340 }
341
342 public static function getDateFormatOptions( $type = 'date' )
343 {
344 switch( $type ){
345 case 'date':
346 $F_j_Y = date_i18n( __( 'F j, Y' ) );
347 $d_m_Y = date_i18n( 'd/m/Y' );
348 $m_d_Y = date_i18n( 'm/d/Y' );
349
350 $formatOptions = array(
351 __( 'F j, Y' ) => '<span>' . $F_j_Y . '</span><code>'.__( 'F j, Y' ).'</code>',
352 'd/m/Y' => '<span>' . $d_m_Y . '</span><code>d/m/Y</code>',
353 'm/d/Y' => '<span>' . $m_d_Y . '</span><code>m/d/Y</code>',
354 );
355
356 break;
357 case 'datetime':
358 $F_j_Y = date_i18n( __('F j, Y g:i a') );
359 $d_m_Y = date_i18n( 'd/m/Y g:i a' );
360 $m_d_Y = date_i18n( 'm/d/Y g:i a' );
361
362 $formatOptions = array(
363 __('F j, Y g:i a') => '<span>' . $F_j_Y . '</span><code>'.__('F j, Y g:i a').'</code>',
364 'd/m/Y g:i a' => '<span>' . $d_m_Y . '</span><code>d/m/Y g:i a</code>',
365 'm/d/Y g:i a' => '<span>' . $m_d_Y . '</span><code>m/d/Y g:i a</code>',
366 );
367 break;
368 case 'time':
369 $g_i_a = date_i18n( __('g:i a') );
370 $H_i_s = date_i18n( 'H:i:s' );
371
372 $formatOptions = array(
373 __('g:i a') => '<span>' . $g_i_a . '</span><code>'.__('g:i a').'</code>',
374 'H:i:s' => '<span>' . $H_i_s . '</span><code>H:i:s</code>',
375 );
376 break;
377 }
378
379 $formatOptions['other'] = '<span>' . esc_html__( 'Custom:', 'filter-everything' ) . '</span>';
380
381 return $formatOptions;
382 }
383
384 public function getOrderByOptions()
385 {
386 $orderBy = array(
387 'default' => esc_html__( 'Default (no sorting)', 'filter-everything' ),
388 'nameasc' => esc_html__( 'Term name &laquo;abc&raquo;', 'filter-everything' ),
389 'postcountasc' => esc_html__( 'Posts count &laquo;123&raquo;', 'filter-everything' ),
390 'idasc' => esc_html__( 'Term ID &laquo;123&raquo;', 'filter-everything' ),
391 'menuasc' => esc_html__( 'Menu order &laquo;123&raquo;', 'filter-everything' ),
392 'namedesc' => esc_html__( 'Term name &laquo;cba&raquo;', 'filter-everything' ),
393 'postcountdesc' => esc_html__( 'Posts count &laquo;321&raquo;', 'filter-everything' ),
394 'iddesc' => esc_html__( 'Term ID &laquo;321&raquo;', 'filter-everything' ),
395 'menudesc' => esc_html__( 'Menu order &laquo;321&raquo;', 'filter-everything' )
396 );
397
398 return $orderBy;
399 }
400
401 public function getFieldsByType($type, $configuredFields = [] )
402 {
403 $selected = [];
404 $type = ucfirst($type);
405
406 foreach ( $configuredFields as $key => $field) {
407 if ( isset( $field['type'] ) && ( $field['type'] === $type ) ) {
408 $selected[$key] = $field;
409 }
410 }
411
412 return $selected;
413
414 }
415
416 private function getFilterNames( $set_id )
417 {
418 $filternames = [];
419
420 if( ! $set_id ){
421 return $filternames;
422 }
423
424 $filters = $this->em->selectOnlySetFilters( $set_id );
425
426 if( ! empty( $filters ) ) {
427 if ( count( $filters ) > 1 ) {
428 $filternames['-1'] = esc_html__( '— Select Filter —', 'filter-everything' ) ;
429 } else {
430 $filternames['-1'] = esc_html__( 'Please, add filters first', 'filter-everything' );
431 }
432
433 foreach ( $filters as $filter ) {
434
435 if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
436 continue;
437 }
438
439 if ( isset( $filter['ID'] ) ) {
440 $filternames[ $filter['ID'] ] = $filter['label'] .' (' . $filter['e_name'] .')';
441 }
442 }
443 }
444
445 return $filternames;
446 }
447
448 public function getEmptyFilterObject( $set_id )
449 {
450 $defaults = new \stdClass();
451 $defaults->ID = self::FLRT_NEW_FILTER_ID;
452 $defaults->post_parent = '';
453 $defaults->menu_order = 0;
454 $defaults->post_title = '';
455 $defaults->post_content = '';
456 $defaults->post_name = '';
457
458 $filter = $this->em->prepareFilter( $defaults );
459
460 return $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
461 }
462
463 public function getFiltersInputs( $set_id )
464 {
465 $preparedFilters = [];
466
467 if( ! $set_id || empty( $set_id ) ){
468 return $preparedFilters;
469 }
470
471 $filters = $this->em->selectOnlySetFilters( $set_id );
472 foreach ( $filters as $field_key => $filter ){
473 $preparedFilters[] = $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
474 }
475
476 return $preparedFilters;
477 }
478
479 private function prepareFilterInputsToDisplay( $filter, $filternames )
480 {
481 $postTypes = false;
482 $terms = [];
483 // Used for filter table class in filter set fields.
484 // E.g. post_meta, taxonomy, author.
485
486 $short_entity = $filter['entity'] ? $filter['entity'] : 'taxonomy';
487
488 if ( $short_entity === 'taxonomy' ) {
489 // Add hierarchical class
490 if ( is_taxonomy_hierarchical( $filter['e_name'] ) ) {
491 $short_entity .= ' taxonomy-hierarchical';
492 }
493
494 // Add product attribute class
495 if ( strpos( $filter['e_name'], 'pa_' ) === 0 ) {
496 $short_entity .= ' taxonomy-product-attribute';
497 }
498
499 if ( strpos( $filter['e_name'], 'product_visibility') !== false
500 && $filter['view'] === 'rating') {
501 $short_entity .= ' selected-and-above-show ';
502 }
503
504 if ( flrt_get_experimental_option('use_color_swatches') === 'on' ) {
505 $taxonomies = flrt_get_experimental_option( 'color_swatches_taxonomies', [] );
506
507 if ( in_array( $filter['e_name'], $taxonomies ) ) {
508 $short_entity .= ' taxonomy-has-swatches';
509 }
510 }
511 }
512
513 if ( in_array( $filter['e_name'], flrt_brand_filter_entities() ) ) {
514 $short_entity .= ' wpc-filter-has-brands';
515 }
516
517
518 $short_entity .= isset( $filter['view'] ) ? ' wpc-view-'.$filter['view'] : '';
519
520 $belongs = $this->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] );
521
522 $postType = '';
523 // For post_meta_num entity postType is critical value so we have to set it up
524 if ( in_array( $filter['entity'], array( 'post_meta_num', 'post_meta_exists', 'post_meta_date' ) ) ) {
525 $fss = Container::instance()->getFilterSetService();
526 $set = $fss->getSet( $filter['parent'] );
527 if( isset( $set['post_type']['value'] ) ){
528 $postType = $set['post_type']['value'];
529 }
530 }
531
532 if ( in_array( $filter['entity'], array( 'post_meta_num', 'tax_numeric')) !== false
533 && $filter['view'] === 'range' && (isset($filter['show_range_list']) && $filter['show_range_list'] === 'yes')) {
534 $short_entity .= ' wpc-view-range-list';
535 }
536
537 $entity = $this->em->getEntityByFilter( $filter, $postType );
538
539 if( $entity ){
540 $terms = $entity->getTermsForSelect();
541 asort( $terms, SORT_NATURAL );
542 }
543
544 // This is required only for filter fields inputs
545 $filter = $this->fse->combineEntityNameInFilter( $filter );
546
547 foreach( $this->getFieldsMapping() as $fieldKey => $fieldData ){
548
549 if ( isset( $filter[$fieldKey] ) ) {
550
551 if ( $fieldKey === 'parent_filter' ) {
552 // Exclude current filter
553 if ( isset( $filternames[ $filter['ID'] ] ) ) {
554 unset( $filternames[ $filter['ID'] ] );
555 }
556
557 if ( ! empty( $filternames ) ) {
558 $fieldData['options'] = $filternames;
559 }
560 }
561
562 $default_value = isset( $fieldData['default'] ) ? $fieldData['default'] : '';
563
564 $multiple = ( $fieldKey === 'exclude' && ! in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) );
565
566 $fieldData['name'] = $this->generateInputName( $filter['ID'], $fieldKey, $multiple );
567 $fieldData['id'] = $this->generateInputID( $filter['ID'], $fieldKey );
568
569 $fieldData['value'] = ( $filter[$fieldKey] ) ? $filter[$fieldKey] : $default_value;
570
571 if( $fieldKey === 'hide_until_parent' ){
572 if( (int) $filter['parent_filter'] > 0 ){
573 $fieldData['additional_class'] = 'wpc-opened';
574 }
575 }
576
577 if ( $fieldKey === 'slug' && $fieldData['value'] ) {
578 $fieldData['readonly'] = 'readonly';
579 }
580
581 if ( $fieldKey === 'e_name' && $fieldData['value'] ) {
582 unset( $fieldData['options'] );
583 $fieldData['readonly'] = 'readonly';
584 $fieldData['type'] = 'Text';
585
586 if ( $filter['entity'] === 'tax_numeric' ) {
587 $fieldData['label'] = esc_html__( 'Taxonomy', 'filter-everything' );
588 $fieldData['instructions'] = esc_html__( 'Taxonomy with numeric values you need to filter by', 'filter-everything' );
589 }
590 }
591
592 if( $fieldKey === 'entity' ) {
593 $fieldData['short_entity'] = $short_entity;
594
595 if ( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) {
596 $fieldData['entity_belongs'] = true;
597 } else {
598 $fieldData['entity_belongs'] = $belongs;
599 }
600
601 // Add instead-entity field
602 if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){
603
604 /**
605 * @feature maybe move this to separate method
606 * @bug slug wasn't saved for new filter
607 */
608 // For existing filters we need to forbid changing entity value
609 // And we need to show input instead select
610 $fieldData['type'] = 'Text';
611 // To make it compatible with Text field
612 unset( $fieldData['options'] );
613 // Forbid to edit field
614 $fieldData['readonly'] = 'readonly';
615
616 $flatEntities = $this->em->getFlatEntities();
617
618 $insteadEntityVal = isset( $flatEntities[ $fieldData['value'] ] ) ? $flatEntities[ $fieldData['value'] ] : '';
619
620 if( $fieldData['value'] === 'post_meta_exists' && ! defined('FLRT_FILTERS_PRO') ){
621 $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
622 }
623
624 if( $fieldData['value'] === 'tax_numeric' && ! defined('FLRT_FILTERS_PRO') ){
625 $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
626 }
627
628 // Field, that will be visible instead of entity, that will be hidden.
629 $prepared[ 'instead-entity' ] = $this->getInsteadEntityField( $filter['ID'], $insteadEntityVal );
630
631 }
632
633 if ( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) {
634 if ( ! defined('FLRT_FILTERS_PRO') ) {
635 $fieldData['disabled'] = array( 'post_meta_exists', 'tax_numeric' );
636 }
637 }
638
639 }
640
641 if (!defined('FLRT_FILTERS_PRO')) {
642 if ($fieldKey === 'show_range_list') {
643 $fieldData['value'] = '';
644 }
645 }
646
647
648 if( $fieldKey === 'exclude' ){
649 // We always add terms even they are empty array to fill Select2 with related terms.
650 $fieldData['options'] = $terms;
651
652 if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
653 $fieldData['options'] = [];
654 }
655 }
656
657 // Set disabled fields for some situations
658 if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) /* $filter['entity'] === 'author_author'*/ && $fieldKey === 'logic' ){
659 $fieldData['disabled'] = array('and');
660 }
661
662 if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $fieldKey === 'logic' ){
663 $fieldData['disabled'] = array('or');
664 }
665
666 if ( $fieldKey === 'view' ) {
667 if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric' ] ) ) {
668 $fieldData['disabled'] = array(
669 'checkboxes',
670 'dropdown',
671 'radio',
672 'labels',
673 'date',
674 'post_meta_date',
675 'colors',
676 'image',
677 'rating',
678 );
679 } else if ( in_array( $filter['entity'], [ 'post_date', 'post_meta_date' ] ) ) {
680 $fieldData['disabled'] = array(
681 'checkboxes',
682 'dropdown',
683 'radio',
684 'labels',
685 'range',
686 'colors',
687 'image',
688 'rating',
689 );
690 } else if ( in_array( $filter['entity'], [ 'taxonomy_product_visibility' ] ) ) {
691 $fieldData['disabled'] = array(
692 'date',
693 'post_meta_date',
694 'range',
695 );
696 }else {
697 $fieldData['disabled'] = array(
698 'range',
699 'date',
700 'post_meta_date',
701 'rating',
702 );
703 }
704 }
705
706 if( $fieldKey === 'orderby' ) {
707 if ( $filter['entity'] !== 'taxonomy_product_cat' && ( mb_strpos( $filter['entity'], 'taxonomy_pa' ) === false ) ){
708 $fieldData['disabled'] = ['menuasc', 'menudesc'];
709 }
710 }
711
712 if ( ( $filter['ID'] === self::FLRT_NEW_FILTER_ID || $filter['entity'] === 'post_date' || $filter['entity'] === 'post_meta_date') && $fieldKey === 'date_format' ) {
713 // This can be new filter or existing
714 $date_type = $filter['date_type'] ? $filter['date_type'] : 'date';
715 $dateFormatOptions = $this->getDateFormatOptions( $date_type );
716 $disabled_custom = true;
717 $value_to_custom = $fieldData['value'];
718
719 if ( ! in_array( $value_to_custom, self::getPossibleDateFormats( $date_type ) ) ) {
720 $disabled_custom = false;
721 $fieldData['value'] = 'other';
722 }
723
724 $customFormatField = '</label><label>' . $this->addCustomDateFormatField( $fieldData['name'], $value_to_custom, $disabled_custom );
725 $dateFormatOptions['other'] = str_replace( '</span>', '</span>' . $customFormatField, $dateFormatOptions['other'] );
726
727 $fieldData['options'] = $dateFormatOptions;
728 }
729 }
730
731 $prepared[ $fieldKey ] = $fieldData;
732 }
733
734 return $prepared;
735 }
736
737 public function sanitizeFilterFields( $filter )
738 {
739 if( is_array( $filter ) ){
740 $sanitizedFilter = [];
741 $not_esc_html = [ 'e_name', 'tooltip' ];
742
743 foreach( $filter as $key => $value ){
744
745 if( in_array( $key, $not_esc_html, true ) ){
746 // Why? because meta_key field can contain any different characters
747 $sanitizedFilter[ $key ] = $value;
748 }else{
749
750 if( is_array( $value ) ){
751 $sanitizedFilter[ $key ] = map_deep( $value, 'esc_html' );
752 } else {
753 $sanitizedFilter[ $key ] = esc_html( $value );
754 }
755
756 }
757 }
758
759 if( isset( $sanitizedFilter['menu_order'] ) ){
760 $sanitizedFilter['menu_order'] = flrt_sanitize_int( $sanitizedFilter['menu_order'] );
761 }
762
763 if( isset( $sanitizedFilter['label'] ) ){
764 $sanitizedFilter['label'] = sanitize_text_field( $sanitizedFilter['label'] );
765 }
766
767 if( isset( $sanitizedFilter['slug'] ) ){
768 $sanitizedFilter['slug'] = preg_replace( '/[^a-z0-9\-\_]+/', '', mb_strtolower($sanitizedFilter['slug']) );
769 $sanitizedFilter['slug'] = trim($sanitizedFilter['slug'], '-');
770 }
771
772 if( isset( $sanitizedFilter['step'] ) ){
773 $sanitizedFilter['step'] = preg_replace('/[^\d\.]+/', '', $sanitizedFilter['step'] );
774 if( ! $sanitizedFilter['step'] ){
775 $sanitizedFilter['step'] = 1;
776 }
777 }
778
779 if( isset( $sanitizedFilter['tooltip'] ) ){
780 $sanitizedFilter['tooltip'] = wp_kses(
781 $sanitizedFilter['tooltip'],
782 array(
783 'br' => array(),
784 'span' => array('class' => true, 'id' => true),
785 'em' => array(),
786 'strong' => array('class' => true),
787 'i' => array(),
788 'b' => array()
789 )
790 );
791 }
792
793 return $sanitizedFilter;
794 }
795
796 return $filter;
797 }
798
799 public function validateTheFilter( $filter, $id = false ) {
800
801 $valid = true;
802 $validEntity = true;
803 $validator = new Validator();
804
805 if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) {
806 $filterID = $id;
807 }else{
808 $filterID = $filter['ID'];
809 }
810
811 // Check all fields are our fields
812 $defaultFields = $this->getFieldsMapping();
813 // To make compatible with this field
814 $defaultFields['instead-entity'] = true;
815
816 foreach( $filter as $fieldKey => $fieldValue ){
817 if( ! isset( $defaultFields[ $fieldKey ] ) ){
818 $this->pushError(32); // Invalid fields present
819 $valid = false;
820 }
821 }
822
823 /**
824 * Check required field data, that should be not empty
825 */
826 if( isset( $filter['ID'] ) ){
827 // We need to check post type for current ID otherwise we can override any existing post
828 if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){
829 $savedFilter = get_post( $filter['ID'] );
830
831 // Filter post doesn't exist
832 if( ! $savedFilter ){
833 $this->pushError(33); // Invalid filter ID
834 $valid = false;
835 }
836 // Other post type
837 if( ! isset( $savedFilter->post_type ) || $savedFilter->post_type !== FLRT_FILTERS_POST_TYPE ){
838 $this->pushError(33); // Invalid filter ID
839 $valid = false;
840 }
841 }
842
843 } else {
844 $this->pushError(33); // Invalid filter ID
845 $valid = false;
846 }
847
848 /**
849 * Parent field aka Set ID
850 */
851 if( isset( $filter['parent'] ) ){
852
853 if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ) {
854 $savedSet = get_post($filter['parent']);
855
856 // Set post doesn't exist
857 if (!$savedSet) {
858 $this->pushError(34); // Invalid Set ID
859 $valid = false;
860 }
861 // Other post type
862 if ( ! isset( $savedSet->post_type ) || $savedSet->post_type !== FLRT_FILTERS_SET_POST_TYPE) {
863 $this->pushError(34); // Invalid Set ID
864 $valid = false;
865 }
866 }
867
868 }else{
869 $this->pushError( 34); // Invalid Set ID
870 $valid = false;
871 }
872
873 /**
874 * Entity
875 */
876 if( isset( $filter['entity'] ) ){
877 if( ! $validator->validatePossibleEntity( $filter['entity'] ) ){
878 $this->pushError( 35, $filterID, 'entity' ); // Invalid Entity
879 $this->pushError( 35, $filterID, 'instead-entity' );
880 $validEntity = false;
881 $valid = false;
882 }
883
884 }else{
885 $this->pushError( 35, $filterID, 'entity' ); // Invalid Entity
886 $this->pushError( 35, $filterID, 'instead-entity' );
887 $validEntity = false;
888 $valid = false;
889 }
890
891 /**
892 * Slug validations
893 */
894 if( isset( $filter['slug'] ) ){
895 $existingSlugs = get_option('wpc_filter_permalinks', []);
896 $prefix = $filter['slug'];
897
898 // Check this only for new filters
899 if( $filter['ID'] === self::FLRT_NEW_FILTER_ID && $validEntity ) {
900 $fs = Container::instance()->getFilterService();
901 $newEntityKey = $fs->getEntityKey($filter['entity'], $filter['e_name']);
902 // Prohibit using the same slug for another entity
903 if (!$validator->validateExistingPrefix($prefix, $existingSlugs, $newEntityKey)) {
904 $this->pushError( 36, $filterID, 'slug' ); // Prefix is already used
905 $valid = false;
906 }
907 }
908
909 // Ensure, that prefix contains at least one alphabetic character
910 // Also this prevents from empty prefix
911 if ( ! $validator->validateAlphabCharsExists( $prefix ) ) {
912 $this->pushError( 37, $filterID, 'slug' ); // Invalid prefix. Has to contain at least one alphabetic symbol
913 $valid = false;
914 }
915
916 // Check hyphens problem when cat and cat-x exists
917 if ( ! $validator->validatePrefixHyphens( $prefix, $existingSlugs ) ) {
918 $this->pushError( 39, $filterID, 'slug' ); // Invalid prefix. Incorrect hyphens.
919 $valid = false;
920 }
921
922 // Check for slugs, that matches with native WP Entities
923 if ( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ) {
924 $errorCode = 3991;
925 if ( defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) {
926 $errorCode = 399;
927 }
928 $this->pushError( $errorCode, $filterID, 'slug' ); // Invalid prefix. Equal to wp entity.
929 $valid = false;
930 }
931
932 } else {
933 $this->pushError( 38, $filterID, 'slug' ); // Invalid prefix. Empty.
934 $valid = false;
935 }
936
937 /**
938 * E_name validations
939 */
940 if ( isset( $filter['e_name'] ) ) {
941
942 if( isset( $filter['entity'] ) ) {
943 if ( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists', 'tax_numeric', 'post_meta_date' ) ) ) {
944 $e_name = $filter['e_name'];
945
946 if ( $e_name === '' ) {
947 $this->pushError( 401, $filterID, 'e_name' ); // Select or Enter meta key.
948 $valid = false;
949 } else {
950
951 if ( $filter['entity'] === 'tax_numeric' ) {
952 // Validate taxonomy
953 $taxonomies = $this->em->getPossibleTaxonomies();
954 $taxonomy_e_name = $filter[ 'e_name' ];
955
956 if( mb_strpos( $filter[ 'e_name' ], 'taxonomy_' ) === false ){
957 $taxonomy_e_name = 'taxonomy_' . $filter[ 'e_name' ];
958 }
959
960 if ( ! isset( $taxonomies[ $taxonomy_e_name ] ) ) {
961 $this->pushError( 402, $filterID, 'e_name' ); // Invalid E_name.
962 $valid = false;
963 }
964 } else {
965 // Should not be empty and should contain at least one alphabetic character
966 if ( ! $validator->validateAlphabCharsExists( $e_name ) ) {
967 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
968 $valid = false;
969 }
970
971 // Should not contain characters that escaped by esc_attr
972 if ( ! $validator->validateEscAttrCharacters( $e_name ) ) {
973 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
974 $valid = false;
975 }
976
977 // Some meta keys should be prohibited to use
978 $excludedMetaKeys = flrt_get_forbidden_meta_keys();
979 if ( in_array( $e_name, $excludedMetaKeys, true ) ) {
980 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
981 $valid = false;
982 }
983 }
984 }
985 }
986
987 if ( in_array( $filter['e_name'], array( 'post_date') ) ) {
988
989 if ( isset( $filter['date_format'] ) && isset( $filter['date_type'] ) ) {
990
991 $date_time = flrt_split_date_time( $filter['date_format'] );
992
993 switch ( $filter['date_type'] ) {
994 case 'date':
995 // check for date
996 if ( ! $date_time['date'] ) {
997 $this->pushError( 60, $filterID, 'date_format' ); // Invalid date_format.
998 $valid = false;
999 }
1000 break;
1001 case 'datetime':
1002 if ( ! $date_time['date'] || ! $date_time['time'] ) {
1003 $this->pushError( 62, $filterID, 'date_format' ); // Invalid date_format.
1004 $valid = false;
1005 }
1006 break;
1007 case 'time':
1008 if ( ! $date_time['time'] ) {
1009 $this->pushError( 61, $filterID, 'date_format' ); // Invalid date_format.
1010 $valid = false;
1011 }
1012 break;
1013 }
1014
1015 }
1016 }
1017 }
1018
1019 }else{
1020 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
1021 $valid = false;
1022 }
1023
1024
1025 if ( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO && isset($filter['e_name'])) {
1026 if (!empty($filter['entity'])) {
1027 if (in_array($filter['entity'], array('post_meta_num', 'tax_numeric'))) {
1028 if (isset($filter['view']) && $filter['view'] === 'range' && !empty($filter['show_range_list']) && $filter['show_range_list'] === 'yes') {
1029 if (empty($filter['range_list_input'])) {
1030 $this->pushError(93, $filterID, 'range_list_input'); // Empty range_list.
1031 $valid = false;
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 /**
1039 * View validations
1040 */
1041 if( isset( $filter['view'] ) ){
1042 $viewOptions = array_keys( $this->getViewOptions() );
1043
1044 if( ! $validator->validateView( $filter, $viewOptions ) ){
1045 $this->pushError( 41, $filterID, 'view' ); // Invalid View.
1046 $valid = false;
1047 }
1048
1049 } else {
1050 $this->pushError( 41, $filterID, 'view' ); // Invalid View.
1051 $valid = false;
1052 }
1053
1054 /**
1055 * Date Type validations
1056 */
1057 if ( isset( $filter['date_type'] ) ) {
1058 if( ! in_array( $filter['date_type'], array( 'date', 'datetime', 'time' ), true ) ){
1059 $this->pushError( 411, $filterID, 'date_type' ); // Invalid Data Type
1060 $valid = false;
1061 }
1062 }
1063
1064 /**
1065 * Logic validations
1066 */
1067 if( isset( $filter['logic'] ) ){
1068
1069 if( ! in_array( $filter['logic'], array( 'or', 'and' ), true ) ){
1070 $this->pushError( 42, $filterID, 'logic' ); // Invalid Logic.
1071 $valid = false;
1072 }
1073
1074 // For author and post_meta_exists entities logic can be only OR
1075 if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) ){
1076 if( $filter['logic'] !== 'or' ){
1077 $this->pushError( 45, $filterID, 'logic' ); // Not acceptable logic.
1078 $valid = false;
1079 }
1080 }
1081
1082 // For author entity logic can be only OR
1083 if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
1084 if( $filter['logic'] !== 'and' ){
1085 $this->pushError( 47, $filterID, 'logic' ); // Not acceptable logic.
1086 $valid = false;
1087 }
1088 }
1089
1090 } else {
1091 $this->pushError( 42, $filterID, 'logic' ); // Invalid Logic.
1092 $valid = false;
1093 }
1094
1095 /**
1096 * Orderby validations
1097 */
1098 if( isset( $filter['orderby'] ) ){
1099 $orderBy = array_keys( $this->getOrderByOptions() );
1100 if( ! in_array( $filter['orderby'], $orderBy, true ) ){
1101 $this->pushError( 43, $filterID, 'orderby' ); // Invalid Orderby.
1102 $valid = false;
1103 }
1104 } else {
1105 $this->pushError( 43, $filterID, 'orderby' ); // Invalid Orderby.
1106 $valid = false;
1107 }
1108
1109 /**
1110 * In path and Collapse doesn't require validations
1111 * because their values are prepended in code
1112 */
1113
1114 /**
1115 * Exclude validations
1116 */
1117 if( isset( $filter['exclude'] ) && $validEntity ){
1118 if( ! $validator->validateExcludeTerms( $filter['exclude'], $filter ) ){
1119 $this->pushError( 44, $filterID, 'exclude' ); // Invalid Exclude.
1120 $valid = false;
1121 }
1122 }
1123
1124 if( isset( $filter['include'] ) ){
1125 if( ! in_array( $filter['include'], ['yes', 'no'] ) ){
1126 $this->pushError( 441, $filterID, 'exclude' ); // Invalid Exclude.
1127 $valid = false;
1128 }
1129 }
1130
1131 // In case when checkbox is not checked there is no $_POST['in_path'] parameter
1132 if( isset( $filter['in_path'] ) ){
1133 if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $filter['in_path'] === 'yes' ){
1134 $this->pushError( 46, $filterID, 'in_path' ); // Invalid In Path for Post meta num.
1135 $valid = false;
1136 }
1137 }
1138
1139 // Check data types if they are correct
1140 /**
1141 * @todo validate range_slider, show_chips, step fields !!! IMPORTANT
1142 */
1143
1144 // Check combinations of different data
1145 /**
1146 * The unique slug and entity problem
1147 */
1148 // The slug and entity pair should be the same between all filters in all sets!
1149 // Otherwise we will have different slugs and URLs in different categories for the same Post type.
1150 // If user adding new slug for the same entity, it should be notified, that this new slug
1151 // will be changed for all same entities.
1152
1153 // Obligatorily sanitize all data. Maybe separate method for that.
1154 // If entity is taxonomy, field e_name should be empty.
1155
1156 // Filter slugs should not be from blacklist. Blacklist - typical Wordpress entities /categories, tags etc
1157
1158 // Slugs should be checked for non UTF symbols and maybe converted to latin UTF symbols
1159 // Because user can add slug = категория and also user can add slug 'cheap' where 'c' will be cyrillic or other.
1160
1161 return $valid;
1162 }
1163
1164 public function validateFilters( $filters ) {
1165
1166 if ( ! $filters ) {
1167 return false;
1168 }
1169 $valid = true;
1170
1171 // Check permissions
1172 if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
1173 // An error occurred. You do not have permission to edit this.
1174 $this->pushError(202);
1175 $valid = false;
1176 }
1177
1178 // Check for equal filters
1179 foreach ( (array) $filters as $filter ) {
1180 if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ) {
1181 // To avoid few filters with the same meta key
1182 if ( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists'), true ) ) {
1183 $keys[] = 'post_meta' . $filter['e_name'];
1184 } elseif ( $filter['entity'] === 'tax_numeric' || $filter['entity'] === 'post_date' || $filter['entity'] === 'post_meta_date') {
1185 $keys[] = $filter['entity'] .'_'. $filter['e_name'];
1186 } else {
1187 $keys[] = $filter['entity'] . $filter['e_name'];
1188 }
1189 }
1190 }
1191
1192 if ( flrt_array_contains_duplicate( $keys ) ) {
1193 $this->pushError(31); // Equal filters
1194 $valid = false;
1195 }
1196
1197 return apply_filters( 'wpc_validate_filter_fields', $valid, $this );
1198 }
1199
1200 public function ajaxValidateFilters()
1201 {
1202 $postData = Container::instance()->getThePost();
1203 $data = isset( $postData['validateData'] ) ? $postData['validateData'] : false;
1204 // The current admin script sends a JSON string, but a stale (cached or
1205 // un-rebuilt .min) copy still sends the bracket-serialized array —
1206 // accept both instead of fataling on stripslashes(array) under PHP 8
1207 if ( is_array( $data ) ) {
1208 $data = wp_unslash( $data );
1209 } elseif ( $data ) {
1210 $data = json_decode( stripslashes( $data ), true );
1211 }
1212 if ( isset( $data['wpc_set_fields']['wp_filter_query_vars'] ) ) {
1213 $data['wpc_set_fields']['wp_filter_query_vars'] = wp_slash( $data['wpc_set_fields']['wp_filter_query_vars'] );
1214 }
1215 $response = [];
1216
1217 if( ! $data ){
1218 $this->pushError(20);
1219 }
1220
1221 if( ! isset( $data['_flrt_nonce'] ) || ! wp_verify_nonce( $data['_flrt_nonce'], FilterSet::NONCE_ACTION ) ){
1222 $this->pushError(20); // Default common error
1223 }
1224
1225 // Validate all filters
1226 // If no one filter it's ok
1227 if( isset( $data['wpc_filter_fields'] ) ){
1228
1229 $this->validateFilters( $data['wpc_filter_fields'] );
1230
1231 // Set up checkbox fields if they are empty
1232 $filterConfiguredFields = $this->getFieldsMapping();
1233
1234 // Validate each filter separately
1235 foreach ( (array) $data['wpc_filter_fields'] as $filterId => $filter ) {
1236 $filter = $this->prepareFilterCheckboxFields($filter, $this->getFieldsByType('checkbox', $filterConfiguredFields));
1237 $this->validateTheFilter( $filter, $filterId );
1238 }
1239
1240 }
1241
1242
1243 $this->fillErrorsMessages();
1244 $errors = $this->getErrors();
1245 // Send errors if they exist
1246 if( $errors && ! empty( $errors ) ){
1247 $response['errors'] = $errors;
1248 wp_send_json_error($response);
1249 }
1250
1251 /**
1252 * @feature it is better to validate all fields and collect all errors to show them simultaneously.
1253 */
1254
1255 wp_send_json_success();
1256 }
1257
1258 function saveFilter( $filter ) {
1259 // May have been posted. Remove slashes.
1260 $filter = wp_unslash( $filter );
1261
1262 $filter = apply_filters( 'wpc_pre_save_filter', $filter );
1263
1264 unset( $filter['instead-entity'] );
1265 // Make a backup of field data and remove some args.
1266 $_filter = $filter;
1267 flrt_extract_vars( $_filter, array( 'ID', 'label', 'parent', 'menu_order', 'slug' ) );
1268
1269 $_filter = $this->fse->splitEntityFullNameInFilter( $_filter );
1270
1271 // Create array of data to save.
1272 $to_save = array(
1273 'ID' => $filter['ID'],
1274 'post_status' => 'publish',
1275 'post_type' => FLRT_FILTERS_POST_TYPE,
1276 'post_title' => $filter['label'],
1277 'post_content' => maybe_serialize( $_filter ),
1278 'post_parent' => $filter['parent'],
1279 'menu_order' => $filter['menu_order'] ? $filter['menu_order'] : 0,
1280 'post_name' => $filter['slug'],
1281 'post_excerpt' => $filter['entity']
1282 );
1283
1284 // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data.
1285 remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
1286
1287 do_action( 'wpc_pre_save_post_filter', $to_save, $filter );
1288
1289 add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 );
1290
1291 // Slash data.
1292 // WP expects all data to be slashed and will unslash it (fixes '\' character issues).
1293 $to_save = wp_slash( $to_save );
1294
1295 // Update or Insert.
1296 if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
1297 $filter['ID'] = wp_insert_post( $to_save );
1298 }else{
1299 wp_update_post( $to_save );
1300 }
1301
1302 remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 );
1303
1304 // Return field.
1305 return $filter;
1306 }
1307
1308 public function prepareFilterCheckboxFields( $filter, $configuredFields = [] )
1309 {
1310 foreach ( $configuredFields as $key => $checkbox ){
1311 if( ! isset( $filter[ $key ] ) ){
1312 $filter[ $key ] = 'no';
1313 }else{
1314 $filter[ $key ] = 'yes';
1315 }
1316 }
1317
1318 return $filter;
1319 }
1320
1321 public function getFieldsMapping()
1322 {
1323 return $this->defaultFields;
1324 }
1325
1326 public function deleteFilter( $ID, $force = false )
1327 {
1328 if( ! $ID ){
1329 return false;
1330 }
1331
1332 if( $force ){
1333 return wp_delete_post( $ID, true );
1334 }else{
1335 return wp_trash_post( $ID );
1336 }
1337
1338 }
1339
1340 public function deleteRelatedFilters( $postid, $post )
1341 {
1342 if( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ){
1343 return $postid;
1344 }
1345
1346 $args = array(
1347 'post_type' => FLRT_FILTERS_POST_TYPE,
1348 'posts_per_page' => -1,
1349 'post_parent' => $postid,
1350 'post_status' => array('any'),
1351 );
1352
1353 $setFilters = get_posts( $args );
1354
1355 if( ! empty( $setFilters ) ){
1356 foreach ( $setFilters as $filter ) {
1357 $this->deleteFilter( $filter->ID, true );
1358 }
1359 }
1360
1361 return $postid;
1362 }
1363
1364 public function ajaxDeleteFilter()
1365 {
1366 $postData = Container::instance()->getThePost();
1367 $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false;
1368 if( $filterId === self::FLRT_NEW_FILTER_ID ){
1369 wp_send_json_success();
1370 }
1371
1372 $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false;
1373 $errorResponse = array(
1374 'fid' => $filterId,
1375 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything')
1376 );
1377
1378 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
1379 wp_send_json_error( $errorResponse );
1380 }
1381
1382 if( ! $filterId ){
1383 wp_send_json_error( $errorResponse );
1384 }
1385
1386 $filter = get_post( $filterId );
1387
1388 if( ! isset( $filter->post_type ) || ( $filter->post_type !== FLRT_FILTERS_POST_TYPE ) ){
1389 wp_send_json_error( $errorResponse );
1390 }
1391
1392 if( $filterPost = $this->deleteFilter( $filterId, true ) ){
1393 $response['fid'] = $filterPost->ID;
1394 wp_send_json_success( $response );
1395 }else{
1396 wp_send_json_error( $errorResponse );
1397 }
1398 }
1399
1400 public static function getPossibleDateFormats( $date_type )
1401 {
1402 $possibleFormats = [
1403 'date' => array(
1404 'd/m/Y',
1405 'm/d/Y',
1406 __('F j, Y'),
1407 ),
1408 'datetime' => array(
1409 'd/m/Y g:i a',
1410 'm/d/Y g:i a',
1411 __('F j, Y g:i a'),
1412 ),
1413 'time' => array(
1414 __('g:i a'),
1415 'H:i:s',
1416 )
1417 ];
1418
1419 if ( isset( $possibleFormats[$date_type] ) ) {
1420 return $possibleFormats[$date_type];
1421 }
1422
1423 return [];
1424 }
1425
1426 private function addCustomDateFormatField( $name, $value, $disabled = false )
1427 {
1428 $html = '<input type="text" name="'.$name.'" value="'.$value.'"';
1429 if ( $disabled ) {
1430 $html .= ' disabled="disabled"';
1431 }
1432
1433 $html .= ' class="wpc-date-custom-format" />';
1434
1435 return $html;
1436 }
1437
1438 public function sendDateFormats()
1439 {
1440 $postData = Container::instance()->getThePost();
1441 $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false;
1442 $setId = isset( $postData['setId'] ) ? $postData['setId'] : false;
1443 $dateType = isset( $postData['dateType'] ) ? $postData['dateType'] : false;
1444
1445 $errorResponse = array(
1446 'fid' => $filterId,
1447 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything')
1448 );
1449
1450 if ( ! $filterId || ! $dateType ) {
1451 wp_send_json_error( $errorResponse );
1452 }
1453
1454 $response = [];
1455
1456 // $savedValue = $this->em->getFilterBy( 'ID', $filterId, array( array( 'ID' => $setId ) ) );
1457 $savedValue = $this->em->getFilterById( $filterId );
1458 $formatoptions = self::getDateFormatOptions( $dateType );
1459
1460 $field = [
1461 'type' => 'Radio',
1462 'class' => 'wpc-date-format',
1463 'default' => __( 'F j, Y' ),
1464 ];
1465
1466 $default_formats = [
1467 'date' => __( 'F j, Y' ),
1468 'datetime' => __('F j, Y g:i a'),
1469 'time' => __('g:i a'),
1470 ];
1471
1472 $disabled_custom = true;
1473 $value_to_custom = ( ! empty( $savedValue['date_format'] ) && $dateType === ( $savedValue['date_type'] ?? '' ) ) ? $savedValue['date_format'] : $default_formats[$dateType];
1474 if ( $value_to_custom && ! in_array( $value_to_custom, self::getPossibleDateFormats( $dateType ) ) ) {
1475 $disabled_custom = false;
1476 $field['value'] = 'other';
1477 }
1478
1479 $customFormatField = '</label><label>' . $this->addCustomDateFormatField( $this->generateInputName( $filterId, 'date_format' ), $value_to_custom, $disabled_custom );
1480 $formatoptions['other'] = str_replace( '</span>', '</span>' . $customFormatField, $formatoptions['other'] );
1481
1482 $field['name'] = $this->generateInputName( $filterId, 'date_format' );
1483 $field['id'] = $this->generateInputID( $filterId, 'date_format' );
1484 $field['options'] = $formatoptions;
1485
1486 ob_start();
1487
1488 echo flrt_render_input( $field );
1489
1490 $response['html'] = ob_get_clean();
1491
1492 wp_send_json_success( $response );
1493 die();
1494 }
1495
1496 public function sendExcludedTerms()
1497 {
1498 $container = Container::instance();
1499 $postData = $container->getThePost();
1500 $validator = new Validator();
1501 $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false;
1502 $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false;
1503 $entity = isset( $postData['entity'] ) ? $postData['entity'] : false;
1504 $e_name = isset( $postData['ename'] ) ? $postData['ename'] : false;
1505
1506 $errorResponse = array(
1507 'fid' => $filterId,
1508 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything')
1509 );
1510
1511 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
1512 wp_send_json_error( $errorResponse );
1513 }
1514
1515 if( ! $validator->validatePossibleEntity( $entity ) ){
1516 wp_send_json_error( $errorResponse );
1517 }
1518
1519 $em = $container->getEntityManager();
1520 if( $e_name ){
1521 $filterEntity['entity'] = $entity;
1522 $filterEntity['e_name'] = $e_name;
1523 }else{
1524 $fse = $container->getFilterService();
1525 $filterEntity = $fse->splitEntityFullNameInFilter( array( 'entity' => $entity ) );
1526 }
1527
1528 $entity = $em->getEntityByFilter( $filterEntity );
1529 $terms = $entity->getTermsForSelect2();
1530
1531 if( ! empty( $terms ) ){
1532 $response['terms'] = $terms;
1533 $response['fid'] = $filterId;
1534 wp_send_json_success( $response );
1535 }else{
1536 wp_send_json_error( $errorResponse );
1537 }
1538
1539 }
1540
1541 public function addSpinnerToDateFormats( $html, $attributes )
1542 {
1543 if ( isset( $attributes['class'] ) && $attributes['class'] === 'wpc-date-format' ) {
1544 $spinner = '<span class="spinner"></span>'."\r\n";
1545 $openContainer = '<div class="wpc-after-spinner-container">'."\r\n";
1546
1547 $closeContainer = '</div>'."\r\n";
1548
1549 if ( isset( $attributes['options']['d/m/Y'] ) ) {
1550 $date_type = 'wpc-type-date';
1551 }
1552
1553 if ( isset( $attributes['options']['d/m/Y g:i a'] ) ) {
1554 $date_type = 'wpc-type-datetime';
1555 }
1556
1557 if ( isset( $attributes['options']['H:i:s'] ) ) {
1558 $date_type = 'wpc-type-time';
1559 }
1560
1561 $html = str_replace( 'wpc-radio-list', 'wpc-radio-list ' . $date_type, $html );
1562
1563 $html = $spinner . $openContainer . $html . $closeContainer;
1564 }
1565 return $html;
1566 }
1567
1568 public function addSpinnerToSelect( $html, $attributes )
1569 {
1570 if( isset( $attributes['class'] ) ){
1571 $requiredIds = array(
1572 $this->generateInputID( self::FLRT_NEW_FILTER_ID, 'exclude' ),
1573 );
1574
1575 if( in_array( $attributes['id'], $requiredIds ) ){
1576
1577 $spinner = '<span class="spinner"></span>'."\r\n";
1578 $openContainer = '<div class="wpc-after-spinner-container">'."\r\n";
1579
1580 $closeContainer = '</div>'."\r\n";
1581
1582 $html = $spinner . $openContainer . $html . $closeContainer;
1583
1584 }
1585 }
1586 return $html;
1587 }
1588
1589 private function getInsteadEntityField( $filterId, $insteadEntityVal )
1590 {
1591 $insteadEntity = array(
1592 'type' => 'Text',
1593 'label' => esc_html__( 'Filter by', 'filter-everything' ),
1594 'class' => 'wpc-field-instead-entity',
1595 'name' => $this->generateInputName( $filterId, 'instead-entity' ),
1596 'id' => $this->generateInputID( $filterId, 'instead-entity' ),
1597 'value' => $insteadEntityVal,
1598 'readonly' => 'readonly',
1599 'default' => '',
1600 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything'),
1601 'tooltip' => esc_html__( 'An already selected value cannot be changed. But you can always delete the current one and create a new filter if you need.', 'filter-everything' ),
1602 'required' => true
1603 );
1604
1605 return $insteadEntity;
1606 }
1607
1608
1609 /**
1610 * @return true|false
1611 */
1612 public function filterBelongsToPostType( $parent, $entity, $e_name )
1613 {
1614 if( ! isset( $parent ) ){
1615 return false;
1616 }
1617
1618 $fss = Container::instance()->getFilterSetService();
1619 $set = $fss->getSet( $parent );
1620
1621 if( ! isset( $set['post_type']['value'] ) ){
1622 return false;
1623 }
1624
1625 $post_type = $set['post_type']['value'];
1626
1627 return $this->entityBelongsToPostType( $post_type, $entity, $e_name );
1628 }
1629
1630 /**
1631 * @return true|false
1632 */
1633 private function entityBelongsToPostType( $post_type, $entity, $e_name )
1634 {
1635 if ( empty( $post_type ) ){
1636 return false;
1637 }
1638
1639 if( in_array( $entity, array(
1640 'author',
1641 'post_date',
1642 'post_meta',
1643 'post_meta_num',
1644 'post_meta_date',
1645 ) ) ){
1646 return true;
1647 }
1648
1649 if( in_array( $entity, array( 'post_meta_exists', 'tax_numeric' ) ) && defined( 'FLRT_FILTERS_PRO' ) ){
1650 return true;
1651 }
1652
1653 if( $entity === 'taxonomy' || $entity === 'tax_numeric' ){
1654 if ( $entity === 'tax_numeric' ) {
1655 if ( ! defined( 'FLRT_FILTERS_PRO' ) ) {
1656 return false;
1657 }
1658 // We have to cut e_name to make correct verification
1659 if ( mb_strpos( $e_name, 'taxonomy_' ) !== false ) {
1660 $e_name = mb_strcut( $e_name, strlen( 'taxonomy_' ) );
1661 }
1662 }
1663
1664 return $this->isTaxonomyBelongsToPostType( $post_type, $e_name );
1665 }
1666
1667 return false;
1668 }
1669
1670 public function getErrorMessage( $code = 20, $limit = null )
1671 {
1672 $messages = self::getErrorsList($limit);
1673
1674 if( isset( $messages[$code] ) ){
1675 return $messages[$code];
1676 }
1677
1678 return $messages[20];
1679 }
1680
1681 public function pushError( $errorCode, $filterId = false, $filterKey = '' )
1682 {
1683 $to_add = true;
1684 $error = array(
1685 'code' => $errorCode
1686 );
1687
1688 if( $filterId && $filterKey ){
1689 $error['id'] = $this->generateInputID( $filterId, $filterKey );
1690 }
1691
1692 if ( ! empty( $this->errors ) ) {
1693 foreach ( $this->errors as $single_error ) {
1694 if ( isset( $single_error['code'] ) && $single_error['code'] == $errorCode ) {
1695 $to_add = false;
1696 break;
1697 }
1698 }
1699 }
1700
1701 if ( $to_add ) {
1702 $this->errors[] = $error;
1703 return $error;
1704 }
1705
1706 return false;
1707 }
1708
1709 public function getErrors()
1710 {
1711 if( ! empty( $this->errors ) ) {
1712 return $this->errors;
1713 }
1714
1715 return false;
1716 }
1717
1718 public function fillErrorsMessages()
1719 {
1720 if( ! empty( $this->errors ) ){
1721 $errorsWithMessages = [];
1722 $messagesList = $this->getErrorsList();
1723
1724 foreach ( $this->errors as $index => $error ){
1725 if( isset( $error['code'] ) && isset( $messagesList[ $error['code'] ] ) ){
1726 $errorsWithMessages[$index] = $error;
1727 $errorsWithMessages[$index]['message'] = $messagesList[ $error['code'] ];
1728 }
1729 }
1730
1731 $this->errors = $errorsWithMessages;
1732 return true;
1733 }
1734
1735 return false;
1736 }
1737
1738 public function getErrorCodes()
1739 {
1740 $codes = [];
1741
1742 if( ! empty( $this->errors ) ){
1743 foreach( $this->errors as $error ){
1744 $codes[] = $error['code'];
1745 }
1746 }
1747
1748 return $codes;
1749 }
1750
1751
1752 /**
1753 * @return true|false
1754 */
1755 private function isTaxonomyBelongsToPostType( $post_type, $taxonomy = null )
1756 {
1757 if ( is_object( $post_type ) )
1758 $post_type = $post_type->post_type;
1759
1760 if ( empty( $post_type ) ){
1761 return false;
1762 }
1763
1764 $taxonomies = get_object_taxonomies( $post_type );
1765
1766 return in_array( $taxonomy, $taxonomies );
1767 }
1768
1769 public function generateInputID( $ID, $key )
1770 {
1771 return self::FILTER_FIELD_KEY . '-' . $ID . '-' . $key;
1772 }
1773
1774 public function generateInputName( $ID, $key, $multiple = false )
1775 {
1776 $name = self::FILTER_FIELD_KEY . '['. $ID .']['. $key . ']';
1777 if( $multiple ){
1778 $name .= '[]';
1779 }
1780 return $name;
1781 }
1782
1783 public static function getErrorsList($limit = null)
1784 {
1785 $effectiveLimit = is_int($limit) ? $limit : FilterSet::FREE_LIMIT_NEW;
1786
1787 $errors = array(
1788 // Set errors
1789 20 => esc_html__('An error occurred. Filter Set fields were not saved, please try again.', 'filter-everything'),
1790 201 => esc_html__('An error occurred. SEO Rule fields were not saved, please try again.', 'filter-everything'),
1791 202 => esc_html__('An error occurred. You do not have permission to edit this.', 'filter-everything'),
1792 211 => esc_html__( 'Error: invalid WP Page type.', 'filter-everything' ),
1793 21 => esc_html__( 'Error: invalid post type.', 'filter-everything' ),
1794 22 => esc_html__( 'Error: invalid value of the Where to filter? field.', 'filter-everything' ),
1795 221 => esc_html__( 'Error: invalid query.', 'filter-everything' ),
1796 23 => esc_html__( 'Error: invalid Hide empty field.', 'filter-everything' ),
1797 24 => esc_html__( 'Error: invalid Show count field.', 'filter-everything' ),
1798 25 => esc_html__( 'Error: invalid Horizontal layout field.', 'filter-everything' ),
1799 242 => esc_html__( 'Error: invalid «Apply Button» mode field.', 'filter-everything' ),
1800 // 241 => esc_html__( 'Error: invalid Order field.', 'filter-everything' ),
1801 // Filters errors
1802 31 => esc_html__( 'Error: two or more filters with equal Filter By and Meta key values are forbidden in the same Set. Please, remove or change equal filters.', 'filter-everything' ),
1803 32 => esc_html__( 'Error: invalid fields present.', 'filter-everything' ),
1804 33 => esc_html__( 'Error: invalid filter ID.', 'filter-everything' ),
1805 34 => esc_html__( 'Error: invalid set ID.', 'filter-everything' ),
1806 35 => esc_html__( 'Error: invalid filter entity.', 'filter-everything' ),
1807 36 => esc_html__( 'Error: filter prefix is already used for another entity.', 'filter-everything' ),
1808 37 => esc_html__( 'Error: filter prefix should have at least one alphabetic symbol.', 'filter-everything' ),
1809 38 => esc_html__( 'Error: filter prefix should not be empty.', 'filter-everything' ),
1810 39 => esc_html__( 'Error: prefix part before "-" character can not be equal to another existing prefix.', 'filter-everything' ),
1811 399 => esc_html__( 'Error: this prefix is not allowed because it matches a taxonomy or term name already used on your site. Please use a different prefix.', 'filter-everything' ),
1812 3991 => esc_html__( 'Error: var name is not allowed because it matches a taxonomy or term name already used on your site.', 'filter-everything' ),
1813 40 => esc_html__( 'Error: invalid Meta key value', 'filter-everything' ),
1814 401 => esc_html__( 'Error: you must enter Meta Key', 'filter-everything' ),
1815 402 => esc_html__( 'Error: invalid Taxonomy', 'filter-everything' ),
1816 41 => esc_html__( 'Error: invalid View parameter.', 'filter-everything' ),
1817 411 => esc_html__( 'Error: invalid Date Type parameter.', 'filter-everything' ),
1818 42 => esc_html__( 'Error: invalid Logic parameter', 'filter-everything' ),
1819 43 => esc_html__( 'Error: invalid the Sort Terms by parameter', 'filter-everything' ),
1820 44 => esc_html__( 'Error: invalid exclude terms', 'filter-everything' ),
1821 441 => esc_html__( 'Error: invalid include checkbox value', 'filter-everything' ),
1822 45 => esc_html__( 'Error: for filter Post Author logic OR is acceptable only.', 'filter-everything' ),
1823 46 => esc_html__( 'Error: numeric filter can not be in the URL path.', 'filter-everything' ),
1824 47 => esc_html__( 'Error: only AND logic is supported for numeric filters.', 'filter-everything' ),
1825 48 => esc_html__( 'Error: Range slider is acceptable for Post Meta Num filters only.', 'filter-everything' ),
1826 50 => esc_html__( 'Error: SEO Rule must have specified Post Type.', 'filter-everything' ),
1827 51 => esc_html__( 'Error: SEO Rule must contain at least one filter.', 'filter-everything' ),
1828 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ),
1829 53 => esc_html__( 'Error: invalid or forbidden filter presents.', 'filter-everything' ),
1830 54 => esc_html__( 'Error: invalid SEO Rule ID.', 'filter-everything' ),
1831 55 => esc_html__( 'Error: SEO rule with selected Filters Combination already exists.', 'filter-everything' ),
1832 60 => esc_html__( 'Error: Invalid date format.', 'filter-everything' ),
1833 61 => esc_html__( 'Error: Invalid time format.', 'filter-everything' ),
1834 62 => esc_html__( 'Error: Invalid date or time format.', 'filter-everything' ),
1835 90 => wp_kses(
1836 sprintf(
1837 __('Error: you can not update settings because the Filter Everything Pro plugin is locked. Please, enter your <a href="%1$s" target="_blank">license key</a> to unlock it.', 'filter-everything' ),
1838 admin_url( 'edit.php?post_type=filter-set&page=filters-settings&tab=license' ) ),
1839 array(
1840 'a' => array(
1841 'href'=> true,
1842 'target' => true
1843 )
1844 )
1845 ),
1846 91 => esc_html__('Error: you can not update settings because the Filter Everything Pro plugin is locked. Please, ask your site Superadmin to enter the plugin license key to unlock it.', 'filter-everything' ),
1847 92 => wp_kses(
1848 sprintf(
1849 /* translators: 1: maximum number of free filter sets per post type, 2: PRO upgrade URL */
1850 __('The free version allows up to %1$d filter sets per post type. Upgrade to <a href=\'%2$s\' target=\'_blank\'>PRO</a> for unlimited filter sets.', 'filter-everything' ),
1851 $effectiveLimit,
1852 esc_url(FLRT_PLUGIN_URL .'/pricing/?utm_source=free_plugin&utm_medium=internal&utm_campaign=free_plugin_upgrade&utm_content=msg_three_set') ),
1853 array(
1854 'a' => array(
1855 'href'=> true,
1856 'target' => true
1857 )
1858 )
1859 ),
1860 93 => esc_html__('Error: The range list is empty. Please add values to the list.', 'filter-everything' ),
1861
1862 );
1863
1864 return $errors;
1865 }
1866 }