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

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

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