| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Class for generating SQL clauses that filter a primary query according to date. 4 * 5 * WP_Date_Query is a helper that allows primary query classes, such as WP_Query, to filter 6 * their results by date columns, by generating `WHERE` subclauses to be attached to the 7 * primary SQL query string. 8 * 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. 11 * See WP_Date_Query::validate_date_values(). 12 * 13 * @link https://developer.wordpress.org/reference/classes/wp_query/ 14 * 15 * @since 3.7.0 16 * 17 * @phpstan-type Date_Query_Compare '='|'!='|'>'|'>='|'<'|'<='|'IN'|'NOT IN'|'BETWEEN'|'NOT BETWEEN' 18 */ 19 #[AllowDynamicProperties] 20 class WP_Date_Query { 21 /** 22 * Array of date queries. 23 * 24 * See WP_Date_Query::__construct() for information on date query arguments. 25 * 26 * @since 3.7.0 27 * @var array 28 */ 29 public $queries = array(); 30 31 /** 32 * The default relation between top-level queries. Can be either 'AND' or 'OR'. 33 * 34 * @since 3.7.0 35 * @var string 36 */ 37 public $relation = 'AND'; 38 39 /** 40 * The column to query against. Can be changed via the query arguments. 41 * 42 * @since 3.7.0 43 * @var string 44 */ 45 public $column = 'post_date'; 46 47 /** 48 * The value comparison operator. Can be changed via the query arguments. 49 * 50 * @since 3.7.0 51 * @var string 52 * @phpstan-var Date_Query_Compare 53 */ 54 public $compare = '='; 55 56 /** 57 * Supported time-related parameter keys. 58 * 59 * @since 4.1.0 60 * @var string[] 61 */ 62 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' ); 63 64 /** 65 * Constructor. 66 * 67 * Time-related parameters that normally require integer values ('year', 'month', 'week', 'dayofyear', 'day', 68 * 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second') accept arrays of integers for some values of 69 * 'compare'. When 'compare' is 'IN' or 'NOT IN', arrays are accepted; when 'compare' is 'BETWEEN' or 'NOT 70 * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values. 71 * 72 * @since 3.7.0 73 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range. 74 * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter. 75 * 76 * @param array $date_query { 77 * Array of date query clauses. 78 * 79 * @type string $column Optional. The column to query against. If undefined, inherits the value of 80 * the `$default_column` parameter. See WP_Date_Query::validate_column() and 81 * the {@see 'date_query_valid_columns'} filter for the list of accepted values. 82 * Default 'post_date'. 83 * @type string $compare Optional. The comparison operator. Accepts '=', '!=', '>', '>=', '<', '<=', 84 * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Default '='. 85 * @type string $relation Optional. The boolean relationship between the date queries. Accepts 'OR' or 'AND'. 86 * Default 'OR'. 87 * @type array ...$0 { 88 * Optional. An array of first-order clause parameters, or another fully-formed date query. 89 * 90 * @type string|array $before { 91 * Optional. Date to retrieve posts before. Accepts `strtotime()`-compatible string, 92 * or array of 'year', 'month', 'day' values. 93 * 94 * @type string $year The four-digit year. Default empty. Accepts any four-digit year. 95 * @type string $month Optional when passing array. The month of the year. 96 * Default (string:empty)|(array:1). Accepts numbers 1-12. 97 * @type string $day Optional when passing array. The day of the month. 98 * Default (string:empty)|(array:1). Accepts numbers 1-31. 99 * } 100 * @type string|array $after { 101 * Optional. Date to retrieve posts after. Accepts `strtotime()`-compatible string, 102 * or array of 'year', 'month', 'day' values. 103 * 104 * @type string $year The four-digit year. Accepts any four-digit year. Default empty. 105 * @type string $month Optional when passing array. The month of the year. Accepts numbers 1-12. 106 * Default (string:empty)|(array:12). 107 * @type string $day Optional when passing array. The day of the month. Accepts numbers 1-31. 108 * Default (string:empty)|(array:last day of month). 109 * } 110 * @type string $column Optional. Used to add a clause comparing a column other than 111 * the column specified in the top-level `$column` parameter. 112 * See WP_Date_Query::validate_column() and 113 * the {@see 'date_query_valid_columns'} filter for the list 114 * of accepted values. Default is the value of top-level `$column`. 115 * @type string $compare Optional. The comparison operator. Accepts '=', '!=', '>', '>=', 116 * '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Comparisons 117 * support arrays in some time-related parameters. Default '='. 118 * @type bool $inclusive Optional. Include results from dates specified in 'before' or 119 * 'after'. Default false. 120 * @type int|int[] $year Optional. The four-digit year number. Accepts any four-digit year 121 * or an array of years if `$compare` supports it. Default empty. 122 * @type int|int[] $month Optional. The two-digit month number. Accepts numbers 1-12 or an 123 * array of valid numbers if `$compare` supports it. Default empty. 124 * @type int|int[] $week Optional. The week number of the year. Accepts numbers 1-53 or an 125 * array of valid numbers if `$compare` supports it. Default empty. 126 * @type int|int[] $dayofyear Optional. The day number of the year. Accepts numbers 1-366 or an 127 * array of valid numbers if `$compare` supports it. 128 * @type int|int[] $day Optional. The day of the month. Accepts numbers 1-31 or an array 129 * of valid numbers if `$compare` supports it. Default empty. 130 * @type int|int[] $dayofweek Optional. The day number of the week. Accepts numbers 1-7 (1 is 131 * Sunday) or an array of valid numbers if `$compare` supports it. 132 * Default empty. 133 * @type int|int[] $dayofweek_iso Optional. The day number of the week (ISO). Accepts numbers 1-7 134 * (1 is Monday) or an array of valid numbers if `$compare` supports it. 135 * Default empty. 136 * @type int|int[] $hour Optional. The hour of the day. Accepts numbers 0-23 or an array 137 * of valid numbers if `$compare` supports it. Default empty. 138 * @type int|int[] $minute Optional. The minute of the hour. Accepts numbers 0-59 or an array 139 * of valid numbers if `$compare` supports it. Default empty. 140 * @type int|int[] $second Optional. The second of the minute. Accepts numbers 0-59 or an 141 * array of valid numbers if `$compare` supports it. Default empty. 142 * } 143 * } 144 * @param string $default_column Optional. Default column to query against. See WP_Date_Query::validate_column() 145 * and the {@see 'date_query_valid_columns'} filter for the list of accepted values. 146 * Default 'post_date'. 147 */ 148 public function __construct( $date_query, $default_column = 'post_date' ) { 149 if ( empty( $date_query ) || ! is_array( $date_query ) ) { 150 return; 151 } 152 153 if ( isset( $date_query['relation'] ) ) { 154 $this->relation = $this->sanitize_relation( $date_query['relation'] ); 155 } else { 156 $this->relation = 'AND'; 157 } 158 159 // Support for passing time-based keys in the top level of the $date_query array. 160 if ( ! isset( $date_query[0] ) ) { 161 $date_query = array( $date_query ); 162 } 163 164 if ( ! empty( $date_query['column'] ) ) { 165 $date_query['column'] = esc_sql( $date_query['column'] ); 166 } else { 167 $date_query['column'] = esc_sql( $default_column ); 168 } 169 170 $this->column = $this->validate_column( $this->column ); 171 172 $this->compare = $this->get_compare( $date_query ); 173 174 $this->queries = $this->sanitize_query( $date_query ); 175 } 176 177 /** 178 * Recursive-friendly query sanitizer. 179 * 180 * Ensures that each query-level clause has a 'relation' key, and that 181 * each first-order clause contains all the necessary keys from `$defaults`. 182 * 183 * @since 4.1.0 184 * 185 * @param array $queries 186 * @param array $parent_query 187 * @return array Sanitized queries. 188 */ 189 public function sanitize_query( $queries, $parent_query = null ) { 190 $cleaned_query = array(); 191 192 $defaults = array( 193 'column' => 'post_date', 194 'compare' => '=', 195 'relation' => 'AND', 196 ); 197 198 // Numeric keys should always have array values. 199 foreach ( $queries as $qkey => $qvalue ) { 200 if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) { 201 unset( $queries[ $qkey ] ); 202 } 203 } 204 205 // Each query should have a value for each default key. Inherit from the parent when possible. 206 foreach ( $defaults as $dkey => $dvalue ) { 207 if ( isset( $queries[ $dkey ] ) ) { 208 continue; 209 } 210 211 $queries[ $dkey ] = $parent_query[ $dkey ] ?? $dvalue; 212 } 213 214 // Validate the dates passed in the query. 215 if ( $this->is_first_order_clause( $queries ) ) { 216 $this->validate_date_values( $queries ); 217 } 218 219 // Sanitize the relation parameter. 220 $queries['relation'] = $this->sanitize_relation( $queries['relation'] ); 221 222 foreach ( $queries as $key => $q ) { 223 if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) { 224 // This is a first-order query. Trust the values and sanitize when building SQL. 225 $cleaned_query[ $key ] = $q; 226 } else { 227 // Any array without a time key is another query, so we recurse. 228 $cleaned_query[] = $this->sanitize_query( $q, $queries ); 229 } 230 } 231 232 return $cleaned_query; 233 } 234 235 /** 236 * Determines whether this is a first-order clause. 237 * 238 * Checks to see if the current clause has any time-related keys. 239 * If so, it's first-order. 240 * 241 * @since 4.1.0 242 * 243 * @param array $query Query clause. 244 * @return bool True if this is a first-order clause. 245 */ 246 protected function is_first_order_clause( $query ) { 247 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); 248 return ! empty( $time_keys ); 249 } 250 251 /** 252 * Determines and validates what comparison operator to use. 253 * 254 * @since 3.7.0 255 * 256 * @param array $query A date query or a date subquery. 257 * @return string The comparison operator. 258 * 259 * @phpstan-return Date_Query_Compare 260 */ 261 public function get_compare( $query ) { 262 if ( ! empty( $query['compare'] ) 263 && in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) 264 ) { 265 return strtoupper( $query['compare'] ); 266 } 267 268 return $this->compare; 269 } 270 271 /** 272 * Validates the given date_query values and triggers errors if something is not valid. 273 * 274 * Note that date queries with invalid date ranges are allowed to 275 * continue (though of course no items will be found for impossible dates). 276 * This method only generates debug notices for these cases. 277 * 278 * @since 4.1.0 279 * 280 * @param array $date_query The date_query array. 281 * @return bool True if all values in the query are valid, false if one or more fail. 282 */ 283 public function validate_date_values( $date_query = array() ) { 284 if ( empty( $date_query ) ) { 285 return false; 286 } 287 288 $valid = true; 289 290 /* 291 * Validate 'before' and 'after' up front, then let the 292 * validation routine continue to be sure that all invalid 293 * values generate errors too. 294 */ 295 if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ) { 296 $valid = $this->validate_date_values( $date_query['before'] ); 297 } 298 299 if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ) { 300 $valid = $this->validate_date_values( $date_query['after'] ); 301 } 302 303 // Array containing all min-max checks. 304 $min_max_checks = array(); 305 306 // Days per year. 307 if ( array_key_exists( 'year', $date_query ) ) { 308 /* 309 * If a year exists in the date query, we can use it to get the days. 310 * If multiple years are provided (as in a BETWEEN), use the first one. 311 */ 312 if ( is_array( $date_query['year'] ) ) { 313 $_year = reset( $date_query['year'] ); 314 } else { 315 $_year = $date_query['year']; 316 } 317 318 $max_days_of_year = (int) gmdate( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1; 319 } else { 320 // Otherwise we use the max of 366 (leap-year). 321 $max_days_of_year = 366; 322 } 323 324 $min_max_checks['dayofyear'] = array( 325 'min' => 1, 326 'max' => $max_days_of_year, 327 ); 328 329 // Days per week. 330 $min_max_checks['dayofweek'] = array( 331 'min' => 1, 332 'max' => 7, 333 ); 334 335 // Days per week. 336 $min_max_checks['dayofweek_iso'] = array( 337 'min' => 1, 338 'max' => 7, 339 ); 340 341 // Months per year. 342 $min_max_checks['month'] = array( 343 'min' => 1, 344 'max' => 12, 345 ); 346 347 // Weeks per year. 348 if ( isset( $_year ) ) { 349 /* 350 * If we have a specific year, use it to calculate number of weeks. 351 * Note: the number of weeks in a year is the date in which Dec 28 appears. 352 */ 353 $week_count = gmdate( 'W', mktime( 0, 0, 0, 12, 28, $_year ) ); 354 355 } else { 356 // Otherwise set the week-count to a maximum of 53. 357 $week_count = 53; 358 } 359 360 $min_max_checks['week'] = array( 361 'min' => 1, 362 'max' => $week_count, 363 ); 364 365 // Days per month. 366 $min_max_checks['day'] = array( 367 'min' => 1, 368 'max' => 31, 369 ); 370 371 // Hours per day. 372 $min_max_checks['hour'] = array( 373 'min' => 0, 374 'max' => 23, 375 ); 376 377 // Minutes per hour. 378 $min_max_checks['minute'] = array( 379 'min' => 0, 380 'max' => 59, 381 ); 382 383 // Seconds per minute. 384 $min_max_checks['second'] = array( 385 'min' => 0, 386 'max' => 59, 387 ); 388 389 // Concatenate and throw a notice for each invalid value. 390 foreach ( $min_max_checks as $key => $check ) { 391 if ( ! array_key_exists( $key, $date_query ) ) { 392 continue; 393 } 394 395 // Throw a notice for each failing value. 396 foreach ( (array) $date_query[ $key ] as $_value ) { 397 $is_between = $_value >= $check['min'] && $_value <= $check['max']; 398 399 if ( ! is_numeric( $_value ) || ! $is_between ) { 400 $error = sprintf( 401 /* translators: Date query invalid date message. 1: Invalid value, 2: Type of value, 3: Minimum valid value, 4: Maximum valid value. */ 402 __( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ), 403 '<code>' . esc_html( $_value ) . '</code>', 404 '<code>' . esc_html( $key ) . '</code>', 405 '<code>' . esc_html( $check['min'] ) . '</code>', 406 '<code>' . esc_html( $check['max'] ) . '</code>' 407 ); 408 409 _doing_it_wrong( __CLASS__, $error, '4.1.0' ); 410 411 $valid = false; 412 } 413 } 414 } 415 416 // If we already have invalid date messages, don't bother running through checkdate(). 417 if ( ! $valid ) { 418 return $valid; 419 } 420 421 $day_month_year_error_msg = ''; 422 423 $day_exists = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] ); 424 $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] ); 425 $year_exists = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] ); 426 427 if ( $day_exists && $month_exists && $year_exists ) { 428 // 1. Checking day, month, year combination. 429 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'] ) ) ) { 430 $day_month_year_error_msg = sprintf( 431 /* translators: 1: Year, 2: Month, 3: Day of month. */ 432 __( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ), 433 '<code>' . esc_html( $date_query['year'] ) . '</code>', 434 '<code>' . esc_html( $date_query['month'] ) . '</code>', 435 '<code>' . esc_html( $date_query['day'] ) . '</code>' 436 ); 437 438 $valid = false; 439 } 440 } elseif ( $day_exists && $month_exists ) { 441 /* 442 * 2. checking day, month combination 443 * We use 2012 because, as a leap year, it's the most permissive. 444 */ 445 if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) { 446 $day_month_year_error_msg = sprintf( 447 /* translators: 1: Month, 2: Day of month. */ 448 __( 'The following values do not describe a valid date: month %1$s, day %2$s.' ), 449 '<code>' . esc_html( $date_query['month'] ) . '</code>', 450 '<code>' . esc_html( $date_query['day'] ) . '</code>' 451 ); 452 453 $valid = false; 454 } 455 } 456 457 if ( ! empty( $day_month_year_error_msg ) ) { 458 _doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' ); 459 } 460 461 return $valid; 462 } 463 464 /** 465 * Validates a column name parameter. 466 * 467 * Column names without a table prefix (like 'post_date') are checked against a list of 468 * allowed and known tables, and then, if found, have a table prefix (such as 'wp_posts.') 469 * prepended. Prefixed column names (such as 'wp_posts.post_date') bypass this allowed 470 * check, and are only sanitized to remove illegal characters. 471 * 472 * @since 3.7.0 473 * 474 * @global wpdb $wpdb WordPress database abstraction object. 475 * 476 * @param string $column The user-supplied column name. 477 * @return string A validated column name value. 478 */ 479 public function validate_column( $column ) { 480 global $wpdb; 481 482 $valid_columns = array( 483 'post_date', // Part of $wpdb->posts. 484 'post_date_gmt', // Part of $wpdb->posts. 485 'post_modified', // Part of $wpdb->posts. 486 'post_modified_gmt', // Part of $wpdb->posts. 487 'comment_date', // Part of $wpdb->comments. 488 'comment_date_gmt', // Part of $wpdb->comments. 489 'user_registered', // Part of $wpdb->users. 490 ); 491 492 if ( is_multisite() ) { 493 $valid_columns = array_merge( 494 $valid_columns, 495 array( 496 'registered', // Part of $wpdb->blogs. 497 'last_updated', // Part of $wpdb->blogs. 498 ) 499 ); 500 } 501 502 // Attempt to detect a table prefix. 503 if ( ! str_contains( $column, '.' ) ) { 504 /** 505 * Filters the list of valid date query columns. 506 * 507 * @since 3.7.0 508 * @since 4.1.0 Added 'user_registered' to the default recognized columns. 509 * @since 4.6.0 Added 'registered' and 'last_updated' to the default recognized columns. 510 * 511 * @param string[] $valid_columns An array of valid date query columns. Defaults 512 * are 'post_date', 'post_date_gmt', 'post_modified', 513 * 'post_modified_gmt', 'comment_date', 'comment_date_gmt', 514 * 'user_registered', 'registered', 'last_updated'. 515 */ 516 if ( ! in_array( $column, apply_filters( 'date_query_valid_columns', $valid_columns ), true ) ) { 517 $column = 'post_date'; 518 } 519 520 $known_columns = array( 521 $wpdb->posts => array( 522 'post_date', 523 'post_date_gmt', 524 'post_modified', 525 'post_modified_gmt', 526 ), 527 $wpdb->comments => array( 528 'comment_date', 529 'comment_date_gmt', 530 ), 531 $wpdb->users => array( 532 'user_registered', 533 ), 534 ); 535 536 if ( is_multisite() ) { 537 $known_columns[ $wpdb->blogs ] = array( 538 'registered', 539 'last_updated', 540 ); 541 } 542 543 // If it's a known column name, add the appropriate table prefix. 544 foreach ( $known_columns as $table_name => $table_columns ) { 545 if ( in_array( $column, $table_columns, true ) ) { 546 $column = $table_name . '.' . $column; 547 break; 548 } 549 } 550 } 551 552 // Remove unsafe characters. 553 return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column ); 554 } 555 556 /** 557 * Generates WHERE clause to be appended to a main query. 558 * 559 * @since 3.7.0 560 * 561 * @return string MySQL WHERE clause. 562 */ 563 public function get_sql() { 564 $sql = $this->get_sql_clauses(); 565 566 $where = $sql['where']; 567 568 /** 569 * Filters the date query WHERE clause. 570 * 571 * @since 3.7.0 572 * 573 * @param string $where WHERE clause of the date query. 574 * @param WP_Date_Query $query The WP_Date_Query instance. 575 */ 576 return apply_filters( 'get_date_sql', $where, $this ); 577 } 578 579 /** 580 * Generates SQL clauses to be appended to a main query. 581 * 582 * Called by the public WP_Date_Query::get_sql(), this method is abstracted 583 * out to maintain parity with the other Query classes. 584 * 585 * @since 4.1.0 586 * 587 * @return string[] { 588 * Array containing JOIN and WHERE SQL clauses to append to the main query. 589 * 590 * @type string $join SQL fragment to append to the main JOIN clause. 591 * @type string $where SQL fragment to append to the main WHERE clause. 592 * } 593 */ 594 protected function get_sql_clauses() { 595 $sql = $this->get_sql_for_query( $this->queries ); 596 597 if ( ! empty( $sql['where'] ) ) { 598 $sql['where'] = ' AND ' . $sql['where']; 599 } 600 601 return $sql; 602 } 603 604 /** 605 * Generates SQL clauses for a single query array. 606 * 607 * If nested subqueries are found, this method recurses the tree to 608 * produce the properly nested SQL. 609 * 610 * @since 4.1.0 611 * 612 * @param array $query Query to parse. 613 * @param int $depth Optional. Number of tree levels deep we currently are. 614 * Used to calculate indentation. Default 0. 615 * @return array { 616 * Array containing JOIN and WHERE SQL clauses to append to a single query array. 617 * 618 * @type string $join SQL fragment to append to the main JOIN clause. 619 * @type string $where SQL fragment to append to the main WHERE clause. 620 * } 621 */ 622 protected function get_sql_for_query( $query, $depth = 0 ) { 623 $sql_chunks = array( 624 'join' => array(), 625 'where' => array(), 626 ); 627 628 $sql = array( 629 'join' => '', 630 'where' => '', 631 ); 632 633 $indent = ''; 634 for ( $i = 0; $i < $depth; $i++ ) { 635 $indent .= ' '; 636 } 637 638 foreach ( $query as $key => $clause ) { 639 if ( 'relation' === $key ) { 640 $relation = $query['relation']; 641 } elseif ( is_array( $clause ) ) { 642 643 // This is a first-order clause. 644 if ( $this->is_first_order_clause( $clause ) ) { 645 $clause_sql = $this->get_sql_for_clause( $clause, $query ); 646 647 $where_count = count( $clause_sql['where'] ); 648 if ( ! $where_count ) { 649 $sql_chunks['where'][] = ''; 650 } elseif ( 1 === $where_count ) { 651 $sql_chunks['where'][] = $clause_sql['where'][0]; 652 } else { 653 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; 654 } 655 656 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); 657 // This is a subquery, so we recurse. 658 } else { 659 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); 660 661 $sql_chunks['where'][] = $clause_sql['where']; 662 $sql_chunks['join'][] = $clause_sql['join']; 663 } 664 } 665 } 666 667 // Filter to remove empties. 668 $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); 669 $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); 670 671 if ( empty( $relation ) ) { 672 $relation = 'AND'; 673 } 674 675 // Filter duplicate JOIN clauses and combine into a single string. 676 if ( ! empty( $sql_chunks['join'] ) ) { 677 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); 678 } 679 680 // Generate a single WHERE clause with proper brackets and indentation. 681 if ( ! empty( $sql_chunks['where'] ) ) { 682 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; 683 } 684 685 return $sql; 686 } 687 688 /** 689 * Turns a single date clause into pieces for a WHERE clause. 690 * 691 * A wrapper for get_sql_for_clause(), included here for backward 692 * compatibility while retaining the naming convention across Query classes. 693 * 694 * @since 3.7.0 695 * 696 * @param array $query Date query arguments. 697 * @return array { 698 * Array containing JOIN and WHERE SQL clauses to append to the main query. 699 * 700 * @type string[] $join Array of SQL fragments to append to the main JOIN clause. 701 * @type string[] $where Array of SQL fragments to append to the main WHERE clause. 702 * } 703 */ 704 protected function get_sql_for_subquery( $query ) { 705 return $this->get_sql_for_clause( $query, array() ); 706 } 707 708 /** 709 * Turns a first-order date query into SQL for a WHERE clause. 710 * 711 * @since 4.1.0 712 * 713 * @global wpdb $wpdb WordPress database abstraction object. 714 * 715 * @param array $query Date query clause. 716 * @param array $parent_query Parent query of the current date query. 717 * @return array { 718 * Array containing JOIN and WHERE SQL clauses to append to the main query. 719 * 720 * @type string[] $join Array of SQL fragments to append to the main JOIN clause. 721 * @type string[] $where Array of SQL fragments to append to the main WHERE clause. 722 * } 723 */ 724 protected function get_sql_for_clause( $query, $parent_query ) { 725 global $wpdb; 726 727 // The sub-parts of a $where part. 728 $where_parts = array(); 729 730 $column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column; 731 732 $column = $this->validate_column( $column ); 733 734 $compare = $this->get_compare( $query ); 735 736 $inclusive = ! empty( $query['inclusive'] ); 737 738 // Assign greater- and less-than values. 739 $lt = '<'; 740 $gt = '>'; 741 742 if ( $inclusive ) { 743 $lt .= '='; 744 $gt .= '='; 745 } 746 747 // Range queries. 748 if ( ! empty( $query['after'] ) ) { 749 $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); 750 } 751 if ( ! empty( $query['before'] ) ) { 752 $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); 753 } 754 // Specific value queries. 755 756 $date_units = array( 757 'YEAR' => array( 'year' ), 758 'MONTH' => array( 'month', 'monthnum' ), 759 '_wp_mysql_week' => array( 'week', 'w' ), 760 'DAYOFYEAR' => array( 'dayofyear' ), 761 'DAYOFMONTH' => array( 'day' ), 762 'DAYOFWEEK' => array( 'dayofweek' ), 763 'WEEKDAY' => array( 'dayofweek_iso' ), 764 ); 765 766 // Check of the possible date units and add them to the query. 767 foreach ( $date_units as $sql_part => $query_parts ) { 768 foreach ( $query_parts as $query_part ) { 769 if ( isset( $query[ $query_part ] ) ) { 770 $value = $this->build_value( $compare, $query[ $query_part ] ); 771 if ( $value ) { 772 switch ( $sql_part ) { 773 case '_wp_mysql_week': 774 $where_parts[] = _wp_mysql_week( $column ) . " $compare $value"; 775 break; 776 case 'WEEKDAY': 777 $where_parts[] = "$sql_part( $column ) + 1 $compare $value"; 778 break; 779 default: 780 $where_parts[] = "$sql_part( $column ) $compare $value"; 781 } 782 783 break; 784 } 785 } 786 } 787 } 788 789 if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) { 790 // Avoid notices. 791 foreach ( array( 'hour', 'minute', 'second' ) as $unit ) { 792 $query[ $unit ] ??= null; 793 } 794 795 $time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] ); 796 if ( $time_query ) { 797 $where_parts[] = $time_query; 798 } 799 } 800 801 /* 802 * Return an array of 'join' and 'where' for compatibility 803 * with other query classes. 804 */ 805 return array( 806 'where' => $where_parts, 807 'join' => array(), 808 ); 809 } 810 811 /** 812 * Builds and validates a value string based on the comparison operator. 813 * 814 * @since 3.7.0 815 * 816 * @param string $compare The compare operator to use. 817 * @param string|int|array<int|string>|null $value The value. 818 * @return string|false|int The value to be used in SQL or false on error. 819 * 820 * @phpstan-param Date_Query_Compare $compare 821 * @phpstan-param int|numeric-string|array<int|numeric-string>|null $value 822 */ 823 public function build_value( $compare, $value ) { 824 if ( ! isset( $value ) ) { 825 return false; 826 } 827 828 switch ( $compare ) { 829 case 'IN': 830 case 'NOT IN': 831 $value = (array) $value; 832 833 // Remove non-numeric values. 834 $value = array_filter( $value, 'is_numeric' ); 835 836 if ( empty( $value ) ) { 837 return false; 838 } 839 840 return '(' . implode( ',', array_map( 'intval', $value ) ) . ')'; 841 842 case 'BETWEEN': 843 case 'NOT BETWEEN': 844 if ( ! is_array( $value ) || 2 !== count( $value ) ) { 845 $value = array( $value, $value ); 846 } else { 847 $value = array_values( $value ); 848 } 849 850 // If either value is non-numeric, bail. 851 foreach ( $value as $v ) { 852 if ( ! is_numeric( $v ) ) { 853 return false; 854 } 855 } 856 857 $value = array_map( 'intval', $value ); 858 859 return $value[0] . ' AND ' . $value[1]; 860 861 default: 862 if ( ! is_numeric( $value ) ) { 863 return false; 864 } 865 866 return (int) $value; 867 } 868 } 869 870 /** 871 * Builds a MySQL format date/time based on some query parameters. 872 * 873 * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to 874 * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can 875 * pass a string that will be passed to date_create(). 876 * 877 * @since 3.7.0 878 * 879 * @param string|array $datetime An array of parameters or a strtotime() string. 880 * @param bool $default_to_max Whether to round up incomplete dates. Supported by values 881 * of $datetime that are arrays, or string values that are a 882 * subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i'). 883 * Default: false. 884 * @return string A MySQL format date/time. An unparseable string results in the Unix epoch 885 * (`1970-01-01 00:00:00`). 886 * 887 * @phpstan-param string|array{ 888 * year?: int<0, max>|numeric-string, 889 * month?: int<0, max>|numeric-string, 890 * day?: int<0, max>|numeric-string, 891 * hour?: int<0, max>|numeric-string, 892 * minute?: int<0, max>|numeric-string, 893 * second?: int<0, max>|numeric-string, 894 * } $datetime 895 */ 896 public function build_mysql_datetime( $datetime, $default_to_max = false ) { 897 if ( ! is_array( $datetime ) ) { 898 899 /* 900 * Try to parse some common date formats, so we can detect 901 * the level of precision and support the 'inclusive' parameter. 902 */ 903 if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) { 904 // Y 905 $datetime = array( 906 'year' => (int) $matches[1], 907 ); 908 909 } elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) { 910 // Y-m 911 $datetime = array( 912 'year' => (int) $matches[1], 913 'month' => (int) $matches[2], 914 ); 915 916 } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) { 917 // Y-m-d 918 $datetime = array( 919 'year' => (int) $matches[1], 920 'month' => (int) $matches[2], 921 'day' => (int) $matches[3], 922 ); 923 924 } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) { 925 // Y-m-d H:i 926 $datetime = array( 927 'year' => (int) $matches[1], 928 'month' => (int) $matches[2], 929 'day' => (int) $matches[3], 930 'hour' => (int) $matches[4], 931 'minute' => (int) $matches[5], 932 ); 933 } 934 935 // If no match is found, we don't support default_to_max. 936 if ( ! is_array( $datetime ) ) { 937 $wp_timezone = wp_timezone(); 938 939 // Assume local timezone if not provided. 940 $dt = date_create( $datetime, $wp_timezone ); 941 942 if ( false === $dt ) { 943 return gmdate( 'Y-m-d H:i:s', 0 ); 944 } 945 946 return $dt->setTimezone( $wp_timezone )->format( 'Y-m-d H:i:s' ); 947 } 948 } 949 950 $datetime = array_map( 'absint', $datetime ); 951 952 $datetime['year'] ??= (int) current_time( 'Y' ); 953 $datetime['month'] ??= ( $default_to_max ) ? 12 : 1; 954 $datetime['day'] ??= ( $default_to_max ) ? (int) gmdate( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1; 955 $datetime['hour'] ??= ( $default_to_max ) ? 23 : 0; 956 $datetime['minute'] ??= ( $default_to_max ) ? 59 : 0; 957 $datetime['second'] ??= ( $default_to_max ) ? 59 : 0; 958 959 return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] ); 960 } 961 962 /** 963 * Builds a query string for comparing time values (hour, minute, second). 964 * 965 * If just hour, minute, or second is set than a normal comparison will be done. 966 * However if multiple values are passed, a pseudo-decimal time will be created 967 * in order to be able to accurately compare against. 968 * 969 * @since 3.7.0 970 * 971 * @global wpdb $wpdb WordPress database abstraction object. 972 * 973 * @param string $column The column to query against. Needs to be pre-validated! 974 * @param string $compare The comparison operator. Needs to be pre-validated! 975 * @param int|null $hour Optional. An hour value (0-23). 976 * @param int|null $minute Optional. A minute value (0-59). 977 * @param int|null $second Optional. A second value (0-59). 978 * @return string|false A query part or false on failure. 979 * 980 * @phpstan-param Date_Query_Compare $compare 981 */ 982 public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) { 983 global $wpdb; 984 985 // Have to have at least one. 986 if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) { 987 return false; 988 } 989 990 // Complex combined queries aren't supported for multi-value queries. 991 if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) { 992 $return = array(); 993 994 $value = $this->build_value( $compare, $hour ); 995 if ( false !== $value ) { 996 $return[] = "HOUR( $column ) $compare $value"; 997 } 998 999 $value = $this->build_value( $compare, $minute ); 1000 if ( false !== $value ) { 1001 $return[] = "MINUTE( $column ) $compare $value"; 1002 } 1003 1004 $value = $this->build_value( $compare, $second ); 1005 if ( false !== $value ) { 1006 $return[] = "SECOND( $column ) $compare $value"; 1007 } 1008 1009 return implode( ' AND ', $return ); 1010 } 1011 1012 // Cases where just one unit is set. 1013 if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) { 1014 $value = $this->build_value( $compare, $hour ); 1015 if ( false !== $value ) { 1016 return "HOUR( $column ) $compare $value"; 1017 } 1018 } elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) ) { 1019 $value = $this->build_value( $compare, $minute ); 1020 if ( false !== $value ) { 1021 return "MINUTE( $column ) $compare $value"; 1022 } 1023 } elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) ) { 1024 $value = $this->build_value( $compare, $second ); 1025 if ( false !== $value ) { 1026 return "SECOND( $column ) $compare $value"; 1027 } 1028 } 1029 1030 // Single units were already handled. Since hour & second isn't allowed, minute must to be set. 1031 if ( ! isset( $minute ) ) { 1032 return false; 1033 } 1034 1035 $format = ''; 1036 $time = ''; 1037 1038 // Hour. 1039 if ( null !== $hour ) { 1040 $format .= '%H.'; 1041 $time .= sprintf( '%02d', $hour ) . '.'; 1042 } else { 1043 $format .= '0.'; 1044 $time .= '0.'; 1045 } 1046 1047 // Minute. 1048 $format .= '%i'; 1049 $time .= sprintf( '%02d', $minute ); 1050 1051 if ( isset( $second ) ) { 1052 $format .= '%s'; 1053 $time .= sprintf( '%02d', $second ); 1054 } 1055 1056 return $wpdb->prepare( "DATE_FORMAT( $column, %s ) $compare %f", $format, $time ); 1057 } 1058 1059 /** 1060 * Sanitizes a 'relation' operator. 1061 * 1062 * @since 6.0.3 1063 * 1064 * @param string $relation Raw relation key from the query argument. 1065 * @return string Sanitized relation. Either 'AND' or 'OR'. 1066 */ 1067 public function sanitize_relation( $relation ) { 1068 if ( 'OR' === strtoupper( $relation ) ) { 1069 return 'OR'; 1070 } else { 1071 return 'AND'; 1072 } 1073 } 1074 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Nov 14 08:20:01 2026 | Cross-referenced by PHPXref |