PluginProbe
ElasticPress / 4.7.2
ElasticPress v4.7.2
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 4.7.2, at includes/classes/Indexable/Post/DateQuery.php

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