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