1 <?php |
1 <?php |
2 /** |
2 /** |
3 * Class for generating SQL clauses that filter a primary query according to date. |
3 * Class for generating SQL clauses that filter a primary query according to date. |
4 * |
4 * |
5 * `WP_Date_Query` is a helper that allows primary query classes, such as {@see WP_Query}, |
5 * WP_Date_Query is a helper that allows primary query classes, such as WP_Query, to filter |
6 * to filter their results by date columns, by generating `WHERE` subclauses to be attached |
6 * their results by date columns, by generating `WHERE` subclauses to be attached to the |
7 * to the primary SQL query string. |
7 * primary SQL query string. |
8 * |
8 * |
9 * Attempting to filter by an invalid date value (eg month=13) will generate SQL that will |
9 * Attempting to filter by an invalid date value (eg month=13) will generate SQL that will |
10 * return no results. In these cases, a _doing_it_wrong() error notice is also thrown. |
10 * return no results. In these cases, a _doing_it_wrong() error notice is also thrown. |
11 * See {@link WP_Date_Query::validate_date_values()}. |
11 * See WP_Date_Query::validate_date_values(). |
12 * |
12 * |
13 * @link https://codex.wordpress.org/Function_Reference/WP_Query Codex page. |
13 * @link https://codex.wordpress.org/Function_Reference/WP_Query Codex page. |
14 * |
14 * |
15 * @since 3.7.0 |
15 * @since 3.7.0 |
16 */ |
16 */ |
17 class WP_Date_Query { |
17 class WP_Date_Query { |
18 /** |
18 /** |
19 * Array of date queries. |
19 * Array of date queries. |
20 * |
20 * |
21 * See {@see WP_Date_Query::__construct()} for information on date query arguments. |
21 * See WP_Date_Query::__construct() for information on date query arguments. |
22 * |
22 * |
23 * @since 3.7.0 |
23 * @since 3.7.0 |
24 * @access public |
|
25 * @var array |
24 * @var array |
26 */ |
25 */ |
27 public $queries = array(); |
26 public $queries = array(); |
28 |
27 |
29 /** |
28 /** |
30 * The default relation between top-level queries. Can be either 'AND' or 'OR'. |
29 * The default relation between top-level queries. Can be either 'AND' or 'OR'. |
31 * |
30 * |
32 * @since 3.7.0 |
31 * @since 3.7.0 |
33 * @access public |
|
34 * @var string |
32 * @var string |
35 */ |
33 */ |
36 public $relation = 'AND'; |
34 public $relation = 'AND'; |
37 |
35 |
38 /** |
36 /** |
39 * The column to query against. Can be changed via the query arguments. |
37 * The column to query against. Can be changed via the query arguments. |
40 * |
38 * |
41 * @since 3.7.0 |
39 * @since 3.7.0 |
42 * @access public |
|
43 * @var string |
40 * @var string |
44 */ |
41 */ |
45 public $column = 'post_date'; |
42 public $column = 'post_date'; |
46 |
43 |
47 /** |
44 /** |
48 * The value comparison operator. Can be changed via the query arguments. |
45 * The value comparison operator. Can be changed via the query arguments. |
49 * |
46 * |
50 * @since 3.7.0 |
47 * @since 3.7.0 |
51 * @access public |
|
52 * @var array |
48 * @var array |
53 */ |
49 */ |
54 public $compare = '='; |
50 public $compare = '='; |
55 |
51 |
56 /** |
52 /** |
57 * Supported time-related parameter keys. |
53 * Supported time-related parameter keys. |
58 * |
54 * |
59 * @since 4.1.0 |
55 * @since 4.1.0 |
60 * @access public |
|
61 * @var array |
56 * @var array |
62 */ |
57 */ |
63 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' ); |
58 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' ); |
64 |
59 |
65 /** |
60 /** |
71 * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values. |
66 * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values. |
72 * |
67 * |
73 * @since 3.7.0 |
68 * @since 3.7.0 |
74 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range. |
69 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range. |
75 * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter. |
70 * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter. |
76 * @access public |
|
77 * |
71 * |
78 * @param array $date_query { |
72 * @param array $date_query { |
79 * Array of date query clauses. |
73 * Array of date query clauses. |
80 * |
74 * |
81 * @type array { |
75 * @type array { |
149 * @param array $default_column Optional. Default column to query against. Default 'post_date'. |
143 * @param array $default_column Optional. Default column to query against. Default 'post_date'. |
150 * Accepts 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt', |
144 * Accepts 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt', |
151 * 'comment_date', 'comment_date_gmt'. |
145 * 'comment_date', 'comment_date_gmt'. |
152 */ |
146 */ |
153 public function __construct( $date_query, $default_column = 'post_date' ) { |
147 public function __construct( $date_query, $default_column = 'post_date' ) { |
154 |
|
155 if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) { |
148 if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) { |
156 $this->relation = 'OR'; |
149 $this->relation = 'OR'; |
157 } else { |
150 } else { |
158 $this->relation = 'AND'; |
151 $this->relation = 'AND'; |
159 } |
152 } |
250 * Determine whether this is a first-order clause. |
242 * Determine whether this is a first-order clause. |
251 * |
243 * |
252 * Checks to see if the current clause has any time-related keys. |
244 * Checks to see if the current clause has any time-related keys. |
253 * If so, it's first-order. |
245 * If so, it's first-order. |
254 * |
246 * |
|
247 * @since 4.1.0 |
|
248 * |
255 * @param array $query Query clause. |
249 * @param array $query Query clause. |
256 * @return bool True if this is a first-order clause. |
250 * @return bool True if this is a first-order clause. |
257 */ |
251 */ |
258 protected function is_first_order_clause( $query ) { |
252 protected function is_first_order_clause( $query ) { |
259 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); |
253 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); |
282 * Note that date queries with invalid date ranges are allowed to |
275 * Note that date queries with invalid date ranges are allowed to |
283 * continue (though of course no items will be found for impossible dates). |
276 * continue (though of course no items will be found for impossible dates). |
284 * This method only generates debug notices for these cases. |
277 * This method only generates debug notices for these cases. |
285 * |
278 * |
286 * @since 4.1.0 |
279 * @since 4.1.0 |
287 * @access public |
|
288 * |
280 * |
289 * @param array $date_query The date_query array. |
281 * @param array $date_query The date_query array. |
290 * @return bool True if all values in the query are valid, false if one or more fail. |
282 * @return bool True if all values in the query are valid, false if one or more fail. |
291 */ |
283 */ |
292 public function validate_date_values( $date_query = array() ) { |
284 public function validate_date_values( $date_query = array() ) { |
353 'max' => 12 |
345 'max' => 12 |
354 ); |
346 ); |
355 |
347 |
356 // Weeks per year. |
348 // Weeks per year. |
357 if ( isset( $_year ) ) { |
349 if ( isset( $_year ) ) { |
358 // If we have a specific year, use it to calculate number of weeks. |
350 /* |
359 $date = new DateTime(); |
351 * If we have a specific year, use it to calculate number of weeks. |
360 $date->setISODate( $_year, 53 ); |
352 * Note: the number of weeks in a year is the date in which Dec 28 appears. |
361 $week_count = $date->format( "W" ) === "53" ? 53 : 52; |
353 */ |
|
354 $week_count = date( 'W', mktime( 0, 0, 0, 12, 28, $_year ) ); |
362 |
355 |
363 } else { |
356 } else { |
364 // Otherwise set the week-count to a maximum of 53. |
357 // Otherwise set the week-count to a maximum of 53. |
365 $week_count = 53; |
358 $week_count = 53; |
366 } |
359 } |
477 * known tables, and then, if found, have a table prefix (such as 'wp_posts.') prepended. |
470 * known tables, and then, if found, have a table prefix (such as 'wp_posts.') prepended. |
478 * Prefixed column names (such as 'wp_posts.post_date') bypass this whitelist check, |
471 * Prefixed column names (such as 'wp_posts.post_date') bypass this whitelist check, |
479 * and are only sanitized to remove illegal characters. |
472 * and are only sanitized to remove illegal characters. |
480 * |
473 * |
481 * @since 3.7.0 |
474 * @since 3.7.0 |
482 * @access public |
|
483 * |
475 * |
484 * @param string $column The user-supplied column name. |
476 * @param string $column The user-supplied column name. |
485 * @return string A validated column name value. |
477 * @return string A validated column name value. |
486 */ |
478 */ |
487 public function validate_column( $column ) { |
479 public function validate_column( $column ) { |
488 global $wpdb; |
480 global $wpdb; |
489 |
481 |
490 $valid_columns = array( |
482 $valid_columns = array( |
491 'post_date', 'post_date_gmt', 'post_modified', |
483 'post_date', 'post_date_gmt', 'post_modified', |
492 'post_modified_gmt', 'comment_date', 'comment_date_gmt', |
484 'post_modified_gmt', 'comment_date', 'comment_date_gmt', |
493 'user_registered', |
485 'user_registered', 'registered', 'last_updated', |
494 ); |
486 ); |
495 |
487 |
496 // Attempt to detect a table prefix. |
488 // Attempt to detect a table prefix. |
497 if ( false === strpos( $column, '.' ) ) { |
489 if ( false === strpos( $column, '.' ) ) { |
498 /** |
490 /** |
499 * Filter the list of valid date query columns. |
491 * Filters the list of valid date query columns. |
500 * |
492 * |
501 * @since 3.7.0 |
493 * @since 3.7.0 |
502 * @since 4.1.0 Added 'user_registered' to the default recognized columns. |
494 * @since 4.1.0 Added 'user_registered' to the default recognized columns. |
503 * |
495 * |
504 * @param array $valid_columns An array of valid date query columns. Defaults |
496 * @param array $valid_columns An array of valid date query columns. Defaults |
542 |
538 |
543 /** |
539 /** |
544 * Generate WHERE clause to be appended to a main query. |
540 * Generate WHERE clause to be appended to a main query. |
545 * |
541 * |
546 * @since 3.7.0 |
542 * @since 3.7.0 |
547 * @access public |
|
548 * |
543 * |
549 * @return string MySQL WHERE clause. |
544 * @return string MySQL WHERE clause. |
550 */ |
545 */ |
551 public function get_sql() { |
546 public function get_sql() { |
552 $sql = $this->get_sql_clauses(); |
547 $sql = $this->get_sql_clauses(); |
553 |
548 |
554 $where = $sql['where']; |
549 $where = $sql['where']; |
555 |
550 |
556 /** |
551 /** |
557 * Filter the date query WHERE clause. |
552 * Filters the date query WHERE clause. |
558 * |
553 * |
559 * @since 3.7.0 |
554 * @since 3.7.0 |
560 * |
555 * |
561 * @param string $where WHERE clause of the date query. |
556 * @param string $where WHERE clause of the date query. |
562 * @param WP_Date_Query $this The WP_Date_Query instance. |
557 * @param WP_Date_Query $this The WP_Date_Query instance. |
565 } |
560 } |
566 |
561 |
567 /** |
562 /** |
568 * Generate SQL clauses to be appended to a main query. |
563 * Generate SQL clauses to be appended to a main query. |
569 * |
564 * |
570 * Called by the public {@see WP_Date_Query::get_sql()}, this method |
565 * Called by the public WP_Date_Query::get_sql(), this method is abstracted |
571 * is abstracted out to maintain parity with the other Query classes. |
566 * out to maintain parity with the other Query classes. |
572 * |
567 * |
573 * @since 4.1.0 |
568 * @since 4.1.0 |
574 * @access protected |
|
575 * |
569 * |
576 * @return array { |
570 * @return array { |
577 * Array containing JOIN and WHERE SQL clauses to append to the main query. |
571 * Array containing JOIN and WHERE SQL clauses to append to the main query. |
578 * |
572 * |
579 * @type string $join SQL fragment to append to the main JOIN clause. |
573 * @type string $join SQL fragment to append to the main JOIN clause. |
733 $lt .= '='; |
724 $lt .= '='; |
734 $gt .= '='; |
725 $gt .= '='; |
735 } |
726 } |
736 |
727 |
737 // Range queries. |
728 // Range queries. |
738 if ( ! empty( $query['after'] ) ) |
729 if ( ! empty( $query['after'] ) ) { |
739 $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); |
730 $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); |
740 |
731 } |
741 if ( ! empty( $query['before'] ) ) |
732 if ( ! empty( $query['before'] ) ) { |
742 $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); |
733 $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); |
743 |
734 } |
744 // Specific value queries. |
735 // Specific value queries. |
745 |
736 |
746 if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) ) |
737 if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) ) |
747 $where_parts[] = "YEAR( $column ) $compare $value"; |
738 $where_parts[] = "YEAR( $column ) $compare $value"; |
748 |
739 |
850 /** |
840 /** |
851 * Builds a MySQL format date/time based on some query parameters. |
841 * Builds a MySQL format date/time based on some query parameters. |
852 * |
842 * |
853 * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to |
843 * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to |
854 * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can |
844 * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can |
855 * pass a string that that will be run through strtotime(). |
845 * pass a string that will be run through strtotime(). |
856 * |
846 * |
857 * @since 3.7.0 |
847 * @since 3.7.0 |
858 * @access public |
|
859 * |
848 * |
860 * @param string|array $datetime An array of parameters or a strotime() string |
849 * @param string|array $datetime An array of parameters or a strotime() string |
861 * @param bool $default_to_max Whether to round up incomplete dates. Supported by values |
850 * @param bool $default_to_max Whether to round up incomplete dates. Supported by values |
862 * of $datetime that are arrays, or string values that are a |
851 * of $datetime that are arrays, or string values that are a |
863 * subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i'). |
852 * subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i'). |
941 * If just hour, minute, or second is set than a normal comparison will be done. |
930 * If just hour, minute, or second is set than a normal comparison will be done. |
942 * However if multiple values are passed, a pseudo-decimal time will be created |
931 * However if multiple values are passed, a pseudo-decimal time will be created |
943 * in order to be able to accurately compare against. |
932 * in order to be able to accurately compare against. |
944 * |
933 * |
945 * @since 3.7.0 |
934 * @since 3.7.0 |
946 * @access public |
|
947 * |
935 * |
948 * @param string $column The column to query against. Needs to be pre-validated! |
936 * @param string $column The column to query against. Needs to be pre-validated! |
949 * @param string $compare The comparison operator. Needs to be pre-validated! |
937 * @param string $compare The comparison operator. Needs to be pre-validated! |
950 * @param int|null $hour Optional. An hour value (0-23). |
938 * @param int|null $hour Optional. An hour value (0-23). |
951 * @param int|null $minute Optional. A minute value (0-59). |
939 * @param int|null $minute Optional. A minute value (0-59). |