PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.1
Filter Everything — WordPress & WooCommerce Filters v1.9.1
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.1, at src/RequestParser.php

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