Ticket #29822: 29822.2.patch
File 29822.2.patch, 20.9 KB (added by , 10 years ago) |
---|
-
src/wp-includes/date.php
diff --git src/wp-includes/date.php src/wp-includes/date.php index 1cf4c6d..be57fac 100644
1 1 <?php 2 2 /** 3 * WP_Date_Query will generate a MySQL WHERE clause for the specified date-based parameters.3 * Class for generating SQL clauses that filter a primary query according to date. 4 4 * 5 * Initialize the class by passing an array of arrays of parameters. 5 * `WP_Date_Query` is a helper that allows primary query classes, such as {@see WP_Query}, 6 * to filter their results by date columns, by generating `WHERE` subclauses to be attached 7 * to the primary SQL query string. 6 8 * 7 9 * @link http://codex.wordpress.org/Function_Reference/WP_Query Codex page. 8 10 * … … 10 12 */ 11 13 class WP_Date_Query { 12 14 /** 13 * List of date queries. 15 * Array of date queries. 16 * 17 * See {@see WP_Date_Query::__construct()} for information on date query arguments. 14 18 * 15 19 * @since 3.7.0 16 20 * @access public … … class WP_Date_Query { 19 23 public $queries = array(); 20 24 21 25 /** 22 * The relation between the queries. Can be either 'AND' or 'OR' and can be changed via the query arguments.26 * The default relation between top-level queries. Can be either 'AND' or 'OR'. 23 27 * 24 28 * @since 3.7.0 25 29 * @access public … … class WP_Date_Query { 46 50 public $compare = '='; 47 51 48 52 /** 53 * Supported time-related parameter keys. 54 * 55 * @since 4.1.0 56 * @access public 57 * @var array 58 */ 59 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'hour', 'minute', 'second' ); 60 61 /** 49 62 * Constructor. 50 63 * 51 64 * @since 3.7.0 52 65 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range. 66 * @access public 53 67 * 54 68 * @param array $date_query { 55 * One or more associative arrays of date query parameters.69 * Array of date query clauses. 56 70 * 57 71 * @type array { 58 72 * @type string $column Optional. The column to query against. If undefined, inherits the value of … … class WP_Date_Query { 60 74 * 'post_date_gmt', 'post_modified','post_modified_gmt', 'comment_date', 61 75 * 'comment_date_gmt'. 62 76 * @type string $compare Optional. The comparison operator. 63 * Default '='. Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN',77 * Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', Default '='. 64 78 * 'BETWEEN', 'NOT BETWEEN'. 65 * @type string $relation Optional. The boolean relationship between the date quer yies.66 * Default 'OR'. Accepts 'OR', 'AND'.79 * @type string $relation Optional. The boolean relationship between the date queries. 80 * Accepts 'OR', 'AND'. Default 'OR'. 67 81 * @type array { 82 Optional. An array of first-order clause parameters, or another fully-formed date query. 68 83 * @type string|array $before Optional. Date to retrieve posts before. Accepts strtotime()-compatible 69 84 * string, or array of 'year', 'month', 'day' values. { 70 85 * … … class WP_Date_Query { 109 124 * 'comment_date', 'comment_date_gmt'. 110 125 */ 111 126 public function __construct( $date_query, $default_column = 'post_date' ) { 112 if ( empty( $date_query ) || ! is_array( $date_query ) )113 return;114 127 115 if ( isset( $date_query['relation'] ) && strtoupper( $date_query['relation'] ) == 'OR' )128 if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) { 116 129 $this->relation = 'OR'; 117 else130 } else { 118 131 $this->relation = 'AND'; 132 } 119 133 120 if ( ! empty( $date_query['column'] ) ) 121 $this->column = esc_sql( $date_query['column'] ); 122 else 123 $this->column = esc_sql( $default_column ); 134 if ( ! is_array( $date_query ) ) { 135 return; 136 } 137 138 // Support for passing time-based keys in the top level of the $date_query array. 139 if ( ! isset( $date_query[0] ) && ! empty( $date_query ) ) { 140 $date_query = array( $date_query ); 141 } 142 143 if ( empty( $date_query ) ) { 144 return; 145 } 146 147 if ( ! empty( $date_query['column'] ) ) { 148 $date_query['column'] = esc_sql( $date_query['column'] ); 149 } else { 150 $date_query['column'] = esc_sql( $default_column ); 151 } 124 152 125 153 $this->column = $this->validate_column( $this->column ); 126 154 127 155 $this->compare = $this->get_compare( $date_query ); 128 156 129 // If an array of arrays wasn't passed, fix it 130 if ( ! isset( $date_query[0] ) ) 131 $date_query = array( $date_query ); 157 $this->queries = $this->sanitize_query( $date_query ); 158 159 return; 160 } 161 162 /** 163 * Recursive-friendly query sanitizer. 164 * 165 * Ensures that each query-level clause has a 'relation' key, and that 166 * each first-order clause contains all the necessary keys from 167 * $defaults. 168 * 169 * @since 4.1.0 170 * @access public 171 * 172 * @param array $query A tax_query query clause. 173 * @return array Sanitized queries. 174 */ 175 public function sanitize_query( $queries, $parent_query = null ) { 176 $cleaned_query = array(); 177 178 $defaults = array( 179 'column' => 'post_date', 180 'compare' => '=', 181 'relation' => 'AND', 182 ); 132 183 133 $this->queries = array(); 134 foreach ( $date_query as $key => $query ) { 135 if ( ! is_array( $query ) ) 184 // Numeric keys should always have array values. 185 foreach ( $queries as $qkey => $qvalue ) { 186 if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) { 187 unset( $queries[ $qkey ] ); 188 } 189 } 190 191 // Each query should have a value for each default key. Inherit from the parent when possible. 192 foreach ( $defaults as $dkey => $dvalue ) { 193 if ( isset( $queries[ $dkey ] ) ) { 136 194 continue; 195 } 137 196 138 $this->queries[$key] = $query; 197 if ( isset( $parent_query[ $dkey ] ) ) { 198 $queries[ $dkey ] = $parent_query[ $dkey ]; 199 } else { 200 $queries[ $dkey ] = $dvalue; 201 } 139 202 } 203 204 foreach ( $queries as $key => $q ) { 205 if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) { 206 $cleaned_query[ $key ] = $q; 207 } else { 208 // Any array without a time key is another query, so we recurse. 209 $cleaned_query[] = $this->sanitize_query( $q, $queries ); 210 } 211 } 212 213 return $cleaned_query; 214 } 215 216 /** 217 * Determine whether this is a first-order clause. 218 * 219 * Essentially, this checks to see if the current clause has any time- 220 * related keys. If so, it's first-order. 221 * 222 * @param array $query Query clause. 223 * @return bool True if this is a first-order clause. 224 */ 225 protected function is_first_order_clause( $query ) { 226 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); 227 return ! empty( $time_keys ); 140 228 } 141 229 142 230 /** … … class WP_Date_Query { 145 233 * @since 3.7.0 146 234 * @access public 147 235 * 148 * @param array $query A date query or a date subquery 149 * @return string The comparison operator 236 * @param array $query A date query or a date subquery. 237 * @return string The comparison operator. 150 238 */ 151 239 public function get_compare( $query ) { 152 240 if ( ! empty( $query['compare'] ) && in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) … … class WP_Date_Query { 184 272 } 185 273 186 274 /** 187 * Turns an array of date query parameters into a MySQL string.275 * Generate WHERE clause to be appended to a main query. 188 276 * 189 277 * @since 3.7.0 190 278 * @access public 191 279 * 192 * @return string MySQL WHERE parameters280 * @return string MySQL WHERE clause. 193 281 */ 194 282 public function get_sql() { 195 // The parts of the final query 196 $where = array(); 197 198 foreach ( $this->queries as $key => $query ) { 199 $where_parts = $this->get_sql_for_subquery( $query ); 200 if ( $where_parts ) { 201 // Combine the parts of this subquery into a single string 202 $where[ $key ] = '( ' . implode( ' AND ', $where_parts ) . ' )'; 203 } 204 } 283 $sql = $this->get_sql_clauses(); 205 284 206 // Combine the subquery strings into a single string 207 if ( $where ) 208 $where = ' AND ( ' . implode( " {$this->relation} ", $where ) . ' )'; 209 else 210 $where = ''; 285 $where = $sql['where']; 211 286 212 287 /** 213 288 * Filter the date query WHERE clause. … … class WP_Date_Query { 221 296 } 222 297 223 298 /** 224 * Turns a single date subquery into pieces for a WHERE clause.299 * Generate SQL clauses to be appended to a main query. 225 300 * 226 * @since 3.7.0 227 * return array 301 * Called by the public {@see WP_Date_Query::get_sql()}, this method 302 * is abstracted out to maintain parity with the other Query classes. 303 * 304 * @since 4.1.0 305 * @access protected 306 * 307 * @return array { 308 * Array containing JOIN and WHERE SQL clauses to append to the main query. 309 * 310 * @type string $join SQL fragment to append to the main JOIN clause. 311 * @type string $where SQL fragment to append to the main WHERE clause. 312 * } 313 */ 314 protected function get_sql_clauses() { 315 $sql = $this->get_sql_for_query( $this->queries ); 316 317 if ( ! empty( $sql['where'] ) ) { 318 $sql['where'] = ' AND ' . $sql['where']; 319 } 320 321 return $sql; 322 } 323 324 /** 325 * Generate SQL clauses for a single query array. 326 * 327 * If nested subqueries are found, this method recurses the tree to 328 * produce the properly nested SQL. 329 * 330 * @since 4.1.0 331 * @access protected 332 * 333 * @param array $query Query to parse. 334 * @param int $depth Optional. Number of tree levels deep we currently are. 335 * Used to calculate indentation. 336 * @return array { 337 * Array containing JOIN and WHERE SQL clauses to append to a single query array. 338 * 339 * @type string $join SQL fragment to append to the main JOIN clause. 340 * @type string $where SQL fragment to append to the main WHERE clause. 341 * } 342 */ 343 protected function get_sql_for_query( $query, $depth = 0 ) { 344 $sql_chunks = array( 345 'join' => array(), 346 'where' => array(), 347 ); 348 349 $sql = array( 350 'join' => '', 351 'where' => '', 352 ); 353 354 $indent = ''; 355 for ( $i = 0; $i < $depth; $i++ ) { 356 $indent .= " "; 357 } 358 359 foreach ( $query as $key => $clause ) { 360 if ( 'relation' === $key ) { 361 $relation = $query['relation']; 362 } else if ( is_array( $clause ) ) { 363 364 // This is a first-order clause. 365 if ( $this->is_first_order_clause( $clause ) ) { 366 $clause_sql = $this->get_sql_for_clause( $clause, $query ); 367 368 $where_count = count( $clause_sql['where'] ); 369 if ( ! $where_count ) { 370 $sql_chunks['where'][] = ''; 371 } else if ( 1 === $where_count ) { 372 $sql_chunks['where'][] = $clause_sql['where'][0]; 373 } else { 374 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; 375 } 376 377 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); 378 // This is a subquery, so we recurse. 379 } else { 380 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); 381 382 $sql_chunks['where'][] = $clause_sql['where']; 383 $sql_chunks['join'][] = $clause_sql['join']; 384 } 385 } 386 } 387 388 // Filter to remove empties. 389 $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); 390 $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); 391 392 if ( empty( $relation ) ) { 393 $relation = 'AND'; 394 } 395 396 // Filter duplicate JOIN clauses and combine into a single string. 397 if ( ! empty( $sql_chunks['join'] ) ) { 398 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); 399 } 400 401 // Generate a single WHERE clause with proper brackets and indentation. 402 if ( ! empty( $sql_chunks['where'] ) ) { 403 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; 404 } 405 406 return $sql; 407 } 408 409 /** 410 * Turns a single date clause into pieces for a WHERE clause. 411 * 412 * A wrapper for get_sql_for_clause(), included here for backward 413 * compatibility while retaining the naming convention across Query classes. 414 * 415 * @since 3.7.0 416 * @access protected 417 * 418 * @param array $query Date query arguments. 419 * @return array { 420 * Array containing JOIN and WHERE SQL clauses to append to the main query. 421 * 422 * @type string $join SQL fragment to append to the main JOIN clause. 423 * @type string $where SQL fragment to append to the main WHERE clause. 424 * } 228 425 */ 229 426 protected function get_sql_for_subquery( $query ) { 427 return $this->get_sql_for_clause( $query, '' ); 428 } 429 430 /** 431 * Turns a first-order date query into SQL for a WHERE clause. 432 * 433 * @since 4.1.0 434 * @access protected 435 * 436 * @param array $query Date query clause. 437 * @param array $parent_query Parent query of the current date query. 438 * @return array { 439 * Array containing JOIN and WHERE SQL clauses to append to the main query. 440 * 441 * @type string $join SQL fragment to append to the main JOIN clause. 442 * @type string $where SQL fragment to append to the main WHERE clause. 443 * } 444 */ 445 protected function get_sql_for_clause( $query, $parent_query ) { 230 446 global $wpdb; 231 447 232 // The sub-parts of a $where part 448 // The sub-parts of a $where part. 233 449 $where_parts = array(); 234 450 235 451 $column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column; … … class WP_Date_Query { 249 465 $gt .= '='; 250 466 } 251 467 252 // Range queries 468 // Range queries. 253 469 if ( ! empty( $query['after'] ) ) 254 470 $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); 255 471 256 472 if ( ! empty( $query['before'] ) ) 257 473 $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); 258 474 259 // Specific value queries 475 // Specific value queries. 260 476 261 477 if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) ) 262 478 $where_parts[] = "YEAR( $column ) $compare $value"; … … class WP_Date_Query { 281 497 $where_parts[] = "DAYOFWEEK( $column ) $compare $value"; 282 498 283 499 if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) { 284 // Avoid notices 500 // Avoid notices. 285 501 foreach ( array( 'hour', 'minute', 'second' ) as $unit ) { 286 if ( ! isset( $query[ $unit] ) ) {287 $query[ $unit] = null;502 if ( ! isset( $query[ $unit ] ) ) { 503 $query[ $unit ] = null; 288 504 } 289 505 } 290 506 … … class WP_Date_Query { 293 509 } 294 510 } 295 511 296 return $where_parts; 512 /* 513 * Return an array of 'join' and 'where' for compatibility 514 * with other query classes. 515 */ 516 return array( 517 'where' => $where_parts, 518 'join' => array(), 519 ); 297 520 } 298 521 299 522 /** -
tests/phpunit/tests/date/query.php
diff --git tests/phpunit/tests/date/query.php tests/phpunit/tests/date/query.php index e9dd191..e87e917 100644
class Tests_WP_Date_Query extends WP_UnitTestCase { 55 55 'year' => 2008, 56 56 'month' => 6, 57 57 ), 58 'column' => 'post_date', 59 'compare' => '=', 60 'relation' => 'AND', 58 61 ), 62 'column' => 'post_date', 63 'compare' => '=', 64 'relation' => 'AND', 59 65 ); 60 66 61 67 $this->assertSame( $expected, $q->queries ); … … class Tests_WP_Date_Query extends WP_UnitTestCase { 73 79 ), 74 80 ) ); 75 81 76 // Note: WP_Date_Query does not reset indexes77 82 $expected = array( 78 2 =>array(83 array( 79 84 'before' => array( 80 85 'year' => 2008, 81 86 'month' => 6, 82 87 ), 88 'column' => 'post_date', 89 'compare' => '=', 90 'relation' => 'AND', 83 91 ), 92 'column' => 'post_date', 93 'compare' => '=', 94 'relation' => 'AND', 84 95 ); 85 96 86 $this->assert Same( $expected, $q->queries );97 $this->assertEquals( $expected, $q->queries ); 87 98 } 88 99 89 100 public function test_get_compare_empty() { -
tests/phpunit/tests/query/dateQuery.php
diff --git tests/phpunit/tests/query/dateQuery.php tests/phpunit/tests/query/dateQuery.php index 759a40a..9654c83 100644
class Tests_Query_DateQuery extends WP_UnitTestCase { 629 629 630 630 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 631 631 632 $this->assertContains( "AND ( ( MONTH( post_date ) = 5 ) ) AND", $this->q->request ); 633 634 $this->assertNotContains( "AND ( ( MONTH( post_date ) = 5 AND MONTH( post_date ) = 9 ) ) AND", $this->q->request ); 632 $this->assertContains( "MONTH( post_date ) = 5", $this->q->request ); 633 $this->assertNotContains( "MONTH( post_date ) = 9", $this->q->request ); 635 634 } 636 635 637 636 public function test_date_params_week_w_duplicate() { … … class Tests_Query_DateQuery extends WP_UnitTestCase { 653 652 654 653 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 655 654 656 $this->assertContains( "AND ( ( WEEK( post_date, 1 ) = 21 ) ) AND", $this->q->request ); 655 $this->assertContains( "WEEK( post_date, 1 ) = 21", $this->q->request ); 656 $this->assertNotContains( "WEEK( post_date, 1 ) = 22", $this->q->request ); 657 } 658 659 /** 660 * @ticket 29822 661 */ 662 public function test_date_query_one_nested_query() { 663 $this->create_posts(); 664 665 $posts = $this->_get_query_result( array( 666 'date_query' => array( 667 'relation' => 'OR', 668 array( 669 'relation' => 'AND', 670 array( 671 'year' => 2004, 672 ), 673 array( 674 'month' => 1, 675 ), 676 ), 677 array( 678 'year' => 1984, 679 ), 680 ), 681 ) ); 682 683 $expected_dates = array( 684 '1984-07-28 19:28:56', 685 '2004-01-03 08:54:10', 686 ); 687 688 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 689 } 690 691 /** 692 * @ticket 29822 693 */ 694 public function test_date_query_one_nested_query_multiple_columns_relation_and() { 695 $p1 = $this->factory->post->create( array( 696 'post_date' => '2012-03-05 15:30:55', 697 ) ); 698 $this->update_post_modified( $p1, '2014-11-03 14:43:00' ); 699 700 $p2 = $this->factory->post->create( array( 701 'post_date' => '2012-05-05 15:30:55', 702 ) ); 703 $this->update_post_modified( $p2, '2014-10-03 14:43:00' ); 704 705 $p3 = $this->factory->post->create( array( 706 'post_date' => '2013-05-05 15:30:55', 707 ) ); 708 $this->update_post_modified( $p3, '2014-10-03 14:43:00' ); 709 710 $p4 = $this->factory->post->create( array( 711 'post_date' => '2012-02-05 15:30:55', 712 ) ); 713 $this->update_post_modified( $p4, '2012-12-03 14:43:00' ); 714 715 $q = new WP_Query( array( 716 'date_query' => array( 717 'relation' => 'AND', 718 array( 719 'column' => 'post_date', 720 array( 721 'year' => 2012, 722 ), 723 ), 724 array( 725 'column' => 'post_modified', 726 array( 727 'year' => 2014, 728 ), 729 ), 730 ), 731 'fields' => 'ids', 732 'update_post_meta_cache' => false, 733 'update_post_term_cache' => false, 734 'post_status' => 'publish', 735 ) ); 736 737 $expected = array( $p1, $p2, ); 657 738 658 $this->assertNotContains( "AND ( ( WEEK( post_date, 1 ) = 21 AND WEEK( post_date, 1 ) = 22 ) ) AND", $this->q->request ); 739 $this->assertEqualSets( $expected, $q->posts ); 740 } 741 742 /** 743 * @ticket 29822 744 */ 745 public function test_date_query_nested_query_multiple_columns_mixed_relations() { 746 $p1 = $this->factory->post->create( array( 747 'post_date' => '2012-03-05 15:30:55', 748 ) ); 749 $this->update_post_modified( $p1, '2014-11-03 14:43:00' ); 750 751 $p2 = $this->factory->post->create( array( 752 'post_date' => '2012-05-05 15:30:55', 753 ) ); 754 $this->update_post_modified( $p2, '2014-10-03 14:43:00' ); 755 756 $p3 = $this->factory->post->create( array( 757 'post_date' => '2013-05-05 15:30:55', 758 ) ); 759 $this->update_post_modified( $p3, '2014-10-03 14:43:00' ); 760 761 $p4 = $this->factory->post->create( array( 762 'post_date' => '2012-02-05 15:30:55', 763 ) ); 764 $this->update_post_modified( $p4, '2012-12-03 14:43:00' ); 765 766 $p5 = $this->factory->post->create( array( 767 'post_date' => '2014-02-05 15:30:55', 768 ) ); 769 $this->update_post_modified( $p5, '2013-12-03 14:43:00' ); 770 771 $q = new WP_Query( array( 772 'date_query' => array( 773 'relation' => 'OR', 774 array( 775 'relation' => 'AND', 776 array( 777 'column' => 'post_date', 778 array( 779 'day' => 05, 780 ), 781 ), 782 array( 783 'column' => 'post_date', 784 array( 785 'before' => array( 786 'year' => 2012, 787 'month' => 4, 788 ), 789 ), 790 ), 791 ), 792 array( 793 'column' => 'post_modified', 794 array( 795 'month' => 12, 796 ), 797 ), 798 ), 799 'fields' => 'ids', 800 'update_post_meta_cache' => false, 801 'update_post_term_cache' => false, 802 'post_status' => 'publish', 803 ) ); 804 805 $expected = array( $p1, $p4, $p5, ); 806 $this->assertEqualSets( $expected, $q->posts ); 659 807 } 660 808 661 809 /** Helpers **********************************************************/ … … class Tests_Query_DateQuery extends WP_UnitTestCase { 692 840 $this->factory->post->create( array( 'post_date' => $post_date ) ); 693 841 } 694 842 } 843 844 /** 845 * There's no way to change post_modified through the API. 846 */ 847 protected function update_post_modified( $post_id, $date ) { 848 global $wpdb; 849 return $wpdb->update( 850 $wpdb->posts, 851 array( 852 'post_modified' => $date, 853 'post_modified_gmt' => $date, 854 ), 855 array( 856 'ID' => $post_id, 857 ), 858 array( 859 '%s', 860 '%s', 861 ), 862 array( 863 '%d', 864 ) 865 ); 866 } 695 867 }