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 +752 -201 1.2.41.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,51 +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' => 'Select',
83 - 'label' => esc_html__( 'Meta Key', 'filter-everything' ),
84 - 'class' => 'wpc-field-ename',
85 - 'options' => $this->metaKeysOptions(),
86 - 'instructions' => esc_html__( 'Name of the Custom Field. Please, see Popular Meta keys at the bottom', 'filter-everything'),
87 - '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
88 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 + ),
89 92 'slug' => array(
90 93 'type' => 'Text',
91 94 'label' => esc_html__( 'Var Name for URL', 'filter-everything' ),
92 95 'class' => 'wpc-field-slug',
93 - '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'),
94 97 'tooltip' => wp_kses(
95 - __( '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'),
96 99 array( 'br' => array() )
97 100 ),
98 101 'required' => true
99 102 ), // Optional
@@ -102,10 +105,69 @@
102 105 'label' => esc_html__( 'View in Widget', 'filter-everything' ),
103 106 'class' => 'wpc-field-view',
104 107 'options' => $this->getViewOptions(),
105 108 'default' => 'checkboxes',
106 - 'instructions' => ''
109 + 'instructions' => '',
107 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 + ),
108 170 'logic' => array(
109 171 'type' => 'Select',
110 172 'label' => esc_html__( 'Filter Logic', 'filter-everything' ),
111 173 'class' => 'wpc-field-logic',
@@ -110,11 +172,11 @@
110 172 'label' => esc_html__( 'Filter Logic', 'filter-everything' ),
111 173 'class' => 'wpc-field-logic',
112 174 'options' => array('or' => esc_html__('OR', 'filter-everything' ), 'and' => esc_html__('AND', 'filter-everything') ),
113 175 'default' => 'or',
114 - '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' ),
115 177 'tooltip' => wp_kses(
116 - __( '«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' ),
117 179 array( 'br' => array() )
118 180 ),
119 181 ), // AND
120 182 'orderby' => array(
@@ -121,10 +183,14 @@
121 183 'type' => 'Select',
122 184 'label' => esc_html__( 'Sort Terms by', 'filter-everything' ),
123 185 'class' => 'wpc-field-orderby',
124 186 'options' => $this->getOrderByOptions(),
125 - 'default' => 'nameasc',
126 - '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 + )
127 193 ),
128 194 'in_path' => array(
129 195 'type' => 'Checkbox',
130 196 'label' => esc_html__( 'In URL', 'filter-everything' ),
@@ -133,29 +199,32 @@
133 199 'instructions' => ''
134 200 ),
135 201 'exclude' => array(
136 202 'type' => 'Select',
137 - 'label' => esc_html__( 'Exclude Terms', 'filter-everything' ),
203 + 'label' => esc_html__( 'Include/Exclude Terms', 'filter-everything' ),
138 204 'class' => 'wpc-field-exclude',
139 - 'options' => $entity->getTermsForSelect(),
205 + 'options' => $entityTerms, //$entity->getTermsForSelect(),
140 206 'multiple' => 'multiple',
141 - 'instructions' => esc_html__( 'Selected terms will not appear in filter widget', 'filter-everything' )
207 + 'instructions' => '',
208 + 'skip_view' => true
142 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 + ),
143 219 'collapse' => array(
144 220 'type' => 'Checkbox',
145 221 'label' => esc_html__( 'Folding', 'filter-everything' ),
146 222 'class' => 'wpc-field-collapse',
147 223 'default' => 'no',
148 - 'instructions' => esc_html__( 'Makes the filter collapsible in widget', 'filter-everything'),
149 - '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' )
150 226 ),
151 - 'show_chips' => array(
152 - 'type' => 'Checkbox',
153 - 'label' => esc_html__( 'Show Selected', 'filter-everything' ),
154 - 'class' => 'wpc-field-show-chips',
155 - 'default' => 'yes',
156 - 'instructions' => esc_html__( 'Show filter selected terms in the list of all chosen items', 'filter-everything' )
157 - ),
158 227 'hierarchy' => array(
159 228 'type' => 'Checkbox',
160 229 'label' => esc_html__( 'Show Hierarchy', 'filter-everything' ),
161 230 'class' => 'wpc-field-hierarchy',
@@ -172,9 +241,9 @@
172 241 'step' => array(
173 242 'type' => 'Text',
174 243 'label' => esc_html__( 'Slider Step', 'filter-everything' ),
175 244 'class' => 'wpc-field-value-step',
176 - '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' ),
177 246 'tooltip' => wp_kses(
178 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'),
179 248 array( 'br' => array() )
180 249 ),
@@ -179,14 +248,74 @@
179 248 array( 'br' => array() )
180 249 ),
181 250 'default' => 1
182 251 ),
252 + 'search' => array(
253 + 'type' => 'Checkbox',
254 + 'label' => esc_html__( 'Terms Search', 'filter-everything' ),
255 + 'class' => 'wpc-field-search',
256 + 'default' => 'no',
257 + 'instructions' => esc_html__( 'Adds a search field that allows you to quickly find filter terms', 'filter-everything' )
258 + ),
259 + 'parent_filter' => array(
260 + 'type' => 'Select',
261 + 'label' => esc_html__( 'Parent Filter', 'filter-everything' ),
262 + 'class' => 'wpc-field-parent-filter',
263 + 'options' => [ '-1' => esc_html__( 'Please, add filters first', 'filter-everything' ) ],
264 + 'instructions' => esc_html__( 'If specified, current Filter terms become available only after the parent Filter selected', 'filter-everything' )
265 + ),
266 + 'hide_until_parent' => array(
267 + 'type' => 'Checkbox',
268 + 'label' => esc_html__( 'Hide until the Parent selected', 'filter-everything' ),
269 + 'class' => 'wpc-field-hide-until-parent',
270 + 'default' => 'no',
271 + 'instructions' => esc_html__( 'Do not show this Filter until a parent is selected. Useful for step-by-step filtering', 'filter-everything' )
272 + ),
273 + 'min_num_label' => array(
274 + 'type' => 'Text',
275 + 'label' => esc_html__( 'Labels for Chips', 'filter-everything' ),
276 + 'class' => 'wpc-field-min-num-label',
277 + 'default' => '',
278 + 'placeholder' => esc_html__( 'e.g. From {value}', 'filter-everything' ),
279 + 'instructions' => '',
280 + 'skip_view' => true,
281 + ),
282 + 'max_num_label' => array(
283 + 'type' => 'Text',
284 + 'label' => '',
285 + 'class' => 'wpc-field-max-num-label',
286 + 'default' => '',
287 + 'placeholder' => esc_html__( 'e.g. To {value}', 'filter-everything' ),
288 + 'instructions' => '',
289 + 'skip_view' => true,
290 + ),
183 291 'tooltip' => array(
184 292 'type' => 'Text',
185 293 'label' => esc_html__( 'Tooltip', 'filter-everything' ),
186 294 'class' => 'wpc-field-tooltip',
187 - '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' ),
188 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' )
189 318 )
190 319 );
191 320
192 321 $this->defaultFields = apply_filters( 'wpc_filter_default_fields', $defaultFields, $this );
@@ -191,72 +320,72 @@
191 320
192 321 $this->defaultFields = apply_filters( 'wpc_filter_default_fields', $defaultFields, $this );
193 322 }
194 323
195 - private function metaKeysOptions()
324 + public static function getViewOptions()
196 325 {
197 - $metaKeys = $this->getMetaKeys();
198 - $newKeys = [ '' => '' ];
326 + $viewOptions = array(
327 + 'checkboxes' => esc_html__('Checkboxes', 'filter-everything'),
328 + 'radio' => esc_html__('Radio buttons', 'filter-everything'),
329 + 'labels' => esc_html__('Labels list', 'filter-everything'),
330 + 'dropdown' => esc_html__('Dropdown', 'filter-everything'),
331 + 'range' => esc_html__('Numeric range', 'filter-everything'),
332 + 'date' => esc_html__('Date range', 'filter-everything'),
333 + );
199 334
200 - if( $metaKeys ){
201 - $excludedMetaKeys = flrt_get_forbidden_meta_keys();
202 -
203 - foreach ( $metaKeys as $key ) {
204 -
205 - if( ! preg_match('/[a-z]/i', $key ) ){
206 - continue;
207 - }
208 -
209 - if( in_array( $key, $excludedMetaKeys ) ){
210 - continue;
211 - }
212 -
213 - if( $key !== esc_attr( $key ) ){
214 - continue;
215 - }
216 -
217 - $newKeys[$key] = $key;
218 - }
335 + if(flrt_is_woocommerce()){
336 + $viewOptions['rating'] = esc_html__('Rating', 'filter-everything');
219 337 }
220 338
221 - return $newKeys;
339 + return $viewOptions;
222 340 }
223 341
224 - private function getMetaKeys()
342 + public static function getDateFormatOptions( $type = 'date' )
225 343 {
226 - global $wpdb;
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' );
227 349
228 - if(! is_admin()){
229 - return array();
230 - }
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 + );
231 355
232 - $limit = apply_filters( 'wpc_post_meta_keys_limit', 100 );
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' );
233 361
234 - $keys = $wpdb->get_col( $wpdb->prepare("SELECT DISTINCT meta_key FROM {$wpdb->postmeta} ORDER BY meta_key LIMIT %d", $limit ) );
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' );
235 371
236 - if ( $keys ) {
237 - natcasesort( $keys );
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;
238 377 }
239 378
240 - return apply_filters( 'wpc_default_meta_keys', $keys );
241 - }
379 + $formatOptions['other'] = '<span>' . esc_html__( 'Custom:', 'filter-everything' ) . '</span>';
242 380
243 - public static function getViewOptions()
244 - {
245 - $viewOptions = array(
246 - 'checkboxes' => esc_html__('Checkboxes', 'filter-everything'),
247 - 'radio' => esc_html__('Radio buttons', 'filter-everything'),
248 - 'labels' => esc_html__('Labels list', 'filter-everything'),
249 - 'dropdown' => esc_html__('Dropdown', 'filter-everything'),
250 - 'range' => esc_html__('Range', 'filter-everything')
251 - );
252 -
253 - return $viewOptions;
381 + return $formatOptions;
254 382 }
255 383
256 384 public function getOrderByOptions()
257 385 {
258 386 $orderBy = array(
387 + 'default' => esc_html__( 'Default (no sorting)', 'filter-everything' ),
259 388 'nameasc' => esc_html__( 'Term name &laquo;abc&raquo;', 'filter-everything' ),
260 389 'postcountasc' => esc_html__( 'Posts count &laquo;123&raquo;', 'filter-everything' ),
261 390 'idasc' => esc_html__( 'Term ID &laquo;123&raquo;', 'filter-everything' ),
262 391 'menuasc' => esc_html__( 'Menu order &laquo;123&raquo;', 'filter-everything' ),
@@ -283,10 +412,42 @@
283 412 return $selected;
284 413
285 414 }
286 415
287 - public function getEmptyFilterObject()
416 + private function getFilterNames( $set_id )
288 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 + {
289 450 $defaults = new \stdClass();
290 451 $defaults->ID = self::FLRT_NEW_FILTER_ID;
291 452 $defaults->post_parent = '';
292 453 $defaults->menu_order = 0;
@@ -295,9 +456,9 @@
295 456 $defaults->post_name = '';
296 457
297 458 $filter = $this->em->prepareFilter( $defaults );
298 459
299 - return $this->prepareFilterInputs( $filter );
460 + return $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
300 461 }
301 462
302 463 public function getFiltersInputs( $set_id )
303 464 {
@@ -307,17 +468,16 @@
307 468 return $preparedFilters;
308 469 }
309 470
310 471 $filters = $this->em->selectOnlySetFilters( $set_id );
311 -
312 472 foreach ( $filters as $field_key => $filter ){
313 - $preparedFilters[] = $this->prepareFilterInputs( $filter );
473 + $preparedFilters[] = $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) );
314 474 }
315 475
316 476 return $preparedFilters;
317 477 }
318 478
319 - private function prepareFilterInputs( $filter )
479 + private function prepareFilterInputsToDisplay( $filter, $filternames )
320 480 {
321 481 $postTypes = false;
322 482 $terms = [];
323 483 // Used for filter table class in filter set fields.
@@ -323,20 +483,46 @@
323 483 // Used for filter table class in filter set fields.
324 484 // E.g. post_meta, taxonomy, author.
325 485
326 486 $short_entity = $filter['entity'] ? $filter['entity'] : 'taxonomy';
327 - // Add hierarchical class
328 - if( $short_entity === 'taxonomy' ){
329 - if( is_taxonomy_hierarchical( $filter['e_name'] ) ){
330 - $short_entity = 'taxonomy taxonomy-hierarchical';
487 +
488 + if ( $short_entity === 'taxonomy' ) {
489 + // Add hierarchical class
490 + if ( is_taxonomy_hierarchical( $filter['e_name'] ) ) {
491 + $short_entity .= ' taxonomy-hierarchical';
331 492 }
493 +
494 + // Add product attribute class
495 + if ( strpos( $filter['e_name'], 'pa_' ) === 0 ) {
496 + $short_entity .= ' taxonomy-product-attribute';
497 + }
498 +
499 + if ( strpos( $filter['e_name'], 'product_visibility') !== false
500 + && $filter['view'] === 'rating') {
501 + $short_entity .= ' selected-and-above-show ';
502 + }
503 +
504 + if ( flrt_get_experimental_option('use_color_swatches') === 'on' ) {
505 + $taxonomies = flrt_get_experimental_option( 'color_swatches_taxonomies', [] );
506 +
507 + if ( in_array( $filter['e_name'], $taxonomies ) ) {
508 + $short_entity .= ' taxonomy-has-swatches';
509 + }
510 + }
332 511 }
333 512
513 + if ( in_array( $filter['e_name'], flrt_brand_filter_entities() ) ) {
514 + $short_entity .= ' wpc-filter-has-brands';
515 + }
516 +
517 +
518 + $short_entity .= isset( $filter['view'] ) ? ' wpc-view-'.$filter['view'] : '';
519 +
334 520 $belongs = $this->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] );
335 521
336 522 $postType = '';
337 523 // For post_meta_num entity postType is critical value so we have to set it up
338 - 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' ) ) ) {
339 525 $fss = Container::instance()->getFilterSetService();
340 526 $set = $fss->getSet( $filter['parent'] );
341 527 if( isset( $set['post_type']['value'] ) ){
342 528 $postType = $set['post_type']['value'];
@@ -342,12 +528,18 @@
342 528 $postType = $set['post_type']['value'];
343 529 }
344 530 }
345 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 +
346 537 $entity = $this->em->getEntityByFilter( $filter, $postType );
347 538
348 539 if( $entity ){
349 540 $terms = $entity->getTermsForSelect();
541 + asort( $terms, SORT_NATURAL );
350 542 }
351 543
352 544 // This is required only for filter fields inputs
353 545 $filter = $this->fse->combineEntityNameInFilter( $filter );
@@ -353,39 +545,60 @@
353 545 $filter = $this->fse->combineEntityNameInFilter( $filter );
354 546
355 547 foreach( $this->getFieldsMapping() as $fieldKey => $fieldData ){
356 548
357 - if( isset( $filter[$fieldKey] ) ){
549 + if ( isset( $filter[$fieldKey] ) ) {
358 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 +
359 562 $default_value = isset( $fieldData['default'] ) ? $fieldData['default'] : '';
360 563
361 - $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' ] ) );
362 565
363 566 $fieldData['name'] = $this->generateInputName( $filter['ID'], $fieldKey, $multiple );
364 567 $fieldData['id'] = $this->generateInputID( $filter['ID'], $fieldKey );
568 +
365 569 $fieldData['value'] = ( $filter[$fieldKey] ) ? $filter[$fieldKey] : $default_value;
366 570
367 - if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
368 - $fieldData['entity_belongs'] = true;
369 - }else{
370 - $fieldData['entity_belongs'] = $belongs;
571 + if( $fieldKey === 'hide_until_parent' ){
572 + if( (int) $filter['parent_filter'] > 0 ){
573 + $fieldData['additional_class'] = 'wpc-opened';
574 + }
371 575 }
372 576
373 - if( $fieldKey === 'entity' ){
374 - $fieldData['short_entity'] = $short_entity;
375 - }
376 -
377 - if( $fieldKey === 'slug' && $fieldData['value'] ){
577 + if ( $fieldKey === 'slug' && $fieldData['value'] ) {
378 578 $fieldData['readonly'] = 'readonly';
379 579 }
380 580
381 - if( $fieldKey === 'e_name' && $fieldData['value'] ){
581 + if ( $fieldKey === 'e_name' && $fieldData['value'] ) {
382 582 unset( $fieldData['options'] );
383 - $fieldData['readonly'] = 'readonly';
384 - $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 + }
385 590 }
386 591
387 - 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 +
388 601 // Add instead-entity field
389 602 if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){
390 603
391 604 /**
@@ -407,53 +620,113 @@
407 620 if( $fieldData['value'] === 'post_meta_exists' && ! defined('FLRT_FILTERS_PRO') ){
408 621 $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
409 622 }
410 623
624 + if( $fieldData['value'] === 'tax_numeric' && ! defined('FLRT_FILTERS_PRO') ){
625 + $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything');
626 + }
627 +
411 628 // Field, that will be visible instead of entity, that will be hidden.
412 629 $prepared[ 'instead-entity' ] = $this->getInsteadEntityField( $filter['ID'], $insteadEntityVal );
413 630
414 631 }
415 632
416 - 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 + }
417 638
418 - if( ! defined('FLRT_FILTERS_PRO') ){
419 - $fieldData['disabled'] = array('post_meta_exists');
420 - }
639 + }
421 640
641 + if (!defined('FLRT_FILTERS_PRO')) {
642 + if ($fieldKey === 'show_range_list') {
643 + $fieldData['value'] = '';
422 644 }
645 + }
423 646
424 - }
425 647
426 648 if( $fieldKey === 'exclude' ){
427 649 // We always add terms even they are empty array to fill Select2 with related terms.
428 650 $fieldData['options'] = $terms;
429 651
430 - if( $filter['entity'] === 'post_meta_num'){
652 + if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
431 653 $fieldData['options'] = [];
432 654 }
433 655 }
434 656
435 657 // Set disabled fields for some situations
436 - 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' ){
437 659 $fieldData['disabled'] = array('and');
438 660 }
439 661
440 - 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' ){
441 663 $fieldData['disabled'] = array('or');
442 664 }
443 665
444 - if( $filter['entity'] === 'post_meta_num' && $fieldKey === 'view' ){
445 - $fieldData['disabled'] = array('checkboxes', 'dropdown', 'radio', 'labels', 'colors', 'image');
446 - }else if( $filter['entity'] !== 'post_meta_num' && $fieldKey === 'view' ){
447 - $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 + }
448 704 }
449 705
450 - if( $fieldKey === 'orderby' && ( mb_strpos( $filter['entity'], 'taxonomy_pa' ) === false ) ){
451 - $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 + }
452 710 }
453 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'];
454 718
719 + if ( ! in_array( $value_to_custom, self::getPossibleDateFormats( $date_type ) ) ) {
720 + $disabled_custom = false;
721 + $fieldData['value'] = 'other';
722 + }
455 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 + }
456 729 }
457 730
458 731 $prepared[ $fieldKey ] = $fieldData;
459 732 }
@@ -474,10 +747,9 @@
474 747 $sanitizedFilter[ $key ] = $value;
475 748 }else{
476 749
477 750 if( is_array( $value ) ){
478 - array_map( 'esc_html', $value );
479 - $sanitizedFilter[ $key ] = $value;
751 + $sanitizedFilter[ $key ] = map_deep( $value, 'esc_html' );
480 752 } else {
481 753 $sanitizedFilter[ $key ] = esc_html( $value );
482 754 }
483 755
@@ -523,18 +795,15 @@
523 795
524 796 return $filter;
525 797 }
526 798
527 - public function validateTheFilter( $filter, $id = false ){
799 + public function validateTheFilter( $filter, $id = false ) {
528 800
529 801 $valid = true;
530 - $newFilter = false;
531 - $filterID = false;
532 802 $validEntity = true;
533 803 $validator = new Validator();
534 804
535 - if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){
536 - $newFilter = true;
805 + if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) {
537 806 $filterID = $id;
538 807 }else{
539 808 $filterID = $filter['ID'];
540 809 }
@@ -622,9 +891,9 @@
622 891 /**
623 892 * Slug validations
624 893 */
625 894 if( isset( $filter['slug'] ) ){
626 - $existingSlugs = get_option('wpc_filter_permalinks');
895 + $existingSlugs = get_option('wpc_filter_permalinks', []);
627 896 $prefix = $filter['slug'];
628 897
629 898 // Check this only for new filters
630 899 if( $filter['ID'] === self::FLRT_NEW_FILTER_ID && $validEntity ) {
@@ -638,23 +907,23 @@
638 907 }
639 908
640 909 // Ensure, that prefix contains at least one alphabetic character
641 910 // Also this prevents from empty prefix
642 - if ( ! $validator->validateAlphabCharsExists($prefix) ) {
911 + if ( ! $validator->validateAlphabCharsExists( $prefix ) ) {
643 912 $this->pushError( 37, $filterID, 'slug' ); // Invalid prefix. Has to contain at least one alphabetic symbol
644 913 $valid = false;
645 914 }
646 915
647 916 // Check hyphens problem when cat and cat-x exists
648 - if (!$validator->validatePrefixHyphens($prefix, $existingSlugs) ) {
917 + if ( ! $validator->validatePrefixHyphens( $prefix, $existingSlugs ) ) {
649 918 $this->pushError( 39, $filterID, 'slug' ); // Invalid prefix. Incorrect hyphens.
650 919 $valid = false;
651 920 }
652 921
653 922 // Check for slugs, that matches with native WP Entities
654 - if( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ){
923 + if ( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ) {
655 924 $errorCode = 3991;
656 - if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ){
925 + if ( defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) {
657 926 $errorCode = 399;
658 927 }
659 928 $this->pushError( $errorCode, $filterID, 'slug' ); // Invalid prefix. Equal to wp entity.
660 929 $valid = false;
@@ -667,36 +936,83 @@
667 936
668 937 /**
669 938 * E_name validations
670 939 */
671 - if( isset( $filter['e_name'] ) ){
940 + if ( isset( $filter['e_name'] ) ) {
672 941
673 942 if( isset( $filter['entity'] ) ) {
674 - 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' ) ) ) {
675 944 $e_name = $filter['e_name'];
676 945
677 - if( $e_name === '' ){
678 - $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.
679 948 $valid = false;
680 949 } else {
681 - // Should not be empty and should contain at least one alphabetic character
682 - if (!$validator->validateAlphabCharsExists($e_name)) {
683 - $this->pushError(40, $filterID, 'e_name'); // Invalid E_name.
684 - $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 + }
685 983 }
984 + }
985 + }
686 986
687 - // Should not contain characters that escaped by esc_attr
688 - if (!$validator->validateEscAttrCharacters($e_name)) {
689 - $this->pushError(40, $filterID, 'e_name'); // Invalid E_name.
690 - $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;
691 1013 }
692 1014
693 - // Some meta keys should be prohibited to use
694 - $excludedMetaKeys = flrt_get_forbidden_meta_keys();
695 - if( in_array( $e_name, $excludedMetaKeys, true ) ){
696 - $this->pushError( 40, $filterID, 'e_name'); // Invalid E_name.
697 - $valid = false;
698 - }
699 1015 }
700 1016 }
701 1017 }
702 1018
@@ -704,8 +1020,22 @@
704 1020 $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name.
705 1021 $valid = false;
706 1022 }
707 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 +
708 1038 /**
709 1039 * View validations
710 1040 */
711 1041 if( isset( $filter['view'] ) ){
@@ -721,8 +1051,18 @@
721 1051 $valid = false;
722 1052 }
723 1053
724 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 + /**
725 1065 * Logic validations
726 1066 */
727 1067 if( isset( $filter['logic'] ) ){
728 1068
@@ -731,9 +1071,9 @@
731 1071 $valid = false;
732 1072 }
733 1073
734 1074 // For author and post_meta_exists entities logic can be only OR
735 - 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' ) ) ){
736 1076 if( $filter['logic'] !== 'or' ){
737 1077 $this->pushError( 45, $filterID, 'logic' ); // Not acceptable logic.
738 1078 $valid = false;
739 1079 }
@@ -739,9 +1079,9 @@
739 1079 }
740 1080 }
741 1081
742 1082 // For author entity logic can be only OR
743 - if( $filter['entity'] === 'post_meta_num' ){
1083 + if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
744 1084 if( $filter['logic'] !== 'and' ){
745 1085 $this->pushError( 47, $filterID, 'logic' ); // Not acceptable logic.
746 1086 $valid = false;
747 1087 }
@@ -780,11 +1120,18 @@
780 1120 $valid = false;
781 1121 }
782 1122 }
783 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 +
784 1131 // In case when checkbox is not checked there is no $_POST['in_path'] parameter
785 1132 if( isset( $filter['in_path'] ) ){
786 - 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' ){
787 1134 $this->pushError( 46, $filterID, 'in_path' ); // Invalid In Path for Post meta num.
788 1135 $valid = false;
789 1136 }
790 1137 }
@@ -813,39 +1160,42 @@
813 1160
814 1161 return $valid;
815 1162 }
816 1163
817 - public function validateFilters( $filters ){
1164 + public function validateFilters( $filters ) {
818 1165
819 - if( ! $filters ){
1166 + if ( ! $filters ) {
820 1167 return false;
821 1168 }
822 1169 $valid = true;
823 1170
824 1171 // Check permissions
825 - 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.
826 1174 $this->pushError(202);
827 1175 $valid = false;
828 1176 }
829 1177
830 1178 // Check for equal filters
831 - foreach ( (array) $filters as $filter ){
832 - if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ){
1179 + foreach ( (array) $filters as $filter ) {
1180 + if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ) {
833 1181 // To avoid few filters with the same meta key
834 - if( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists' ), true ) ){
835 - $keys[] = 'post_meta'.$filter['e_name'];
836 - }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 {
837 1187 $keys[] = $filter['entity'] . $filter['e_name'];
838 1188 }
839 1189 }
840 1190 }
841 1191
842 - if( flrt_array_contains_duplicate( $keys ) ){
1192 + if ( flrt_array_contains_duplicate( $keys ) ) {
843 1193 $this->pushError(31); // Equal filters
844 1194 $valid = false;
845 1195 }
846 1196
847 - return $valid;
1197 + return apply_filters( 'wpc_validate_filter_fields', $valid, $this );
848 1198 }
849 1199
850 1200 public function ajaxValidateFilters()
851 1201 {
@@ -850,8 +1200,19 @@
850 1200 public function ajaxValidateFilters()
851 1201 {
852 1202 $postData = Container::instance()->getThePost();
853 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 + }
854 1215 $response = [];
855 1216
856 1217 if( ! $data ){
857 1218 $this->pushError(20);
@@ -1010,9 +1371,9 @@
1010 1371
1011 1372 $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false;
1012 1373 $errorResponse = array(
1013 1374 'fid' => $filterId,
1014 - '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')
1015 1376 );
1016 1377
1017 1378 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
1018 1379 wp_send_json_error( $errorResponse );
@@ -1035,8 +1396,104 @@
1035 1396 wp_send_json_error( $errorResponse );
1036 1397 }
1037 1398 }
1038 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 +
1039 1496 public function sendExcludedTerms()
1040 1497 {
1041 1498 $container = Container::instance();
1042 1499 $postData = $container->getThePost();
@@ -1047,9 +1504,9 @@
1047 1504 $e_name = isset( $postData['ename'] ) ? $postData['ename'] : false;
1048 1505
1049 1506 $errorResponse = array(
1050 1507 'fid' => $filterId,
1051 - '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')
1052 1509 );
1053 1510
1054 1511 if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){
1055 1512 wp_send_json_error( $errorResponse );
@@ -1080,8 +1537,35 @@
1080 1537 }
1081 1538
1082 1539 }
1083 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 +
1084 1568 public function addSpinnerToSelect( $html, $attributes )
1085 1569 {
1086 1570 if( isset( $attributes['class'] ) ){
1087 1571 $requiredIds = array(
@@ -1113,9 +1597,10 @@
1113 1597 'value' => $insteadEntityVal,
1114 1598 'readonly' => 'readonly',
1115 1599 'default' => '',
1116 1600 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything'),
1117 - '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
1118 1603 );
1119 1604
1120 1605 return $insteadEntity;
1121 1606 }
@@ -1150,17 +1635,33 @@
1150 1635 if ( empty( $post_type ) ){
1151 1636 return false;
1152 1637 }
1153 1638
1154 - 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 + ) ) ){
1155 1646 return true;
1156 1647 }
1157 1648
1158 - 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' ) ){
1159 1650 return true;
1160 1651 }
1161 1652
1162 - 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 +
1163 1664 return $this->isTaxonomyBelongsToPostType( $post_type, $e_name );
1164 1665 }
1165 1666
1166 1667 return false;
@@ -1165,11 +1666,11 @@
1165 1666
1166 1667 return false;
1167 1668 }
1168 1669
1169 - public function getErrorMessage( $code = 20 )
1670 + public function getErrorMessage( $code = 20, $limit = null )
1170 1671 {
1171 - $messages = $this->getErrorsList();
1672 + $messages = self::getErrorsList($limit);
1172 1673
1173 1674 if( isset( $messages[$code] ) ){
1174 1675 return $messages[$code];
1175 1676 }
@@ -1178,9 +1679,10 @@
1178 1679 }
1179 1680
1180 1681 public function pushError( $errorCode, $filterId = false, $filterKey = '' )
1181 1682 {
1182 - $error = array(
1683 + $to_add = true;
1684 + $error = array(
1183 1685 'code' => $errorCode
1184 1686 );
1185 1687
1186 1688 if( $filterId && $filterKey ){
@@ -1186,11 +1688,23 @@
1186 1688 if( $filterId && $filterKey ){
1187 1689 $error['id'] = $this->generateInputID( $filterId, $filterKey );
1188 1690 }
1189 1691
1190 - $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 + }
1191 1700
1192 - return $error;
1701 + if ( $to_add ) {
1702 + $this->errors[] = $error;
1703 + return $error;
1704 + }
1705 +
1706 + return false;
1193 1707 }
1194 1708
1195 1709 public function getErrors()
1196 1710 {
@@ -1265,21 +1779,25 @@
1265 1779 }
1266 1780 return $name;
1267 1781 }
1268 1782
1269 - public static function getErrorsList()
1783 + public static function getErrorsList($limit = null)
1270 1784 {
1785 + $effectiveLimit = is_int($limit) ? $limit : FilterSet::FREE_LIMIT_NEW;
1786 +
1271 1787 $errors = array(
1272 1788 // Set errors
1273 - 20 => esc_html__('An error occurred. Set fields were not saved, please try again.', 'filter-everything'),
1274 - 201 => esc_html__('An error occurred. Rule fields were not saved, please try again.', 'filter-everything'),
1275 - 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'),
1276 1792 211 => esc_html__( 'Error: invalid WP Page type.', 'filter-everything' ),
1277 1793 21 => esc_html__( 'Error: invalid post type.', 'filter-everything' ),
1278 - 22 => esc_html__( 'Error: invalid location.', 'filter-everything' ),
1794 + 22 => esc_html__( 'Error: invalid value of the Where to filter? field.', 'filter-everything' ),
1279 1795 221 => esc_html__( 'Error: invalid query.', 'filter-everything' ),
1280 1796 23 => esc_html__( 'Error: invalid Hide empty field.', 'filter-everything' ),
1281 1797 24 => esc_html__( 'Error: invalid Show count field.', 'filter-everything' ),
1798 + 25 => esc_html__( 'Error: invalid Horizontal layout field.', 'filter-everything' ),
1799 + 242 => esc_html__( 'Error: invalid «Apply Button» mode field.', 'filter-everything' ),
1282 1800 // 241 => esc_html__( 'Error: invalid Order field.', 'filter-everything' ),
1283 1801 // Filters errors
1284 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' ),
1285 1803 32 => esc_html__( 'Error: invalid fields present.', 'filter-everything' ),
@@ -1286,22 +1804,25 @@
1286 1804 33 => esc_html__( 'Error: invalid filter ID.', 'filter-everything' ),
1287 1805 34 => esc_html__( 'Error: invalid set ID.', 'filter-everything' ),
1288 1806 35 => esc_html__( 'Error: invalid filter entity.', 'filter-everything' ),
1289 1807 36 => esc_html__( 'Error: filter prefix is already used for another entity.', 'filter-everything' ),
1290 - 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' ),
1291 1809 38 => esc_html__( 'Error: filter prefix should not be empty.', 'filter-everything' ),
1292 - 39 => esc_html__( 'Error: prefix part before "-" character can not be equal with other existing prefix.', 'filter-everything' ),
1293 - 399 => esc_html__( 'Error: such prefix is not allowed. Please, use another prefix.', 'filter-everything' ),
1294 - 3991 => esc_html__( 'Error: such var name is not allowed. Please, use another name.', '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' ),
1295 1813 40 => esc_html__( 'Error: invalid Meta key value', 'filter-everything' ),
1296 - 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' ),
1297 1816 41 => esc_html__( 'Error: invalid View parameter.', 'filter-everything' ),
1817 + 411 => esc_html__( 'Error: invalid Date Type parameter.', 'filter-everything' ),
1298 1818 42 => esc_html__( 'Error: invalid Logic parameter', 'filter-everything' ),
1299 - 43 => esc_html__( 'Error: invalid Orderby parameter', 'filter-everything' ),
1819 + 43 => esc_html__( 'Error: invalid the Sort Terms by parameter', 'filter-everything' ),
1300 1820 44 => esc_html__( 'Error: invalid exclude terms', 'filter-everything' ),
1821 + 441 => esc_html__( 'Error: invalid include checkbox value', 'filter-everything' ),
1301 1822 45 => esc_html__( 'Error: for filter Post Author logic OR is acceptable only.', 'filter-everything' ),
1302 - 46 => esc_html__( 'Error: Post meta num filter can not be in URL path.', 'filter-everything' ),
1303 - 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' ),
1304 1825 48 => esc_html__( 'Error: Range slider is acceptable for Post Meta Num filters only.', 'filter-everything' ),
1305 1826 50 => esc_html__( 'Error: SEO Rule must have specified Post Type.', 'filter-everything' ),
1306 1827 51 => esc_html__( 'Error: SEO Rule must contain at least one filter.', 'filter-everything' ),
1307 1828 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ),
@@ -1307,9 +1828,39 @@
1307 1828 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ),
1308 1829 53 => esc_html__( 'Error: invalid or forbidden filter presents.', 'filter-everything' ),
1309 1830 54 => esc_html__( 'Error: invalid SEO Rule ID.', 'filter-everything' ),
1310 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 +
1311 1862 );
1312 1863
1313 1864 return $errors;
1314 1865 }
1315 1866 }