PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Indexable / Post / DateQuery.php

DateQuery.php in ElasticPress 5.3.5, at includes/classes/Indexable/Post/DateQuery.php

588 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Support date query integration
4 *
5 * @package elasticpress
6 * @since 1.3
7 */
8
9 namespace ElasticPress\Indexable\Post;
10
11 use WP_Date_Query;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 // @codeCoverageIgnoreStart
15 exit; // Exit if accessed directly.
16 // @codeCoverageIgnoreEnd
17 }
18
19 /**
20 * Date query class
21 */
22 class DateQuery extends WP_Date_Query {
23 /**
24 * Like WP_Date_Query::get_sql
25 * takes WP_Date_Query class queries and returns ES filter arrays
26 *
27 * @since 0.1.4
28 * @return array
29 */
30 public function get_es_filter() {
31 $filter = $this->get_es_filter_for_clauses();
32
33 return $filter;
34 }
35
36 /**
37 * Like WP_Date_Query::get_sql_for_clauses
38 * takes all queries in WP_Date_Query object and gets ES filters for each
39 *
40 * @since 0.1.4
41 * @return array
42 */
43 protected function get_es_filter_for_clauses() {
44 $filter = $this->get_es_filter_for_query( $this->queries );
45
46 return $filter;
47 }
48
49 /**
50 * Get Elaticsearch formatted filters
51 *
52 * @param array $query array of date query clauses.
53 * @param int $depth unused but may be necessary if we do nested date queries.
54 * @since 0.1.4
55 * @return array
56 */
57 protected function get_es_filter_for_query( $query, $depth = 0 ) {
58 $filter_chunks = array(
59 'filters' => [],
60 );
61
62 $filter_array = [];
63
64 foreach ( $query as $key => $clause ) {
65 if ( 'relation' === $key ) {
66 $relation = $query['relation'];
67 } elseif ( is_array( $clause ) ) {
68
69 // This is a first-order clause.
70 if ( $this->is_first_order_clause( $clause ) ) {
71
72 $clause_filter = $this->get_es_filter_for_clause( $clause, $query );
73
74 $filter_count = count( $clause_filter );
75 if ( $filter_count ) {
76 $filter_chunks['filters'][] = $clause_filter;
77 }
78
79 // This is a subquery, so we recurse.
80 }
81 }
82 }
83
84 if ( empty( $relation ) ) {
85 $relation = 'AND';
86 }
87
88 if ( 'AND' === $relation ) {
89 $filter_array['and'] = [];
90
91 $range_filters = [];
92 $term_filters = [];
93 foreach ( $filter_chunks['filters'] as $key => $filter ) {
94 $filter_type = key( $filter );
95 if ( 'date_terms' === $filter_type ) {
96 $term_filters[] = $filter['date_terms'];
97 } elseif ( 'range_filters' === $filter_type ) {
98 $range_filters[] = $filter['range_filters'];
99 }
100 }
101
102 if ( $range_filters ) {
103 $filter_array['and'] = array(
104 'bool' => $this->build_es_range_filter( $range_filters ),
105 );
106 }
107
108 if ( $term_filters ) {
109 $filter_array['and'] = array(
110 'bool' => $this->build_es_date_term_filter( $term_filters ),
111 );
112 }
113 } elseif ( 'OR' === $relation ) {
114 $should_clauses = [];
115 foreach ( $filter_chunks['filters'] as $filter ) {
116
117 $group = [];
118 if ( ! empty( $filter['range_filters'] ) ) {
119 $group[] = [ 'range' => $filter['range_filters'] ];
120 }
121
122 if ( ! empty( $filter['date_terms'] ) ) {
123 foreach ( $filter['date_terms']['must'] ?? [] as $must_clause ) {
124 $group[] = $must_clause;
125 }
126 }
127
128 if ( $group ) {
129 $should_clauses[] = [ 'bool' => [ 'must' => $group ] ];
130 }
131 }
132
133 if ( $should_clauses ) {
134 $filter_array['or'] = [
135 'bool' => [
136 'should' => $should_clauses,
137 ],
138 ];
139 }
140 }
141
142 return $filter_array;
143 }
144
145 /**
146 * Takes array of date term filters and groups them into a filter based on
147 * relationship type
148 *
149 * @param array $date_term_filters Date term filters.
150 * @param string $type type of relationship between date term filters (AND, OR).
151 *
152 * @since 0.1.4
153 * @return array
154 */
155 protected function build_es_date_term_filter( $date_term_filters = [], $type = 'AND' ) {
156 $date_term_filter_array = array(
157 'must' => [],
158 'should' => [],
159 'must_not' => [],
160 );
161
162 if ( 'AND' === $type ) {
163 foreach ( $date_term_filters as $date_term_filter ) {
164 $date_term_filter_array['must'] = ! empty( $date_term_filter['must'] ) ? array_merge( $date_term_filter_array['must'], $date_term_filter['must'] ) : [];
165 $date_term_filter_array['should'] = ! empty( $date_term_filter['should'] ) ? array_merge( $date_term_filter_array['should'], $date_term_filter['should'] ) : [];
166 $date_term_filter_array['must_not'] = ! empty( $date_term_filter['must_not'] ) ? array_merge( $date_term_filter_array['must_not'], $date_term_filter['must_not'] ) : [];
167 }
168 }
169
170 return array_filter( $date_term_filter_array );
171 }
172
173 /**
174 * Takes array of range filters and groups them into a single filter
175 *
176 * @param array $range_filters Range filters.
177 *
178 * @since 0.1.4
179 * @return array
180 */
181 protected function build_es_range_filter( $range_filters = [] ) {
182 $range_filter_array = array(
183 'must' => [],
184 'should' => [],
185 'must_not' => [],
186 );
187
188 foreach ( $range_filters as $key => $range_filter ) {
189 if ( 'not' === key( $range_filter ) ) {
190 $range_filter_array['must_not'][] = array(
191 'range' => $range_filter['not'],
192 );
193 } else {
194 $range_filter_array['must'][] = array(
195 'range' => $range_filter,
196 );
197 }
198 }
199
200 return array_filter( $range_filter_array );
201 }
202
203 /**
204 * Takes SQL query part, and translates it into an ES filter
205 *
206 * @param array $query SQL query piece.
207 * @return array ES filter.
208 */
209 protected function get_es_filter_for_clause( $query ) {
210
211 // The sub-parts of a $where part.
212 $filter_parts = [];
213
214 $column = ( ! empty( $query['column'] ) ) ? $query['column'] : $this->column;
215
216 $compare = $this->get_compare( $query );
217
218 $inclusive = ! empty( $query['inclusive'] ) && true === $query['inclusive'] ? true : false;
219
220 // Assign greater- and less-than values.
221 $lt = 'lt';
222 $gt = 'gt';
223
224 if ( $inclusive ) {
225 $lt .= 'e';
226 $gt .= 'e';
227 }
228
229 // Range queries.
230 if ( ! empty( $query['after'] ) ) {
231 $range_filters = array(
232 "{$column}" => array(
233 "{$gt}" => $this->build_mysql_datetime( $query['after'] ),
234 ),
235 );
236 }
237
238 if ( ! empty( $query['before'] ) ) {
239 $range_filters = empty( $range_filters[ $column ] ) ? array( "{$column}" => array( "{$lt}" => [] ) ) : $range_filters;
240 $range_filters[ $column ][ $lt ] = $this->build_mysql_datetime( $query['before'], $inclusive );
241 }
242
243 if ( ! empty( $query['after'] ) || ! empty( $query['before'] ) ) {
244 $filter_parts['range_filters'] = $range_filters;
245 }
246
247 // Specific value queries.
248 $date_parameters = array(
249 'year' => ! empty( $query['year'] ) ? $query['year'] : false,
250 'month' => ! empty( $query['month'] ) ? $query['month'] : false,
251 'week' => ! empty( $query['week'] ) ? $query['week'] : false,
252 'dayofyear' => ! empty( $query['dayofyear'] ) ? $query['dayofyear'] : false,
253 'day' => ! empty( $query['day'] ) ? $query['day'] : false,
254 'dayofweek' => ! empty( $query['dayofweek'] ) ? $query['dayofweek'] : false,
255 'dayofweek_iso' => ! empty( $query['dayofweek_iso'] ) ? $query['dayofweek_iso'] : false,
256 'hour' => ! empty( $query['hour'] ) ? $query['hour'] : false,
257 'minute' => ! empty( $query['minute'] ) ? $query['minute'] : false,
258 'second' => ! empty( $query['second'] ) ? $query['second'] : false,
259 'm' => ! empty( $query['m'] ) ? $query['m'] : false, // yearmonth
260 );
261
262 if ( empty( $date_parameters['month'] ) && ! empty( $query['monthnum'] ) ) {
263 $date_parameters['month'] = $query['monthnum'];
264 }
265
266 if ( empty( $date_parameters['week'] ) && ! empty( $query['w'] ) ) {
267 $date_parameters['week'] = $query['w'];
268 }
269
270 foreach ( $date_parameters as $param => $value ) {
271 if ( false === $value ) {
272 unset( $date_parameters[ $param ] );
273 }
274 }
275
276 if ( $date_parameters ) {
277
278 self::validate_date_values( $date_parameters );
279
280 $date_terms = array(
281 'must' => [],
282 'should' => [],
283 'must_not' => [],
284 );
285
286 foreach ( $date_parameters as $param => $value ) {
287 if ( '=' === $compare ) {
288 $date_terms['must'][]['term'][ "date_terms.{$param}" ] = $value;
289 } elseif ( '!=' === $compare ) {
290 $date_terms['must_not'][]['term'][ "date_terms.{$param}" ] = $value;
291 } elseif ( 'IN' === $compare ) {
292 foreach ( (array) $value as $in_value ) {
293 $date_terms['should'][]['term'][ "date_terms.{$param}" ] = $in_value;
294 }
295 } elseif ( 'NOT IN' === $compare ) {
296 foreach ( (array) $value as $in_value ) {
297 $date_terms['must_not'][]['term'][ "date_terms.{$param}" ] = $in_value;
298 }
299 } elseif ( 'BETWEEN' === $compare ) {
300 $range_filter[ "date_terms.{$param}" ] = [];
301 $range_filter[ "date_terms.{$param}" ]['gte'] = $value[0];
302 $range_filter[ "date_terms.{$param}" ]['lte'] = $value[1];
303 $filter_parts['range_filters'] = $range_filter;
304 } elseif ( 'NOT BETWEEN' === $compare ) {
305 $range_filter[ "date_terms.{$param}" ] = [];
306 $range_filter[ "date_terms.{$param}" ]['gt'] = $value[0];
307 $range_filter[ "date_terms.{$param}" ]['lt'] = $value[1];
308 $filter_parts['range_filters'] = array( 'not' => $range_filter );
309 } elseif ( strpos( $compare, '>' ) !== false ) {
310 $range = ( strpos( $compare, '=' ) !== false ) ? 'gte' : 'gt';
311 $range_filter[ "date_terms.{$param}" ] = [];
312 $range_filter[ "date_terms.{$param}" ][ $range ] = $value;
313 $filter_parts['range_filters'] = $range_filter;
314 } elseif ( strpos( $compare, '<' ) !== false ) {
315 $range = ( strpos( $compare, '=' ) !== false ) ? 'lte' : 'lt';
316 $range_filter[ "date_terms.{$param}" ] = [];
317 $range_filter[ "date_terms.{$param}" ][ $range ] = $value;
318 $filter_parts['range_filters'] = $range_filter;
319 }
320 }
321
322 $date_terms = array_filter( $date_terms );
323
324 if ( ! empty( $date_terms ) ) {
325 $filter_parts['date_terms'] = $date_terms;
326 }
327 }
328
329 return $filter_parts;
330 }
331
332 /**
333 * Takes WP_Query args, and returns ES filters for query
334 * Support for older style WP_Query date params
335 *
336 * @param array $args WP_Query args.
337 * @return array|bool
338 */
339 public static function simple_es_date_filter( $args ) {
340 $date_parameters = array(
341 'year' => ! empty( $args['year'] ) ? $args['year'] : false,
342 'month' => ! empty( $args['month'] ) ? $args['month'] : false,
343 'week' => ! empty( $args['week'] ) ? $args['week'] : false,
344 'day' => ! empty( $args['day'] ) ? $args['day'] : false,
345 'hour' => ! empty( $args['hour'] ) ? $args['hour'] : false,
346 'minute' => ! empty( $args['minute'] ) ? $args['minute'] : false,
347 'second' => ! empty( $args['second'] ) ? $args['second'] : false,
348 'm' => ! empty( $args['m'] ) ? $args['m'] : false, // yearmonth
349 );
350
351 if ( ! $date_parameters['month'] && ! empty( $args['monthnum'] ) ) {
352 $date_parameters['month'] = $args['monthnum'];
353 }
354
355 if ( ! $date_parameters['week'] && ! empty( $args['w'] ) ) {
356 $date_parameters['week'] = $args['w'];
357 }
358
359 foreach ( $date_parameters as $param => $value ) {
360 if ( false === $value ) {
361 unset( $date_parameters[ $param ] );
362 }
363 }
364
365 if ( ! $date_parameters ) {
366 return false;
367 }
368
369 $date_terms = [];
370 foreach ( $date_parameters as $param => $value ) {
371 $date_terms[]['term'][ "date_terms.{$param}" ] = $value;
372 }
373
374 return array(
375 'bool' => array(
376 'must' => $date_terms,
377 ),
378 );
379 }
380
381 /**
382 * Introduced in WP 4.1 added here for backwards compatibility
383 *
384 * @var array
385 */
386 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' );
387
388 /**
389 * Introduced in WP 4.1 added here for backwards compatibility
390 *
391 * @param array $query Date query array
392 * @return boolean
393 */
394 protected function is_first_order_clause( $query ) {
395 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) );
396 return ! empty( $time_keys );
397 }
398
399 /**
400 * Introduced in WP 4.1 added here for backwards compatibility
401 *
402 * @param array $date_query Date query array
403 * @return boolean
404 */
405 public function validate_date_values( $date_query = [] ) {
406 if ( empty( $date_query ) ) {
407 return false;
408 }
409
410 $valid = true;
411
412 /*
413 * Validate 'before' and 'after' up front, then let the
414 * validation routine continue to be sure that all invalid
415 * values generate errors too.
416 */
417 if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ) {
418 $valid = $this->validate_date_values( $date_query['before'] );
419 }
420
421 if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ) {
422 $valid = $this->validate_date_values( $date_query['after'] );
423 }
424
425 // Array containing all min-max checks.
426 $min_max_checks = [];
427
428 // Days per year.
429 if ( array_key_exists( 'year', $date_query ) ) {
430 /*
431 * If a year exists in the date query, we can use it to get the days.
432 * If multiple years are provided (as in a BETWEEN), use the first one.
433 */
434 if ( is_array( $date_query['year'] ) ) {
435 $_year = reset( $date_query['year'] );
436 } else {
437 $_year = $date_query['year'];
438 }
439
440 $max_days_of_year = date_i18n( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1;
441 } else {
442 // otherwise we use the max of 366 (leap-year).
443 $max_days_of_year = 366;
444 }
445
446 $min_max_checks['dayofyear'] = array(
447 'min' => 1,
448 'max' => $max_days_of_year,
449 );
450
451 // Days per week.
452 $min_max_checks['dayofweek'] = array(
453 'min' => 1,
454 'max' => 7,
455 );
456
457 // Days per week.
458 $min_max_checks['dayofweek_iso'] = array(
459 'min' => 1,
460 'max' => 7,
461 );
462
463 // Months per year.
464 $min_max_checks['month'] = array(
465 'min' => 1,
466 'max' => 12,
467 );
468
469 // Weeks per year.
470 if ( isset( $_year ) ) {
471 // If we have a specific year, use it to calculate number of weeks.
472 $date = new \DateTime();
473 $date->setISODate( $_year, 53 );
474 $week_count = $date->format( 'W' ) === '53' ? 53 : 52;
475
476 } else {
477 // Otherwise set the week-count to a maximum of 53.
478 $week_count = 53;
479 }
480
481 $min_max_checks['week'] = array(
482 'min' => 1,
483 'max' => $week_count,
484 );
485
486 // Days per month.
487 $min_max_checks['day'] = array(
488 'min' => 1,
489 'max' => 31,
490 );
491
492 // Hours per day.
493 $min_max_checks['hour'] = array(
494 'min' => 0,
495 'max' => 23,
496 );
497
498 // Minutes per hour.
499 $min_max_checks['minute'] = array(
500 'min' => 0,
501 'max' => 59,
502 );
503
504 // Seconds per minute.
505 $min_max_checks['second'] = array(
506 'min' => 0,
507 'max' => 59,
508 );
509
510 // Concatenate and throw a notice for each invalid value.
511 foreach ( $min_max_checks as $key => $check ) {
512 if ( ! array_key_exists( $key, $date_query ) ) {
513 continue;
514 }
515
516 // Throw a notice for each failing value.
517 $is_between = true;
518 foreach ( (array) $date_query[ $key ] as $_value ) {
519 $is_between = $_value >= $check['min'] && $_value <= $check['max'];
520
521 if ( ! is_numeric( $_value ) || ! $is_between ) {
522 $error = sprintf(
523 /* translators: Date query invalid date message: 1: invalid value, 2: type of value, 3: minimum valid value, 4: maximum valid value */
524 __( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ),
525 '<code>' . esc_html( $_value ) . '</code>',
526 '<code>' . esc_html( $key ) . '</code>',
527 '<code>' . esc_html( $check['min'] ) . '</code>',
528 '<code>' . esc_html( $check['max'] ) . '</code>'
529 );
530
531 _doing_it_wrong( __CLASS__, esc_html( $error ), '4.1.0' );
532
533 $valid = false;
534 }
535 }
536 }
537
538 // If we already have invalid date messages, don't bother running through checkdate().
539 if ( ! $valid ) {
540 return $valid;
541 }
542
543 $day_month_year_error_msg = '';
544
545 $day_exists = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] );
546 $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] );
547 $year_exists = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] );
548
549 if ( $day_exists && $month_exists && $year_exists ) {
550 // 1. Checking day, month, year combination.
551 if ( ! wp_checkdate( $date_query['month'], $date_query['day'], $date_query['year'], sprintf( '%s-%s-%s', $date_query['year'], $date_query['month'], $date_query['day'] ) ) ) {
552 /* translators: 1: year, 2: month, 3: day of month */
553 $day_month_year_error_msg = sprintf(
554 // translators: 1: Year, 2: Month, 3: Day
555 __( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ),
556 '<code>' . esc_html( $date_query['year'] ) . '</code>',
557 '<code>' . esc_html( $date_query['month'] ) . '</code>',
558 '<code>' . esc_html( $date_query['day'] ) . '</code>'
559 );
560
561 $valid = false;
562 }
563 } elseif ( $day_exists && $month_exists ) {
564 /*
565 * 2. checking day, month combination
566 * We use 2012 because, as a leap year, it's the most permissive.
567 */
568 if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) {
569 /* translators: 1: month, 2: day of month */
570 $err_msg = __( 'The following values do not describe a valid date: month %1$s, day %2$s.' );
571 $day_month_year_error_msg = sprintf(
572 $err_msg,
573 '<code>' . esc_html( $date_query['month'] ) . '</code>',
574 '<code>' . esc_html( $date_query['day'] ) . '</code>'
575 );
576
577 $valid = false;
578 }
579 }
580
581 if ( ! empty( $day_month_year_error_msg ) ) {
582 _doing_it_wrong( __CLASS__, esc_html( $day_month_year_error_msg ), '4.1.0' );
583 }
584
585 return $valid;
586 }
587 }
588