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

RequestParser.php in Filter Everything — WordPress & WooCommerce Filters 1.9.6, at src/RequestParser.php

646 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 class RequestParser
10 {
11 private $request;
12
13 private $queryVars = [];
14
15 private $separator;
16
17 private $separator_for_path_segments;
18
19 public function __construct( $request, $separator = FLRT_PREFIX_SEPARATOR, $separator_for_getPathSegments = '/')
20 {
21 $this->setRequest( $request );
22 $this->separator = $separator;
23 $this->separator_for_path_segments = $separator_for_getPathSegments;
24 }
25
26 private function initQueryVars(){
27 // Setup default $queryVars
28 $this->queryVars = array(
29 'queried_values' => [],
30 'segments_order' => [],
31 'wpc_logic_separators' => [],
32 'non_filter_segments' => [],
33 'error' => '',
34 );
35 }
36
37 public function getQueryVars(){
38 $this->initQueryVars();
39 $this->parseRequest();
40 $this->validateQueryVars();
41 return apply_filters( 'wpc_query_vars', $this->queryVars );
42 }
43
44 private function isSlugInRequest( $slug ){
45 return ( $this->isSlugInQuerySting( $slug ) || $this->isSlugInPath( $slug ) );
46 }
47
48 public function detectFilterRequest(){
49 $em = Container::instance()->getEntityManager();
50 foreach ( $em->getGlobalConfiguredSlugs() as $slug ){
51 if( $this->isSlugInRequest( $slug ) ){
52 return true;
53 }
54 }
55 return false;
56 }
57
58 private function isSlugInPath( $slug ){
59 if( mb_strpos( $this->separator_for_path_segments . $this->request, $this->separator_for_path_segments . $slug . $this->separator ) !== false ){
60 return true;
61 }
62 return false;
63 }
64
65 /**
66 * Checks whether the specified slug exists in the Query String or not
67 * @param $slug
68 * @return bool
69 */
70 private function isSlugInQuerySting( $slug ){
71 /**
72 * This case happens most often and that's why it is first
73 */
74 if ( $this->extractQueryStringTheParamValues( $slug ) !== false ) {
75 return true;
76 }
77
78 /**
79 * This case happens more rarely
80 */
81 if (
82 ( $this->extractQueryStringTheParamValues( 'max_' . $slug ) !== false )
83 ||
84 ( $this->extractQueryStringTheParamValues( 'min_' . $slug ) !== false )
85 ) {
86 return true;
87 }
88
89 /**
90 * This very rarely
91 */
92 if (
93 ( $this->extractQueryStringTheParamValues( $slug . '_to' ) !== false )
94 ||
95 ( $this->extractQueryStringTheParamValues( $slug . '_from' ) !== false )
96 ) {
97 return true;
98 }
99
100 return false;
101 }
102
103 /**
104 * Extracts from the Query String GET value with specified $key if it exists
105 * Result always must be checked for !== false because 0 may be returned
106 * Examples: 'instock;onbackorder', '118.81', '59.03'
107 * @param $key [max_{slug}|min_{slug}|{slug}]
108 * @return false|mixed
109 */
110 public function extractQueryStringTheParamValues( $key )
111 {
112 $container = Container::instance();
113 $get = $container->getTheGet();
114 $post = $container->getThePost();
115
116 if( isset( $post['flrt_ajax_link'] ) ){
117 $parts = parse_url( $post['flrt_ajax_link'] );
118
119 if( isset( $parts['query'] ) ){
120 parse_str( $parts['query'], $output );
121 if( isset( $output[$key] ) ){
122 return $this->urlEncodeGetValues( $output[$key] );
123 }
124 }
125 }
126
127 if( isset( $get[$key] ) ){
128 return $this->urlEncodeGetValues( $get[$key] );
129 }
130
131 return false;
132 }
133
134 private function urlEncodeGetValues( $values )
135 {
136 if ( is_array( $values ) ) {
137 $queriedValues = $values;
138 } elseif ( is_string( $values ) ) {
139 $queriedValues = explode( FLRT_QUERY_TERMS_SEPARATOR, $values );
140 } else {
141 $queriedValues = [];
142 }
143
144 $queriedValues = array_map( 'urlencode', $queriedValues );
145 $queriedValues = array_map( 'mb_strtolower', $queriedValues );
146
147 return implode(FLRT_QUERY_TERMS_SEPARATOR, $queriedValues );
148 }
149
150 /**
151 * Extracts all filter values from the Query String
152 * @param $slug string a filter slug
153 * @return array
154 */
155 private function extractValuesFromQueryString( $slug ) {
156 $em = Container::instance()->getEntityManager();
157 $filter = $em->getFilterBySlug( $slug /*, array( 'entity' )*/ );
158
159 $values = [];
160
161 /**
162 * Numeric filters
163 */
164 if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric' ] ) ) {
165 // Matches numbers and decimal separator
166 $regexp = '/^([\-]?\d+(?:[\.\,]\d{1,})?)$/';
167
168 if( $this->extractQueryStringTheParamValues( 'min_' . $slug ) !== false ){
169 /**
170 * Safely extract only allowed in numeric filters characters
171 */
172 preg_match($regexp, $this->extractQueryStringTheParamValues( 'min_' . $slug ), $output);
173 $values['min'] = isset( $output[1] ) ? $output[1] : false;
174 }
175
176 if( $this->extractQueryStringTheParamValues( 'max_' . $slug ) !== false ){
177 /**
178 * Safely extract only allowed in numeric filters characters
179 */
180 preg_match($regexp, $this->extractQueryStringTheParamValues( 'max_' . $slug ), $output);
181 $values['max'] = isset( $output[1] ) ? $output[1] : false;
182 }
183 }
184
185 /**
186 * Date filters
187 */
188 if ( in_array( $filter['entity'], [ 'post_date', 'post_meta_date' ] ) ) {
189 // We accept only values that are YYYY-MM-DD or hh.mm.ss or both YYYY-MM-DDthh.mm.ss
190 // We do not accept values YYYY-MM, YYYY, hh.mm, mm.ss etc
191 if ( $this->extractQueryStringTheParamValues( $slug . '_from' ) !== false ) {
192 $from = $this->extractQueryStringTheParamValues( $slug . '_from' );
193 $datetime = $this->parseDate( $from );
194 if ( $datetime ) {
195 $values['from'] = $datetime;
196 } else {
197 $this->set_404( 'Invalid date format' );
198 }
199 }
200
201 if ( $this->extractQueryStringTheParamValues( $slug . '_to' ) !== false ) {
202 $to = $this->extractQueryStringTheParamValues( $slug . '_to' );
203 $datetime = $this->parseDate( $to );
204 if ( $datetime ) {
205 $values['to'] = $datetime;
206 } else {
207 $this->set_404( 'Invalid date format' );
208 }
209 }
210
211 /**
212 * Check if both datetime values 'from' and 'to' have no different format e.g. 2023-04-12 and 17.23.00
213 * simultaneously.
214 * We don't need to generate 404 error because they are GET parameters and we can just
215 * ignore these parameters and open the same page like without them.
216 */
217 if ( isset( $values['from'] ) && isset( $values['to'] ) ) {
218 if ( ! $this->haveDateValuesEqualFormat( $values ) ) {
219 $values = [];
220 $this->set_404( 'Values have different format' );
221 }
222 }
223 //@todo if from date is bigger than to date it means that it is also invalid format
224 // Maybe we have to check this and do not process it.
225 //@todo Maybe we have to remove date queried value if they have invalid format
226 // to avoid these values processing further
227 }
228
229 /**
230 * In the Free plugin version without Permalinks
231 * All slugs located in the Query String URL part
232 */
233 if ( ( $this->extractQueryStringTheParamValues( $slug ) !== false ) ) {
234 if ( ! in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
235 /**
236 * If it is Free version with all filter values in the Query String
237 * we also have to check if terms exists on our site.
238 * Otherwise generate the 404 error.
239 */
240 $params = $this->extractQueryStringTheParamValues( $slug );
241 $values = $this->safeExtractFilterValuesFromQueryString( $params, $slug );
242 } else {
243 /**
244 * For numeric filters just extract numeric values
245 */
246 $values[] = $this->extractQueryStringTheParamValues( $slug );
247 }
248 }
249
250 unset($em);
251
252 return $values;
253 }
254
255 /**
256 * Checks if the date has valid and accepted format and returns
257 * @param $date
258 * @return string datetime in format YYYY-MM-DDthh.mm.ss OR empty string if date is invalid
259 */
260 private function parseDate( $date ) {
261 $date = urldecode( $date );
262 $maybe_time = $maybe_date = false;
263 $queried_value = '';
264 $valid = true;
265
266 if ( ! $date ) {
267 return $queried_value;
268 }
269
270 if ( strpos( $date, FLRT_DATE_TIME_SEPARATOR ) !== false ) {
271 $pieces = explode(FLRT_DATE_TIME_SEPARATOR, $date );
272 $maybe_date = $pieces[0];
273 $maybe_time = $pieces[1];
274 } else {
275 if ( strpos( $date, '.') !== false ) {
276 $maybe_time = $date;
277 } else if ( strpos( $date, '-') !== false ) {
278 $maybe_date = $date;
279 } else {
280 $valid = false;
281 }
282 }
283
284 if ( $maybe_time && ! $this->isValidDate( $maybe_time, "H.i.s" ) ) {
285 $valid = false;
286 }
287 if ( $maybe_date && ! $this->isValidDate( $maybe_date, "Y-m-d") ) {
288 $valid = false;
289 }
290
291 if ( $valid ) {
292
293 if ( $maybe_date && $maybe_time ) {
294 $queried_value = $maybe_date .' '. $maybe_time;
295 } else {
296 if ($maybe_date) {
297 $queried_value = $maybe_date;
298 }
299
300 if ($maybe_time) {
301 $queried_value = $maybe_time;
302 }
303 }
304 }
305
306 return $queried_value;
307 }
308
309 private function isValidDate( $date_or_time, $format = 'Y-m-d' ) {
310 try{
311 $dateObj = new \DateTime( $date_or_time );
312 return $dateObj && $dateObj->format( $format ) === $date_or_time;
313 } catch ( \Exception $e ){
314 return false;
315 }
316 }
317
318 /**
319 * Checks if a given datetime has equal format for both from and to values
320 * @param $values
321 * @return bool
322 */
323 private function haveDateValuesEqualFormat( $values ) {
324 $valid = true;
325
326 if ( ! isset( $values['from'] ) || ! isset( $values['to'] ) ) {
327 return false;
328 }
329
330 $date_1 = str_replace( FLRT_DATE_TIME_SEPARATOR, ' ', $values['from'] );
331 $date_2 = str_replace( FLRT_DATE_TIME_SEPARATOR, ' ', $values['to'] );
332
333 $pcs_1 = date_parse( $date_1 );
334 $pcs_2 = date_parse( $date_2 );
335
336 $parts_1 = [];
337 $parts_2 = [];
338
339 foreach ( [ 'year', 'month', 'day', 'hour', 'minute', 'second' ] as $item ) {
340 if ( isset( $pcs_1[$item] ) && $pcs_1[$item] !== false ) {
341 $parts_1[] = $item;
342 }
343
344 if ( isset( $pcs_2[$item] ) && $pcs_2[$item] !== false ) {
345 $parts_2[] = $item;
346 }
347
348 }
349
350 if( count( array_diff( $parts_1, $parts_2 ) ) > 0 ) {
351 $valid = false;
352 }
353
354 return $valid;
355 }
356
357 private function set_404( $message = '' ){
358 $this->queryVars['error'] = '404';
359 if( $message && FLRT_PLUGIN_DEBUG ){
360 echo esc_html( $message );
361 }
362 }
363
364 public function getRequest(){
365 return $this->request;
366 }
367
368 public function setRequest( $request ){
369 $this->request = strtolower( trim( $request, '/' ) );
370 }
371
372 /**
373 * @return array
374 */
375 private function getPathSegments(){
376 if( $this->request ){
377 return explode($this->separator_for_path_segments, $this->request );
378 }
379 return [];
380 }
381
382 public function cleanUpRequestPathFromFilterSegments( $request_path ){
383 // Otherwise it will be URL encoded with uppercase characters
384 // But tax term slugs always stored lowercase in WordPress DB
385 $request_path = strtolower($request_path);
386
387 foreach( $this->getPathSegments() as $segment ){
388 if( $this->checkSlugInSegmentForCleaningNativePath( $segment ) ){
389 /**
390 *@improvement Maybe remove query_args also
391 */
392 $request_path = str_replace($this->separator_for_path_segments . $segment, '', $request_path );
393 }
394 }
395
396 return $request_path;
397 }
398
399 public function parseRequest(){
400 /**
401 * @bug this method fires twice.
402 */
403
404 $pathSegments = apply_filters( 'wpc_filter_path_segments', $this->getPathSegments() );
405 $em = Container::instance()->getEntityManager();
406 $fse = Container::instance()->getFilterService();
407 // Path values
408 foreach( $pathSegments as $segment ){
409 if( $slug = $this->getSlugFromSegment( $segment ) ){
410 $segmentParams = $this->cutParamsFromSegment( $segment, $slug );
411 // List of entity, e_name, slug should be unique for all filters
412 $filter_entity = $em->getFilterBySlug( $slug /*, array( 'entity', 'e_name', 'slug', 'in_path' )*/ );
413 $filter_entity['values'] = $this->extractQueriedValuesFromSegment( $segmentParams, $slug );
414 $filter_entity['founded_in_path'] = 'yes';
415 $this->queryVars['queried_values'][$slug] = $filter_entity;
416
417 $order_element = $fse->getEntityFullName( $filter_entity['entity'], $filter_entity['e_name'] );
418
419 $this->queryVars['segments_order'][] = $order_element;
420 } else {
421 $this->queryVars['non_filter_segments'][] = $segment;
422 }
423 }
424
425 // Query string values
426 foreach ( $em->getConfiguredQuerySlugs() as $slug ) {
427 if ( $this->isSlugInQuerySting( $slug ) ) {
428 $filter_entity = $em->getFilterBySlug( $slug /*, array( 'entity', 'e_name', 'slug', 'in_path' ) */ );
429 $filter_entity['values'] = $this->extractValuesFromQueryString( $slug );
430 $filter_entity['founded_in_path'] = 'no';
431 $this->queryVars['queried_values'][$slug] = $filter_entity;
432 }
433 }
434
435 unset($em, $fse);
436 }
437
438 private function checkSlugInSegmentForCleaningNativePath( $segment ){
439 $em = Container::instance()->getEntityManager();
440 $permalinks_disabled = (defined( 'FLRT_PERMALINKS_ENABLED' ) && !FLRT_PERMALINKS_ENABLED);
441
442 foreach( $em->getConfiguredPathSlugs() as $key => $slug ){
443 if( mb_strpos( $segment, $slug . $this->separator ) === 0 ){
444 return $permalinks_disabled ? false : $slug;
445 }
446 }
447 return false;
448 }
449
450 private function getSlugFromSegment( $segment ){
451 $em = Container::instance()->getEntityManager();
452 foreach( $em->getConfiguredPathSlugs() as $key => $slug ){
453 if( mb_strpos( $segment, $slug . $this->separator ) === 0 ){
454 return $slug;
455 }
456 }
457 return false;
458 }
459
460 private function cutParamsFromSegment( $segment, $slug ){
461 return mb_substr( $segment, mb_strlen( $slug . $this->separator ) );
462 }
463
464 private function checkValuesOrder( $segmentParams, $sep ){
465 $fse = Container::instance()->getFilterService();
466 $terms = explode( $sep, $segmentParams );
467 $terms = $fse->sortTerms($terms);
468 $sortedParams = implode( $sep, $terms );
469
470 if( $segmentParams !== $sortedParams ){
471 return false;
472 }
473
474 return true;
475 }
476
477 /**
478 * @param string $segmentParams specially formatted sting like two#or#or-or-three#and
479 * @param array $filters filters arrays with logic value
480 */
481 private function extractLogicSeparator( $segmentParams, $filters ){
482 $fse = Container::instance()->getFilterService();
483 foreach( $filters as $filter ){
484 $logicSeparator = $fse->getLogicSeparator( $filter['logic'] ); // -or- | -and-
485 if( mb_strpos( $segmentParams, $logicSeparator ) !== false ){
486 $this->queryVars['wpc_logic_separators'][$filter['slug']] = $filter['logic'];
487 return $logicSeparator;
488 }
489 }
490
491 return false;
492 }
493
494 private function safeExtractFilterValuesFromQueryString( $filterParams, $slug ) {
495 // $filterParams = accessories;tshirts
496 $em = Container::instance()->getEntityManager();
497 $allEntityTerms = $em->getEntityAllTermsSlugs( $slug );
498 $queriedValues = $em->safeExplodeFilterValues( $filterParams, $slug, FLRT_QUERY_TERMS_SEPARATOR );
499 $queriedValues = $em->safeImplodeFilterValues( $queriedValues, FLRT_QUERY_TERMS_SEPARATOR );
500
501 $allEntityTerms_flipped = array_flip( $allEntityTerms );
502 foreach ( $queriedValues as $k => $value ) {
503 if ( ! isset( $allEntityTerms_flipped[$value] ) ) {
504 unset( $queriedValues[$k] );
505 $this->set_404( 'Term does not exist - ' . $value );
506 }
507 }
508
509 // Check duplicates
510 if( flrt_array_contains_duplicate( $queriedValues ) ){
511 $this->set_404('Param duplicates');
512 $queriedValues = array_unique($queriedValues);
513 }
514
515 unset( $em );
516
517 return $queriedValues;
518 }
519
520 /**
521 * @param string $segmentParams
522 * @param string $slug
523 * @return false|array
524 */
525 private function extractQueriedValuesFromSegment( $segmentParams, $slug ){
526
527 $em = Container::instance()->getEntityManager();
528 $allEntityTerms = $em->getEntityAllTermsSlugs( $slug );
529
530 $filters = $em->getAllFiltersBySlug( $slug /*, array( 'logic', 'slug' )*/ );
531
532 $segmentParams = $em->safeExplodeFilterValues( $segmentParams, $slug, $this->separator, false );
533 $logicSeparator = $this->extractLogicSeparator( $segmentParams, $filters );
534
535 // $segmentParams = two#or#or-or-three#and
536 // $valueSeparator = '-or-'
537 if( $logicSeparator ) {
538
539 $queriedValues = explode( $logicSeparator, $segmentParams );
540
541 if ( ! $this->checkValuesOrder($segmentParams, $logicSeparator) ) {
542 /**
543 * @feature maybe redirect to URL with correct order of values
544 */
545 $this->set_404('Invalid params order');
546 }
547 }else{
548 $queriedValues[0] = $segmentParams;
549 }
550
551 $queriedValues = $em->safeImplodeFilterValues( $queriedValues, $this->separator );
552
553 $allEntityTerms_flipped = array_flip( $allEntityTerms );
554 foreach ( $queriedValues as $k => $value ) {
555 if ( ! isset( $allEntityTerms_flipped[$value] ) ) {
556 unset( $queriedValues[$k] );
557 $this->set_404( 'Term does not exist - ' . $value );
558 }
559 }
560
561 // Check duplicates
562 if( flrt_array_contains_duplicate( $queriedValues ) ){
563 $this->set_404('Param duplicates');
564 $queriedValues = array_unique($queriedValues);
565 }
566
567 unset($em);
568
569 return $queriedValues;
570 }
571
572 private function validateSegmentsOrder( $template = [] ){
573 $fse = Container::instance()->getFilterService();
574
575 if( ! $template ){
576 $template = $fse->getFiltersOrder();
577 }
578
579 $to_compare = $this->queryVars['segments_order'];
580
581 if( flrt_array_contains_duplicate( $to_compare ) ){
582 return false;
583 }
584
585 if( ! is_array( $template ) || ! is_array( $to_compare ) ){
586 return false;
587 }
588
589 $new_template = array_intersect( $template, $to_compare );
590
591 if( empty( $new_template ) ){
592 return false;
593 }
594
595 $new_template = array_values( $new_template );
596 $already_compared = [];
597 $i = 0;
598
599 foreach ( $to_compare as $index => $value ) {
600 if( in_array( $value, $already_compared ) ){
601 $existing_index = array_search( $value, $already_compared );
602 $existing_index++;
603 if( isset( $already_compared[$existing_index] ) ){
604 return false;
605 }
606 }
607
608 if( $value === $new_template[$i] ){
609 $i++;
610 $already_compared[] = $value;
611 }
612 }
613
614 unset( $fse );
615
616 return ( $new_template === $already_compared );
617
618 }
619
620 private function validateQueryVars(){
621 // Check for segments duplicates
622 $maybe_duplicates = [];
623 $fse = Container::instance()->getFilterService();
624
625 if( ! empty( $this->queryVars['queried_values'] ) ){
626 foreach( $this->queryVars['queried_values'] as $filter ){
627 $maybe_duplicates[] = $fse->getEntityFullName( $filter['entity'], $filter['e_name'] );
628 }
629
630 if( flrt_array_contains_duplicate( $maybe_duplicates ) ){
631 $this->set_404( 'Segment duplicates' );
632 }
633 }
634
635 // Check segments order
636 if( ! empty( $this->queryVars['segments_order'] ) ) {
637 if (!$this->validateSegmentsOrder()) {
638 $this->set_404('Invalid segments order');
639 }
640 }
641
642 unset( $fse );
643 // Check something other
644 // If max < than min maybe should be an error
645 }
646 }