PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.7
Filter Everything — WordPress & WooCommerce Filters v1.9.7
1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 All 52 releases
← All changes | src/Admin/FilterFields.php +739 -157 1.5.11.9.7 View file →
@@ -2,10 +2,10 @@
2 2
3 3
4 4 namespace FilterEverything\Filter;
5 5
6 -if ( ! defined('WPINC') ) {
7 - wp_die();
6 +if ( ! defined('ABSPATH') ) {
7 + exit;
8 8 }
9 9
10 10 class FilterFields
11 11 {
@@ -32,15 +32,17 @@
32 32 }
33 33
34 34 public function registerHooks()
35 35 {
36 - if( ! $this->hooksRegistered ){
37 - add_filter('wpc_input_type_select', [$this, 'addSpinnerToSelect'], 10, 2);
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 );
38 39
39 40 add_action( 'wp_ajax_wpc-delete-filter', [ $this, 'ajaxDeleteFilter' ] );
40 41 add_action( 'wp_ajax_wpc-load-exclude-terms', [ $this, 'sendExcludedTerms' ] );
42 + add_action( 'wp_ajax_wpc_get_date_formats', [ $this, 'sendDateFormats' ] );
41 43 add_action( 'wp_ajax_wpc-validate-filters', [ $this, 'ajaxValidateFilters' ] );
42 - add_action( 'after_delete_post', [$this, 'deleteRelatedFilters'], 10, 2 );
44 + add_action( 'after_delete_post', [ $this, 'deleteRelatedFilters' ], 10, 2 );
43 45
44 46 $this->hooksRegistered = true;
45 47 }
46 48 }
@@ -49,50 +51,52 @@
49 51 {
50 52 // maybe add filter in future to allow change default fields
51 53 do_action( 'wpc_before_setup_filter_fields' );
52 54 $entity = $this->em->getEntityByFilter( array('entity' => 'taxonomy', 'e_name' => 'category'), 'post' );
55 + $entityTerms = $entity->getTermsForSelect();
56 + asort( $entityTerms, SORT_NATURAL );
53 57
54 58 $defaultFields = array(
55 59 'ID' => array(
56 - 'type' => 'Hidden'
60 + 'type' => 'Hidden'
57 61 ),
58 62 'parent' => array(
59 - 'type' => 'Hidden'
63 + 'type' => 'Hidden'
60 64 ),
61 65 'menu_order' => array(
62 - 'type' => 'Hidden',
63 - 'class' => 'wpc-menu-order-field'
66 + 'type' => 'Hidden',
67 + 'class' => 'wpc-menu-order-field'
64 68 ),
65 - 'label' => array(
66 - 'type' => 'Text',
67 - 'label' => esc_html__( 'Filter Label', 'filter-everything' ),
68 - 'class' => 'wpc-field-label',
69 - 'placeholder' => esc_html__( 'New Filter', 'filter-everything'),
70 - 'instructions' => esc_html__( 'Title which will appear in filter widget', 'filter-everything' )
71 - ),
72 69 'entity' => array(
73 - 'type' => 'Select',
74 - 'label' => esc_html__( 'Filter by', 'filter-everything' ),
75 - 'class' => 'wpc-field-entity',
76 - 'options' => $this->em->getPossibleEntities(),
77 - 'default' => 'taxonomy_category',
78 - 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything' ),
79 - 'required' => true
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
80 77 ),
81 78 'e_name' => array(
82 - 'type' => 'Text',
83 - 'label' => esc_html__( 'Meta Key', 'filter-everything' ),
84 - 'class' => 'wpc-field-ename',
85 - 'instructions' => esc_html__( 'Name of the Custom Field. Please, see Popular Meta keys at the bottom', 'filter-everything'),
86 - 'required' => true
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
87 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 + ),
88 92 'slug' => array(
89 93 'type' => 'Text',
90 94 'label' => esc_html__( 'Var Name for URL', 'filter-everything' ),
91 95 'class' => 'wpc-field-slug',
92 - 'instructions' => esc_html__( 'Name of a part of URL responsible for this filter', 'filter-everything'),
96 + 'instructions' => esc_html__( 'Name of a part of the URL responsible for this filter', 'filter-everything'),
93 97 'tooltip' => wp_kses(
94 - __( 'For example, in the URL path:<br />/?color=blue&size=large<br />"color" and "size" are the names of the URL vars.<br />In PRO version of the plugin the URL part after "?" becomes <br />/color-blue/size-large/', 'filter-everything'),
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'),
95 99 array( 'br' => array() )
96 100 ),
97 101 'required' => true
98 102 ), // Optional
@@ -101,10 +105,69 @@
101 105 'label' => esc_html__( 'View in Widget', 'filter-everything' ),
102 106 'class' => 'wpc-field-view',
103 107 'options' => $this->getViewOptions(),
104 108 'default' => 'checkboxes',
105 - 'instructions' => ''
109 + 'instructions' => '',
106 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 + ),
107 170 'logic' => array(
108 171 'type' => 'Select',
109 172 'label' => esc_html__( 'Filter Logic', 'filter-everything' ),
110 173 'class' => 'wpc-field-logic',
@@ -109,11 +172,11 @@
109 172 'label' => esc_html__( 'Filter Logic', 'filter-everything' ),
110 173 'class' => 'wpc-field-logic',
111 174 'options' => array('or' => esc_html__('OR', 'filter-everything' ), 'and' => esc_html__('AND', 'filter-everything') ),
112 175 'default' => 'or',
113 - 'instructions' => esc_html__( 'Determines how to select posts, when two or more terms of this filter selected', 'filter-everything' ),
176 + 'instructions' => esc_html__( 'Determines how to select posts when two or more terms of this filter are selected', 'filter-everything' ),
114 177 'tooltip' => wp_kses(
115 - __( '«OR» means to show posts if them are at least in one of selected terms. <br />«AND» means, that posts should belong to all selected terms at the same time.', 'filter-everything' ),
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' ),
116 179 array( 'br' => array() )
117 180 ),
118 181 ), // AND
119 182 'orderby' => array(
@@ -120,10 +183,14 @@
120 183 'type' => 'Select',
121 184 'label' => esc_html__( 'Sort Terms by', 'filter-everything' ),
122 185 'class' => 'wpc-field-orderby',
123 186 'options' => $this->getOrderByOptions(),
124 - 'default' => 'nameasc',
125 - 'instructions' => esc_html__('The order in which terms appear in widget', 'filter-everything')
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 + )
126 193 ),
127 194 'in_path' => array(
128 195 'type' => 'Checkbox',
129 196 'label' => esc_html__( 'In URL', 'filter-everything' ),
@@ -132,29 +199,32 @@
132 199 'instructions' => ''
133 200 ),
134 201 'exclude' => array(
135 202 'type' => 'Select',
136 - 'label' => esc_html__( 'Exclude Terms', 'filter-everything' ),
203 + 'label' => esc_html__( 'Include/Exclude Terms', 'filter-everything' ),
137 204 'class' => 'wpc-field-exclude',
138 - 'options' => $entity->getTermsForSelect(),
205 + 'options' => $entityTerms, //$entity->getTermsForSelect(),
139 206 'multiple' => 'multiple',
140 - 'instructions' => esc_html__( 'Selected terms will not appear in filter widget', 'filter-everything' )
207 + 'instructions' => '',
208 + 'skip_view' => true
141 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 + ),
142 219 'collapse' => array(
143 220 'type' => 'Checkbox',
144 221 'label' => esc_html__( 'Folding', 'filter-everything' ),
145 222 'class' => 'wpc-field-collapse',
146 223 'default' => 'no',
147 - 'instructions' => esc_html__( 'Makes the filter collapsible in widget', 'filter-everything'),
148 - 'tooltip' => esc_html__( 'Useful for situations when the filter is rarely applied but takes up some space in widget. Collapsed by default.', 'filter-everything' )
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' )
149 226 ),
150 - 'show_chips' => array(
151 - 'type' => 'Checkbox',
152 - 'label' => esc_html__( 'Show Selected', 'filter-everything' ),
153 - 'class' => 'wpc-field-show-chips',
154 - 'default' => 'yes',
155 - 'instructions' => esc_html__( 'Show filter selected terms in the list of all chosen items', 'filter-everything' )
156 - ),
157 227 'hierarchy' => array(
158 228 'type' => 'Checkbox',
159 229 'label' => esc_html__( 'Show Hierarchy', 'filter-everything' ),
160 230 'class' => 'wpc-field-hierarchy',
@@ -171,9 +241,9 @@
171 241 'step' => array(
172 242 'type' => 'Text',
173 243 'label' => esc_html__( 'Slider Step', 'filter-everything' ),
174 244 'class' => 'wpc-field-value-step',
175 - 'instructions' => esc_html__( 'Determines how numeric value will be changed when you move slider controls', 'filter-everything' ),
245 + 'instructions' => esc_html__( 'Determines how the numeric value will be changed when you move slider controls', 'filter-everything' ),
176 246 'tooltip' => wp_kses(
177 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'),
178 248 array( 'br' => array() )
179 249 ),
@@ -180,19 +250,72 @@
180 250 'default' => 1
181 251 ),
182 252 'search' => array(
183 253 'type' => 'Checkbox',
184 - 'label' => esc_html__( 'Search field', 'filter-everything' ),
254 + 'label' => esc_html__( 'Terms Search', 'filter-everything' ),
185 255 'class' => 'wpc-field-search',
186 256 'default' => 'no',
187 - 'instructions' => esc_html__( 'Adds search field above the terms list', 'filter-everything' )
257 + 'instructions' => esc_html__( 'Adds a search field that allows you to quickly find filter terms', 'filter-everything' )
188 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 + ),
189 291 'tooltip' => array(
190 292 'type' => 'Text',
191 293 'label' => esc_html__( 'Tooltip', 'filter-everything' ),
192 294 'class' => 'wpc-field-tooltip',
193 - 'instructions' => esc_html__( 'Will appear next to the Filter Label', 'filter-everything' ),
295 + 'instructions' => esc_html__( 'Will appear next to the Filter Title', 'filter-everything' ),
194 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' )
195 318 )
196 319 );
197 320
198 321 $this->defaultFields = apply_filters( 'wpc_filter_default_fields', $defaultFields, $this );
@@ -204,17 +327,65 @@
204 327 'checkboxes' => esc_html__('Checkboxes', 'filter-everything'),
205 328 'radio' => esc_html__('Radio buttons', 'filter-everything'),
206 329 'labels' => esc_html__('Labels list', 'filter-everything'),
207 330 'dropdown' => esc_html__('Dropdown', 'filter-everything'),
208 - 'range' => esc_html__('Range', 'filter-everything')
331 + 'range' => esc_html__('Numeric range', 'filter-everything'),
332 + 'date' => esc_html__('Date range', 'filter-everything'),
209 333 );
210 334
335 + if(flrt_is_woocommerce()){
336 + $viewOptions['rating'] = esc_html__('Rating', 'filter-everything');
337 + }
338 +
211 339 return $viewOptions;
212 340 }
213 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 +
214 384 public function getOrderByOptions()
215 385 {
216 386 $orderBy = array(
387 + 'default' => esc_html__( 'Default (no sorting)', 'filter-everything' ),
217 388 'nameasc' => esc_html__( 'Term name &laquo;abc&raquo;', 'filter-everything' ),
218 389 'postcountasc' => esc_html__( 'Posts count &laquo;123&raquo;', 'filter-everything' ),
219 390 'idasc' => esc_html__( 'Term ID &laquo;123&raquo;', 'filter-everything' ),
220 391 'menuasc' => esc_html__( 'Menu order &laquo;123&raquo;', 'filter-everything' ),
@@ -241,10 +412,42 @@
241 412 return $selected;
242 413
243 414 }
244 415
245 - public function getEmptyFilterObject()
416 + private function getFilterNames( $set_id )
246 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 + {
247 450 $defaults = new \stdClass();
248 451 $defaults->ID = self::FLRT_NEW_FILTER_ID;
249 452 $defaults->post_parent = '';
250 453 $defaults->menu_order = 0;
@@ -253,9 +456,9 @@
253 456 $defaults->post_name = '';
254 457
255 458 $filter = $this->em->prepareFilter( $defaults );
256 459
257 - return $this->prepareFilterInputs( $filter );
460 + return $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
258 461 }
259 462
260 463 public function getFiltersInputs( $set_id )
261 464 {
@@ -265,17 +468,16 @@
265 468 return $preparedFilters;
266 469 }
267 470
268 471 $filters = $this->em->selectOnlySetFilters( $set_id );
269 -
270 472 foreach ( $filters as $field_key => $filter ){
271 - $preparedFilters[] = $this->prepareFilterInputs( $filter );
473 + $preparedFilters[] = $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
272 474 }
273 475
274 476 return $preparedFilters;
275 477 }
276 478
277 - private function prepareFilterInputs( $filter )
479 + private function prepareFilterInputsToDisplay( $filter, $filternames )
278 480 {
279 481 $postTypes = false;
280 482 $terms = [];
281 483 // Used for filter table class in filter set fields.
@@ -282,21 +484,38 @@
282 484 // E.g. post_meta, taxonomy, author.
283 485
284 486 $short_entity = $filter['entity'] ? $filter['entity'] : 'taxonomy';
285 487
286 - if( $short_entity === 'taxonomy' ){
488 + if ( $short_entity === 'taxonomy' ) {
287 489 // Add hierarchical class
288 - if( is_taxonomy_hierarchical( $filter['e_name'] ) ){
490 + if ( is_taxonomy_hierarchical( $filter['e_name'] ) ) {
289 491 $short_entity .= ' taxonomy-hierarchical';
290 492 }
291 493
292 494 // Add product attribute class
293 - if( strpos( $filter['e_name'], 'pa_' ) === 0 ){
495 + if ( strpos( $filter['e_name'], 'pa_' ) === 0 ) {
294 496 $short_entity .= ' taxonomy-product-attribute';
295 497 }
296 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 + }
297 511 }
298 512
513 + if ( in_array( $filter['e_name'], flrt_brand_filter_entities() ) ) {
514 + $short_entity .= ' wpc-filter-has-brands';
515 + }
516 +
517 +
299 518 $short_entity .= isset( $filter['view'] ) ? ' wpc-view-'.$filter['view'] : '';
300 519
301 520 $belongs = $this->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] );
302 521
@@ -301,9 +520,9 @@
301 520 $belongs = $this->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] );
302 521
303 522 $postType = '';
304 523 // For post_meta_num entity postType is critical value so we have to set it up
305 - if( in_array( $filter['entity'], array( 'post_meta_num', 'post_meta_exists' ) ) ){
524 + if ( in_array( $filter['entity'], array( 'post_meta_num', 'post_meta_exists', 'post_meta_date' ) ) ) {
306 525 $fss = Container::instance()->getFilterSetService();
307 526 $set = $fss->getSet( $filter['parent'] );
308 527 if( isset( $set['post_type']['value'] ) ){
309 528 $postType = $set['post_type']['value'];
@@ -309,12 +528,18 @@
309 528 $postType = $set['post_type']['value'];
310 529 }
311 530 }
312 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 +
313 537 $entity = $this->em->getEntityByFilter( $filter, $postType );
314 538
315 539 if( $entity ){
316 540 $terms = $entity->getTermsForSelect();
541 + asort( $terms, SORT_NATURAL );
317 542 }
318 543
319 544 // This is required only for filter fields inputs
320 545 $filter = $this->fse->combineEntityNameInFilter( $filter );
@@ -320,13 +545,24 @@
320 545 $filter = $this->fse->combineEntityNameInFilter( $filter );
321 546
322 547 foreach( $this->getFieldsMapping() as $fieldKey => $fieldData ){
323 548
324 - if( isset( $filter[$fieldKey] ) ){
549 + if ( isset( $filter[$fieldKey] ) ) {
325 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 +
326 562 $default_value = isset( $fieldData['default'] ) ? $fieldData['default'] : '';
327 563
328 - $multiple = ( $fieldKey === 'exclude' && ( $filter['entity'] !== 'post_meta_num' ) );
564 + $multiple = ( $fieldKey === 'exclude' && ! in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) );
329 565
330 566 $fieldData['name'] = $this->generateInputName( $filter['ID'], $fieldKey, $multiple );
331 567 $fieldData['id'] = $this->generateInputID( $filter['ID'], $fieldKey );
332 568
@@ -331,29 +567,38 @@
331 567 $fieldData['id'] = $this->generateInputID( $filter['ID'], $fieldKey );
332 568
333 569 $fieldData['value'] = ( $filter[$fieldKey] ) ? $filter[$fieldKey] : $default_value;
334 570
335 - if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
336 - $fieldData['entity_belongs'] = true;
337 - }else{
338 - $fieldData['entity_belongs'] = $belongs;
571 + if( $fieldKey === 'hide_until_parent' ){
572 + if( (int) $filter['parent_filter'] > 0 ){
573 + $fieldData['additional_class'] = 'wpc-opened';
574 + }
339 575 }
340 576
341 - if( $fieldKey === 'entity' ){
342 - $fieldData['short_entity'] = $short_entity;
343 - }
344 -
345 - if( $fieldKey === 'slug' && $fieldData['value'] ){
577 + if ( $fieldKey === 'slug' && $fieldData['value'] ) {
346 578 $fieldData['readonly'] = 'readonly';
347 579 }
348 580
349 - if( $fieldKey === 'e_name' && $fieldData['value'] ){
581 + if ( $fieldKey === 'e_name' && $fieldData['value'] ) {
350 582 unset( $fieldData['options'] );
351 - $fieldData['readonly'] = 'readonly';
352 - $fieldData['type'] = 'Text';
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 + }
353 590 }
354 591
355 - if( $fieldKey === 'entity' ){
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 +
356 601 // Add instead-entity field
357 602 if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){
358 603
359 604 /**
@@ -375,53 +620,113 @@
375 620 if( $fieldData['value'] === 'post_meta_exists' && ! defined('FLRT_FILTERS_PRO') ){
376 621 $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
377 622 }
378 623
624 + if( $fieldData['value'] === 'tax_numeric' && ! defined('FLRT_FILTERS_PRO') ){
625 + $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
626 + }
627 +
379 628 // Field, that will be visible instead of entity, that will be hidden.
380 629 $prepared[ 'instead-entity' ] = $this->getInsteadEntityField( $filter['ID'], $insteadEntityVal );
381 630
382 631 }
383 632
384 - if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
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 + }
385 638
386 - if( ! defined('FLRT_FILTERS_PRO') ){
387 - $fieldData['disabled'] = array('post_meta_exists');
388 - }
639 + }
389 640
641 + if (!defined('FLRT_FILTERS_PRO')) {
642 + if ($fieldKey === 'show_range_list') {
643 + $fieldData['value'] = '';
390 644 }
645 + }
391 646
392 - }
393 647
394 648 if( $fieldKey === 'exclude' ){
395 649 // We always add terms even they are empty array to fill Select2 with related terms.
396 650 $fieldData['options'] = $terms;
397 651
398 - if( $filter['entity'] === 'post_meta_num'){
652 + if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
399 653 $fieldData['options'] = [];
400 654 }
401 655 }
402 656
403 657 // Set disabled fields for some situations
404 - if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists' ) ) /* $filter['entity'] === 'author_author'*/ && $fieldKey === 'logic' ){
658 + if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) /* $filter['entity'] === 'author_author'*/ && $fieldKey === 'logic' ){
405 659 $fieldData['disabled'] = array('and');
406 660 }
407 661
408 - if( $filter['entity'] === 'post_meta_num' && $fieldKey === 'logic' ){
662 + if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $fieldKey === 'logic' ){
409 663 $fieldData['disabled'] = array('or');
410 664 }
411 665
412 - if( $filter['entity'] === 'post_meta_num' && $fieldKey === 'view' ){
413 - $fieldData['disabled'] = array('checkboxes', 'dropdown', 'radio', 'labels', 'colors', 'image');
414 - }else if( $filter['entity'] !== 'post_meta_num' && $fieldKey === 'view' ){
415 - $fieldData['disabled'] = array('range');
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 + }
416 704 }
417 705
418 - if( $fieldKey === 'orderby' && ( mb_strpos( $filter['entity'], 'taxonomy_pa' ) === false ) ){
419 - $fieldData['disabled'] = ['menuasc', 'menudesc'];
706 + if( $fieldKey === 'orderby' ) {
707 + if ( $filter['entity'] !== 'taxonomy_product_cat' && ( mb_strpos( $filter['entity'], 'taxonomy_pa' ) === false ) ){
708 + $fieldData['disabled'] = ['menuasc', 'menudesc'];
709 + }
420 710 }
421 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'];
422 718
719 + if ( ! in_array( $value_to_custom, self::getPossibleDateFormats( $date_type ) ) ) {
720 + $disabled_custom = false;
721 + $fieldData['value'] = 'other';
722 + }
423 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 + }
424 729 }
425 730
426 731 $prepared[ $fieldKey ] = $fieldData;
427 732 }
@@ -442,10 +747,9 @@
442 747 $sanitizedFilter[ $key ] = $value;
443 748 }else{
444 749
445 750 if( is_array( $value ) ){
446 - array_map( 'esc_html', $value );
447 - $sanitizedFilter[ $key ] = $value;
751 + $sanitizedFilter[ $key ] = map_deep( $value, 'esc_html' );
448 752 } else {
449 753 $sanitizedFilter[ $key ] = esc_html( $value );
450 754 }
451 755
@@ -491,18 +795,15 @@
491 795
492 796 return $filter;
493 797 }
494 798
495 - public function validateTheFilter( $filter, $id = false ){
799 + public function validateTheFilter( $filter, $id = false ) {
496 800
497 801 $valid = true;
498 - $newFilter = false;
499 - $filterID = false;
500 802 $validEntity = true;
501 803 $validator = new Validator();
502 804
503 - if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
504 - $newFilter = true;
805 + if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) {
505 806 $filterID = $id;
506 807 }else{
507 808 $filterID = $filter['ID'];
508 809 }
@@ -590,9 +891,9 @@
590 891 /**
591 892 * Slug validations
592 893 */
593 894 if( isset( $filter['slug'] ) ){
594 - $existingSlugs = get_option('wpc_filter_permalinks');
895 + $existingSlugs = get_option('wpc_filter_permalinks', []);
595 896 $prefix = $filter['slug'];
596 897
597 898 // Check this only for new filters
598 899 if( $filter['ID'] === self::FLRT_NEW_FILTER_ID && $validEntity ) {
@@ -606,23 +907,23 @@
606 907 }
607 908
608 909 // Ensure, that prefix contains at least one alphabetic character
609 910 // Also this prevents from empty prefix
610 - if ( ! $validator->validateAlphabCharsExists($prefix) ) {
911 + if ( ! $validator->validateAlphabCharsExists( $prefix ) ) {
611 912 $this->pushError( 37, $filterID, 'slug' ); // Invalid prefix. Has to contain at least one alphabetic symbol
612 913 $valid = false;
613 914 }
614 915
615 916 // Check hyphens problem when cat and cat-x exists
616 - if (!$validator->validatePrefixHyphens($prefix, $existingSlugs) ) {
917 + if ( ! $validator->validatePrefixHyphens( $prefix, $existingSlugs ) ) {
617 918 $this->pushError( 39, $filterID, 'slug' ); // Invalid prefix. Incorrect hyphens.
618 919 $valid = false;
619 920 }
620 921
621 922 // Check for slugs, that matches with native WP Entities
622 - if( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ){
923 + if ( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ) {
623 924 $errorCode = 3991;
624 - if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
925 + if ( defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) {
625 926 $errorCode = 399;
626 927 }
627 928 $this->pushError( $errorCode, $filterID, 'slug' ); // Invalid prefix. Equal to wp entity.
628 929 $valid = false;
@@ -635,36 +936,83 @@
635 936
636 937 /**
637 938 * E_name validations
638 939 */
639 - if( isset( $filter['e_name'] ) ){
940 + if ( isset( $filter['e_name'] ) ) {
640 941
641 942 if( isset( $filter['entity'] ) ) {
642 - if (in_array($filter['entity'], array('post_meta', 'post_meta_num', 'post_meta_exists') ) ) {
943 + if ( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists', 'tax_numeric', 'post_meta_date' ) ) ) {
643 944 $e_name = $filter['e_name'];
644 945
645 - if( $e_name === '' ){
646 - $this->pushError(401, $filterID, 'e_name'); // Select or Enter meta key.
946 + if ( $e_name === '' ) {
947 + $this->pushError( 401, $filterID, 'e_name' ); // Select or Enter meta key.
647 948 $valid = false;
648 949 } else {
649 - // Should not be empty and should contain at least one alphabetic character
650 - if (!$validator->validateAlphabCharsExists($e_name)) {
651 - $this->pushError(40, $filterID, 'e_name'); // Invalid E_name.
652 - $valid = false;
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 + }
653 983 }
984 + }
985 + }
654 986
655 - // Should not contain characters that escaped by esc_attr
656 - if (!$validator->validateEscAttrCharacters($e_name)) {
657 - $this->pushError(40, $filterID, 'e_name'); // Invalid E_name.
658 - $valid = false;
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;
659 1013 }
660 1014
661 - // Some meta keys should be prohibited to use
662 - $excludedMetaKeys = flrt_get_forbidden_meta_keys();
663 - if( in_array( $e_name, $excludedMetaKeys, true ) ){
664 - $this->pushError( 40, $filterID, 'e_name'); // Invalid E_name.
665 - $valid = false;
666 - }
667 1015 }
668 1016 }
669 1017 }
670 1018
@@ -672,8 +1020,22 @@
672 1020 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
673 1021 $valid = false;
674 1022 }
675 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 +
676 1038 /**
677 1039 * View validations
678 1040 */
679 1041 if( isset( $filter['view'] ) ){
@@ -689,8 +1051,18 @@
689 1051 $valid = false;
690 1052 }
691 1053
692 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 + /**
693 1065 * Logic validations
694 1066 */
695 1067 if( isset( $filter['logic'] ) ){
696 1068
@@ -699,9 +1071,9 @@
699 1071 $valid = false;
700 1072 }
701 1073
702 1074 // For author and post_meta_exists entities logic can be only OR
703 - if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists' ) ) ){
1075 + if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) ){
704 1076 if( $filter['logic'] !== 'or' ){
705 1077 $this->pushError( 45, $filterID, 'logic' ); // Not acceptable logic.
706 1078 $valid = false;
707 1079 }
@@ -707,9 +1079,9 @@
707 1079 }
708 1080 }
709 1081
710 1082 // For author entity logic can be only OR
711 - if( $filter['entity'] === 'post_meta_num' ){
1083 + if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
712 1084 if( $filter['logic'] !== 'and' ){
713 1085 $this->pushError( 47, $filterID, 'logic' ); // Not acceptable logic.
714 1086 $valid = false;
715 1087 }
@@ -748,11 +1120,18 @@
748 1120 $valid = false;
749 1121 }
750 1122 }
751 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 +
752 1131 // In case when checkbox is not checked there is no $_POST['in_path'] parameter
753 1132 if( isset( $filter['in_path'] ) ){
754 - if( $filter['entity'] === 'post_meta_num' && $filter['in_path'] === 'yes' ){
1133 + if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $filter['in_path'] === 'yes' ){
755 1134 $this->pushError( 46, $filterID, 'in_path' ); // Invalid In Path for Post meta num.
756 1135 $valid = false;
757 1136 }
758 1137 }
@@ -781,39 +1160,42 @@
781 1160
782 1161 return $valid;
783 1162 }
784 1163
785 - public function validateFilters( $filters ){
1164 + public function validateFilters( $filters ) {
786 1165
787 - if( ! $filters ){
1166 + if ( ! $filters ) {
788 1167 return false;
789 1168 }
790 1169 $valid = true;
791 1170
792 1171 // Check permissions
793 - if( ! current_user_can( 'manage_options' ) ) {
1172 + if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
1173 + // An error occurred. You do not have permission to edit this.
794 1174 $this->pushError(202);
795 1175 $valid = false;
796 1176 }
797 1177
798 1178 // Check for equal filters
799 - foreach ( (array) $filters as $filter ){
800 - if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ){
1179 + foreach ( (array) $filters as $filter ) {
1180 + if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ) {
801 1181 // To avoid few filters with the same meta key
802 - if( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists' ), true ) ){
803 - $keys[] = 'post_meta'.$filter['e_name'];
804 - }else{
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 {
805 1187 $keys[] = $filter['entity'] . $filter['e_name'];
806 1188 }
807 1189 }
808 1190 }
809 1191
810 - if( flrt_array_contains_duplicate( $keys ) ){
1192 + if ( flrt_array_contains_duplicate( $keys ) ) {
811 1193 $this->pushError(31); // Equal filters
812 1194 $valid = false;
813 1195 }
814 1196
815 - return $valid;
1197 + return apply_filters( 'wpc_validate_filter_fields', $valid, $this );
816 1198 }
817 1199
818 1200 public function ajaxValidateFilters()
819 1201 {
@@ -818,8 +1200,19 @@
818 1200 public function ajaxValidateFilters()
819 1201 {
820 1202 $postData = Container::instance()->getThePost();
821 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 + }
822 1215 $response = [];
823 1216
824 1217 if( ! $data ){
825 1218 $this->pushError(20);
@@ -978,9 +1371,9 @@
978 1371
979 1372 $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false;
980 1373 $errorResponse = array(
981 1374 'fid' => $filterId,
982 - 'message' => esc_html__('An error occured. Please, refresh the page and try again.', 'filter-everything')
1375 + 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything')
983 1376 );
984 1377
985 1378 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
986 1379 wp_send_json_error( $errorResponse );
@@ -1003,8 +1396,104 @@
1003 1396 wp_send_json_error( $errorResponse );
1004 1397 }
1005 1398 }
1006 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 +
1007 1496 public function sendExcludedTerms()
1008 1497 {
1009 1498 $container = Container::instance();
1010 1499 $postData = $container->getThePost();
@@ -1015,9 +1504,9 @@
1015 1504 $e_name = isset( $postData['ename'] ) ? $postData['ename'] : false;
1016 1505
1017 1506 $errorResponse = array(
1018 1507 'fid' => $filterId,
1019 - 'message' => esc_html__('An error occured. Please, refresh the page and try again.', 'filter-everything')
1508 + 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything')
1020 1509 );
1021 1510
1022 1511 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
1023 1512 wp_send_json_error( $errorResponse );
@@ -1048,8 +1537,35 @@
1048 1537 }
1049 1538
1050 1539 }
1051 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 +
1052 1568 public function addSpinnerToSelect( $html, $attributes )
1053 1569 {
1054 1570 if( isset( $attributes['class'] ) ){
1055 1571 $requiredIds = array(
@@ -1081,9 +1597,10 @@
1081 1597 'value' => $insteadEntityVal,
1082 1598 'readonly' => 'readonly',
1083 1599 'default' => '',
1084 1600 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything'),
1085 - 'tooltip' => esc_html__( 'An already selected value cannot be changed. But you always can delete the current one and create new filter if you need.', '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
1086 1603 );
1087 1604
1088 1605 return $insteadEntity;
1089 1606 }
@@ -1118,17 +1635,33 @@
1118 1635 if ( empty( $post_type ) ){
1119 1636 return false;
1120 1637 }
1121 1638
1122 - if( in_array( $entity, array( 'author', 'date', 'post_meta', 'post_meta_num' ) ) ){
1639 + if( in_array( $entity, array(
1640 + 'author',
1641 + 'post_date',
1642 + 'post_meta',
1643 + 'post_meta_num',
1644 + 'post_meta_date',
1645 + ) ) ){
1123 1646 return true;
1124 1647 }
1125 1648
1126 - if( in_array( $entity, array( 'post_meta_exists' ) ) && defined( 'FLRT_FILTERS_PRO' ) ){
1649 + if( in_array( $entity, array( 'post_meta_exists', 'tax_numeric' ) ) && defined( 'FLRT_FILTERS_PRO' ) ){
1127 1650 return true;
1128 1651 }
1129 1652
1130 - if( $entity === 'taxonomy' ){
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 +
1131 1664 return $this->isTaxonomyBelongsToPostType( $post_type, $e_name );
1132 1665 }
1133 1666
1134 1667 return false;
@@ -1133,11 +1666,11 @@
1133 1666
1134 1667 return false;
1135 1668 }
1136 1669
1137 - public function getErrorMessage( $code = 20 )
1670 + public function getErrorMessage( $code = 20, $limit = null )
1138 1671 {
1139 - $messages = $this->getErrorsList();
1672 + $messages = self::getErrorsList($limit);
1140 1673
1141 1674 if( isset( $messages[$code] ) ){
1142 1675 return $messages[$code];
1143 1676 }
@@ -1146,9 +1679,10 @@
1146 1679 }
1147 1680
1148 1681 public function pushError( $errorCode, $filterId = false, $filterKey = '' )
1149 1682 {
1150 - $error = array(
1683 + $to_add = true;
1684 + $error = array(
1151 1685 'code' => $errorCode
1152 1686 );
1153 1687
1154 1688 if( $filterId && $filterKey ){
@@ -1154,11 +1688,23 @@
1154 1688 if( $filterId && $filterKey ){
1155 1689 $error['id'] = $this->generateInputID( $filterId, $filterKey );
1156 1690 }
1157 1691
1158 - $this->errors[] = $error;
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 + }
1159 1700
1160 - return $error;
1701 + if ( $to_add ) {
1702 + $this->errors[] = $error;
1703 + return $error;
1704 + }
1705 +
1706 + return false;
1161 1707 }
1162 1708
1163 1709 public function getErrors()
1164 1710 {
@@ -1233,21 +1779,24 @@
1233 1779 }
1234 1780 return $name;
1235 1781 }
1236 1782
1237 - public static function getErrorsList()
1783 + public static function getErrorsList($limit = null)
1238 1784 {
1785 + $effectiveLimit = is_int($limit) ? $limit : FilterSet::FREE_LIMIT_NEW;
1786 +
1239 1787 $errors = array(
1240 1788 // Set errors
1241 - 20 => esc_html__('An error occurred. Set fields were not saved, please try again.', 'filter-everything'),
1242 - 201 => esc_html__('An error occurred. Rule fields were not saved, please try again.', 'filter-everything'),
1243 - 202 => esc_html__('An error occurred. You do not have permissions to edit this.', 'filter-everything'),
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'),
1244 1792 211 => esc_html__( 'Error: invalid WP Page type.', 'filter-everything' ),
1245 1793 21 => esc_html__( 'Error: invalid post type.', 'filter-everything' ),
1246 - 22 => esc_html__( 'Error: invalid location.', 'filter-everything' ),
1794 + 22 => esc_html__( 'Error: invalid value of the Where to filter? field.', 'filter-everything' ),
1247 1795 221 => esc_html__( 'Error: invalid query.', 'filter-everything' ),
1248 1796 23 => esc_html__( 'Error: invalid Hide empty field.', 'filter-everything' ),
1249 1797 24 => esc_html__( 'Error: invalid Show count field.', 'filter-everything' ),
1798 + 25 => esc_html__( 'Error: invalid Horizontal layout field.', 'filter-everything' ),
1250 1799 242 => esc_html__( 'Error: invalid «Apply Button» mode field.', 'filter-everything' ),
1251 1800 // 241 => esc_html__( 'Error: invalid Order field.', 'filter-everything' ),
1252 1801 // Filters errors
1253 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' ),
@@ -1255,22 +1804,25 @@
1255 1804 33 => esc_html__( 'Error: invalid filter ID.', 'filter-everything' ),
1256 1805 34 => esc_html__( 'Error: invalid set ID.', 'filter-everything' ),
1257 1806 35 => esc_html__( 'Error: invalid filter entity.', 'filter-everything' ),
1258 1807 36 => esc_html__( 'Error: filter prefix is already used for another entity.', 'filter-everything' ),
1259 - 37 => esc_html__( 'Error: filter prefix should has at least one alphabetic symbol.', 'filter-everything' ),
1808 + 37 => esc_html__( 'Error: filter prefix should have at least one alphabetic symbol.', 'filter-everything' ),
1260 1809 38 => esc_html__( 'Error: filter prefix should not be empty.', 'filter-everything' ),
1261 - 39 => esc_html__( 'Error: prefix part before "-" character can not be equal with other existing prefix.', 'filter-everything' ),
1262 - 399 => esc_html__( 'Error: this prefix is not allowed because it matches a taxonomy or term name already in use on your site. Please use a different prefix.', 'filter-everything' ),
1263 - 3991 => esc_html__( 'Error: var name is not allowed because it matches a taxonomy or term name already in use on your site.', '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' ),
1264 1813 40 => esc_html__( 'Error: invalid Meta key value', 'filter-everything' ),
1265 - 401 => esc_html__( 'Error: you must select or enter Meta Key', 'filter-everything' ),
1814 + 401 => esc_html__( 'Error: you must enter Meta Key', 'filter-everything' ),
1815 + 402 => esc_html__( 'Error: invalid Taxonomy', 'filter-everything' ),
1266 1816 41 => esc_html__( 'Error: invalid View parameter.', 'filter-everything' ),
1817 + 411 => esc_html__( 'Error: invalid Date Type parameter.', 'filter-everything' ),
1267 1818 42 => esc_html__( 'Error: invalid Logic parameter', 'filter-everything' ),
1268 - 43 => esc_html__( 'Error: invalid Orderby parameter', 'filter-everything' ),
1819 + 43 => esc_html__( 'Error: invalid the Sort Terms by parameter', 'filter-everything' ),
1269 1820 44 => esc_html__( 'Error: invalid exclude terms', 'filter-everything' ),
1821 + 441 => esc_html__( 'Error: invalid include checkbox value', 'filter-everything' ),
1270 1822 45 => esc_html__( 'Error: for filter Post Author logic OR is acceptable only.', 'filter-everything' ),
1271 - 46 => esc_html__( 'Error: Post meta num filter can not be in URL path.', 'filter-everything' ),
1272 - 47 => esc_html__( 'Error: for filter Post Meta Num logic AND 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' ),
1273 1825 48 => esc_html__( 'Error: Range slider is acceptable for Post Meta Num filters only.', 'filter-everything' ),
1274 1826 50 => esc_html__( 'Error: SEO Rule must have specified Post Type.', 'filter-everything' ),
1275 1827 51 => esc_html__( 'Error: SEO Rule must contain at least one filter.', 'filter-everything' ),
1276 1828 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ),
@@ -1276,9 +1828,39 @@
1276 1828 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ),
1277 1829 53 => esc_html__( 'Error: invalid or forbidden filter presents.', 'filter-everything' ),
1278 1830 54 => esc_html__( 'Error: invalid SEO Rule ID.', 'filter-everything' ),
1279 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 +
1280 1862 );
1281 1863
1282 1864 return $errors;
1283 1865 }
1284 1866 }