WordPress.org

Make WordPress Core

Changeset 29925


Ignore:
Timestamp:
10/16/2014 08:11:13 PM (6 years ago)
Author:
boonebgorges
Message:

Throw notices when invalid date values are passed to WP_Date_Query.

_doing_it_wrong() notices are now generated when passing out-of-range values
(month=13) or invalid dates (2014-02-29).

Includes unit tests.

Props ChriCo.
Fixes #25834.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/date.php

    r29923 r29925  
    66 * to filter their results by date columns, by generating `WHERE` subclauses to be attached
    77 * to the 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 {@link WP_Date_Query::validate_date_values()}.
    812 *
    913 * @link http://codex.wordpress.org/Function_Reference/WP_Query Codex page.
     
    202206        }
    203207
     208        // Validate the dates passed in the query.
     209        if ( $this->is_first_order_clause( $queries ) ) {
     210            $this->validate_date_values( $queries );
     211        }
     212
    204213        foreach ( $queries as $key => $q ) {
    205214            if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) {
     
    243252
    244253        return $this->compare;
     254    }
     255
     256    /**
     257     * Validates the given date_query values and triggers errors if something is not valid.
     258     *
     259     * Note that date queries with invalid date ranges are allowed to
     260     * continue (though of course no items will be found for impossible dates).
     261     * This method only generates debug notices for these cases.
     262     *
     263     * @since  4.1.0
     264     * @access public
     265     *
     266     * @param  array $date_query The date_query array.
     267     * @return bool  True if all values in the query are valid, false if one or more fail.
     268     */
     269    public function validate_date_values( $date_query = array() ) {
     270        if ( empty( $date_query ) ) {
     271            return false;
     272        }
     273
     274        $valid = true;
     275
     276        /*
     277         * Validate 'before' and 'after' up front, then let the
     278         * validation routine continue to be sure that all invalid
     279         * values generate errors too.
     280         */
     281        if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ){
     282            $valid = $this->validate_date_values( $date_query['before'] );
     283        }
     284
     285        if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ){
     286            $valid = $this->validate_date_values( $date_query['after'] );
     287        }
     288
     289        // Message template for the min-max-check.
     290        /* translators: Date query invalid date message: 1: invalid value, 2: type of value, 3: minimum valid value, 4: maximum valid value */
     291        $min_max_msg = __( 'Invalid value <code>%1$s</code> for <strong>%2$s</strong>. Excepted value should between <code>%3$d</code> and </code>%4$d</code>.' );
     292
     293        // Array containing all min-max checks.
     294        $min_max_checks = array();
     295
     296        // Days per year.
     297        if ( array_key_exists( 'year', $date_query ) ) {
     298            // If a year exists in the date query, we can use it to get the days.
     299            $max_days_of_year = date( 'z', mktime( 0, 0, 0, 12, 31, $date_query['year'] ) ) + 1;
     300        } else {
     301            // otherwise we use the max of 366 (leap-year)
     302            $max_days_of_year = 366;
     303        }
     304
     305        $min_max_checks['dayofyear'] = array(
     306            'min' => 1,
     307            'max' => $max_days_of_year
     308        );
     309
     310        // Days per week.
     311        $min_max_checks['dayofweek'] = array(
     312            'min' => 1,
     313            'max' => 7
     314        );
     315
     316        // Months per year.
     317        $min_max_checks['month'] = array(
     318            'min' => 1,
     319            'max' => 12
     320        );
     321
     322        // Weeks per year.
     323        if ( array_key_exists( 'year', $date_query ) ) {
     324            // If we have a specific year, use it to calculate number of weeks.
     325            $date = new DateTime();
     326            $date->setISODate( $date_query['year'], 53 );
     327            $week_count = $date->format( "W" ) === "53" ? 53 : 52;
     328
     329        } else {
     330            // Otherwise set the week-count to a maximum of 53.
     331            $week_count = 53;
     332        }
     333
     334        $min_max_checks['week'] = array(
     335            'min' => 1,
     336            'max' => $week_count
     337        );
     338
     339        // Days per month.
     340        $min_max_checks['day'] = array(
     341            'min' => 1,
     342            'max' => 31
     343        );
     344
     345        // Hours per day.
     346        $min_max_checks['hour'] = array(
     347            'min' => 1,
     348            'max' => 23
     349        );
     350
     351        // Minutes per hour.
     352        $min_max_checks['minute'] = array(
     353            'min' => 0,
     354            'max' => 59
     355        );
     356
     357        // Seconds per minute.
     358        $min_max_checks['second'] = array(
     359            'min' => 0,
     360            'max' => 59
     361        );
     362
     363        // Concatenate and throw a notice for each invalid value.
     364        foreach ( $min_max_checks as $key => $check ) {
     365            if ( ! array_key_exists( $key, $date_query ) ) {
     366                continue;
     367            }
     368
     369            $is_between = $date_query[ $key ] >= $check['min'] && $date_query[ $key ] <= $check['max'];
     370
     371            if ( ! $is_between ) {
     372                $error = sprintf(
     373                    $min_max_msg,
     374                    esc_html( $date_query[ $key ] ),
     375                    $key,
     376                    $check['min'],
     377                    $check['max']
     378                );
     379
     380                _doing_it_wrong( __CLASS__, $error, '4.1.0' );
     381
     382                $valid = false;
     383            }
     384        }
     385
     386        // If we already have invalid date messages, don't bother running through checkdate().
     387        if ( ! $valid ) {
     388            return $valid;
     389        }
     390
     391        $day_month_year_error_msg = '';
     392
     393        $day_exists   = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] );
     394        $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] );
     395        $year_exists  = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] );
     396
     397        if ( $day_exists && $month_exists && $year_exists ) {
     398            // 1. Checking day, month, year combination.
     399            if ( ! checkdate( $date_query['month'], $date_query['day'], $date_query['year'] ) ) {
     400                /* translators: 1: year, 2: month, 3: day of month */
     401                $day_month_year_error_msg = sprintf(
     402                    __( 'The following values do not describe a valid date: year <code>%1$s</code>, month <code>%2$s</code>, day <code>%3$s</code>.' ),
     403                    esc_html( $date_query['year'] ),
     404                    esc_html( $date_query['month'] ),
     405                    esc_html( $date_query['day'] )
     406                );
     407
     408                $valid = false;
     409            }
     410
     411        } else if ( $day_exists && $month_exists ) {
     412            /*
     413             * 2. checking day, month combination
     414             * We use 2012 because, as a leap year, it's the most permissive.
     415             */
     416            if ( ! checkdate( $date_query['month'], $date_query['day'], 2012 ) ) {
     417                /* translators: 1: month, 2: day of month */
     418                $day_month_year_error_msg = sprintf(
     419                    __( 'The following values do not describe a valid date: month <code>%1$d</code>, day <code>%2$d</code>.' ),
     420                    esc_html( $date_query['month'] ),
     421                    esc_html( $date_query['day'] )
     422                );
     423
     424                $valid = false;
     425            }
     426        }
     427
     428        if ( ! empty( $day_month_year_error_msg ) ) {
     429            _doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' );
     430        }
     431
     432        return $valid;
    245433    }
    246434
  • trunk/tests/phpunit/tests/date/query.php

    r29923 r29925  
    1010 */
    1111class Tests_WP_Date_Query extends WP_UnitTestCase {
     12    public $q;
     13
     14    public function setUp() {
     15        parent::setUp();
     16        unset( $this->q );
     17        $this->q = new WP_Date_Query( array( 'm' => 2 ) );
     18    }
     19
    1220    public function test_construct_date_query_empty() {
    1321        $q = new WP_Date_Query( array() );
     
    583591        $this->assertRegExp( "/DATE_FORMAT\( post_date, '0\.%i%s' \) = 0\.15350*/", $found );
    584592    }
     593
     594    /**
     595     * @ticket 25834
     596     * @expectedIncorrectUsage WP_Date_Query
     597     */
     598    public function test_validate_date_query_before_after(){
     599        // Valid values.
     600        $valid_args = array(
     601            array(
     602                'month' => 2,
     603                'year'  => 2014,
     604            ),
     605            array(
     606                'day'  => 8,
     607                'year' => 2014,
     608            ),
     609        );
     610
     611        foreach ( $valid_args as $args ) {
     612            $this->assertTrue( $this->q->validate_date_values( array( 'before' => $args ) ) );
     613            $this->assertTrue( $this->q->validate_date_values( array( 'after' => $args ) ) );
     614        }
     615
     616        // Invalid values.
     617        $invalid_args = array(
     618            array(
     619                'month' => 13,
     620            ),
     621            array(
     622                'day' => 32,
     623            ),
     624            array(
     625                'minute' => 60,
     626            ),
     627            array(
     628                'second' => 60,
     629            ),
     630            array(
     631                'week' => 54,
     632            ),
     633        );
     634
     635        foreach ( $invalid_args as $args ) {
     636            $this->assertFalse( $this->q->validate_date_values( array( 'before' => $args ) ) );
     637            $this->assertFalse( $this->q->validate_date_values( array( 'after' => $args ) ) );
     638        }
     639    }
     640
     641    /**
     642     * @ticket 25834
     643     * @expectedIncorrectUsage WP_Date_Query
     644     */
     645    public function test_validate_date_query_before_after_with_month(){
     646        // Both are valid.
     647        $args = array(
     648            'before' => array(
     649                'month' => 2,
     650                'year'  => 2014,
     651            ),
     652            'month' => 10,
     653        );
     654        $this->assertTrue( $this->q->validate_date_values( $args ) );
     655
     656        // 'before' is invalid, 'month' is valid.
     657        $args = array(
     658            'before' => array(
     659                'month' => 13,
     660                'year'  => 2014,
     661            ),
     662            'month' => 10,
     663        );
     664        $this->assertFalse( $this->q->validate_date_values( $args ) );
     665
     666        // 'before' is valid, 'month' is invalid.
     667        $args = array(
     668            'before' => array(
     669                'month' => 10,
     670                'year'  => 2014,
     671            ),
     672            'month' => 14,
     673        );
     674        $this->assertFalse( $this->q->validate_date_values( $args ) );
     675
     676        // Both are invalid.
     677        $args = array(
     678            'before' => array(
     679                'month' => 14,
     680                'year'  => 2014,
     681            ),
     682            'month' => 14,
     683        );
     684        $this->assertFalse( $this->q->validate_date_values( $args ) );
     685    }
     686
     687    /**
     688     * @ticket 25834
     689     * @expectedIncorrectUsage WP_Date_Query
     690     */
     691    public function test_validate_date_values_week() {
     692        // Valid values.
     693        $weeks = range( 1, 53 );
     694        foreach ( $weeks as $week ) {
     695            $this->assertTrue( $this->q->validate_date_values( array( 'week' => $week ) ) );
     696        }
     697
     698        // Invalid values.
     699        $weeks = array( -1, 0, 54 );
     700        foreach ( $weeks as $week ) {
     701            $this->assertFalse( $this->q->validate_date_values( array( 'week' => $week ) ) );
     702        }
     703
     704        // Valid combinations.
     705        $weeks = array(
     706            array(
     707                'week' => 52,
     708                'year' => 2012,
     709            ),
     710            array(
     711                'week' => 53,
     712                'year' => 2009,
     713            ),
     714        );
     715
     716        foreach ( $weeks as $week_args ) {
     717            $this->assertTrue( $this->q->validate_date_values( $week_args ) );
     718        }
     719
     720        // Invalid combinations.
     721        $weeks = array(
     722            // 2012 has 52 weeks.
     723            array(
     724                'week' => 53,
     725                'year' => 2012,
     726            ),
     727
     728            // 2013 has 53 weeks.
     729            array(
     730                'week' => 54,
     731                'year' => 2009,
     732            )
     733        );
     734
     735        foreach ( $weeks as $week_args ) {
     736            $this->assertFalse( $this->q->validate_date_values( $week_args ) );
     737        }
     738    }
     739
     740    /**
     741     * @ticket 25834
     742     * @expectedIncorrectUsage WP_Date_Query
     743     */
     744    public function test_validate_date_values_month() {
     745        // Valid values.
     746        $months = range( 1, 12 );
     747        foreach ( $months as $month ) {
     748            $this->assertTrue( $this->q->validate_date_values( array( 'month' => $month ) ) );
     749        }
     750
     751        // Invalid values.
     752        $months = array( -1, 0, 13, 'string who wants to be a int' );
     753        foreach ( $months as $month ) {
     754            $this->assertFalse( $this->q->validate_date_values( array( 'month' => $month ) ) );
     755        }
     756    }
     757
     758    /**
     759     * @ticket 25834
     760     * @expectedIncorrectUsage WP_Date_Query
     761     */
     762    public function test_validate_date_values_day() {
     763        // Valid values.
     764        $days = range( 1, 31 );
     765        foreach ( $days as $day ) {
     766            $this->assertTrue( $this->q->validate_date_values( array( 'day' => $day ) ) );
     767        }
     768
     769        // Invalid values.
     770        $days = array( -1, 32 );
     771        foreach ( $days as $day ) {
     772            $this->assertFalse( $this->q->validate_date_values( array( 'day' => $day ) ) );
     773        }
     774
     775        // Valid combinations.
     776        $days = array(
     777            array(
     778                'day'   => 29,
     779                'month' => 2,
     780                'year'  => 2008,
     781            ),
     782            array(
     783                'day'   => 28,
     784                'month' => 2,
     785                'year'  => 2009,
     786            ),
     787        );
     788
     789        foreach ( $days as $args ) {
     790            $this->assertTrue( $this->q->validate_date_values( $args ) );
     791        }
     792
     793        // Invalid combinations.
     794        $days = array(
     795            // February 2008 has 29 days.
     796            array(
     797                'day'   => 30,
     798                'month' => 2,
     799                'year'  => 2008,
     800            ),
     801
     802            // February 2009 has 29 days.
     803            array(
     804                'day'   => 29,
     805                'month' => 2,
     806                'year'  => 2009,
     807            ),
     808        );
     809
     810        foreach ( $days as $args ) {
     811            $this->assertFalse( $this->q->validate_date_values( $args ) );
     812        }
     813    }
     814
     815    /**
     816     * @ticket 25834
     817     * @expectedIncorrectUsage WP_Date_Query
     818     */
     819    public function test_validate_date_values_hour() {
     820        // Valid values.
     821        $hours = range( 1, 23 );
     822        foreach ( $hours as $hour ) {
     823            $this->assertTrue( $this->q->validate_date_values( array( 'hour' => $hour ) ) );
     824        }
     825
     826        // Invalid values.
     827        $hours = array( -1, 24, 25, 'string who wants to be a int' );
     828        foreach ( $hours as $hour ) {
     829            $this->assertFalse( $this->q->validate_date_values( array( 'hour' => $hour ) ) );
     830        }
     831    }
     832
     833    /**
     834     * @ticket 25834
     835     * @expectedIncorrectUsage WP_Date_Query
     836     */
     837    public function test_validate_date_values_minute() {
     838        // Valid values.
     839        $minutes = range( 0, 59 );
     840        foreach ( $minutes as $minute ) {
     841            $this->assertTrue( $this->q->validate_date_values( array( 'minute' => $minute ) ) );
     842        }
     843
     844        // Invalid values.
     845        $minutes = array( -1, 60 );
     846        foreach ( $minutes as $minute ) {
     847            $this->assertFalse( $this->q->validate_date_values( array( 'minute' => $minute ) ) );
     848        }
     849    }
     850
     851    /**
     852     * @ticket 25834
     853     * @expectedIncorrectUsage WP_Date_Query
     854     */
     855    public function test_validate_date_values_second() {
     856        // Valid values.
     857        $seconds = range( 0, 59 );
     858        foreach ( $seconds as $second ) {
     859            $this->assertTrue( $this->q->validate_date_values( array( 'second' => $second ) ) );
     860        }
     861
     862        // Invalid values.
     863        $seconds = array( -1, 60 );
     864        foreach ( $seconds as $second ) {
     865            $this->assertFalse( $this->q->validate_date_values( array( 'second' => $second ) ) );
     866        }
     867
     868    }
     869
     870    /**
     871     * @ticket 25834
     872     * @expectedIncorrectUsage WP_Date_Query
     873     */
     874    public function test_validate_date_values_day_of_week() {
     875        // Valid values.
     876        $days_of_week = range( 1, 7 );
     877        foreach ( $days_of_week as $day_of_week ) {
     878            $this->assertTrue( $this->q->validate_date_values( array( 'dayofweek' => $day_of_week ) ) );
     879        }
     880
     881        // Invalid values.
     882        $days_of_week = array( -1, 0, 8 );
     883        foreach ( $days_of_week as $day_of_week ) {
     884            $this->assertFalse( $this->q->validate_date_values( array( 'dayofweek' => $day_of_week ) ) );
     885        }
     886    }
     887
     888    /**
     889     * @ticket 25834
     890     * @expectedIncorrectUsage WP_Date_Query
     891     */
     892    public function test_validate_date_values_day_of_year() {
     893        // Valid values.
     894        $days_of_year = range( 1, 366 );
     895        foreach ( $days_of_year as $day_of_year ) {
     896            $this->assertTrue( $this->q->validate_date_values( array( 'dayofyear' => $day_of_year ) ) );
     897        }
     898
     899        // Invalid values.
     900        $days_of_year = array( -1, 0, 367 );
     901        foreach ( $days_of_year as $day_of_year ) {
     902            $this->assertFalse( @$this->q->validate_date_values( array( 'dayofyear' => $day_of_year ) ) );
     903        }
     904    }
    585905}
Note: See TracChangeset for help on using the changeset viewer.