WordPress.org

Make WordPress Core

Changeset 45882


Ignore:
Timestamp:
08/23/2019 12:56:21 AM (4 weeks ago)
Author:
SergeyBiryukov
Message:

Date/Time: Rewrite and simplify date_i18n() using wp_timezone() to address multiple issues with certain date formats and timezones, while preserving some extra handling for legacy use cases.

Improve unit test coverage.

Props Rarst, remcotolsma, raubvogel.
Fixes #25768.

Location:
trunk
Files:
2 edited

Legend:

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

    r45877 r45882  
    129129 * take over the format for the date. If it isn't, then the date format string
    130130 * will be used instead.
     131 *
     132 * Note that due to the way WP typically generates a sum of timestamp and offset
     133 * with `strtotime()`, it implies offset added at a _current_ time, not at the time
     134 * the timestamp represents. Storing such timestamps or calculating them differently
     135 * will lead to invalid output.
    131136 *
    132137 * @since 0.71
     
    144149function date_i18n( $dateformatstring, $timestamp_with_offset = false, $gmt = false ) {
    145150    global $wp_locale;
     151
    146152    $i = $timestamp_with_offset;
    147153
     
    155161     */
    156162    $req_format = $dateformatstring;
    157 
    158     $dateformatstring = preg_replace( '/(?<!\\\\)c/', DATE_W3C, $dateformatstring );
     163    $new_format = '';
     164
     165    // We need to unpack shorthand `r` format because it has parts that might be localized.
    159166    $dateformatstring = preg_replace( '/(?<!\\\\)r/', DATE_RFC2822, $dateformatstring );
    160167
    161     if ( ( ! empty( $wp_locale->month ) ) && ( ! empty( $wp_locale->weekday ) ) ) {
    162         $datemonth            = $wp_locale->get_month( gmdate( 'm', $i ) );
    163         $datemonth_abbrev     = $wp_locale->get_month_abbrev( $datemonth );
    164         $dateweekday          = $wp_locale->get_weekday( gmdate( 'w', $i ) );
    165         $dateweekday_abbrev   = $wp_locale->get_weekday_abbrev( $dateweekday );
    166         $datemeridiem         = $wp_locale->get_meridiem( gmdate( 'a', $i ) );
    167         $datemeridiem_capital = $wp_locale->get_meridiem( gmdate( 'A', $i ) );
    168         $dateformatstring     = ' ' . $dateformatstring;
    169         $dateformatstring     = preg_replace( '/([^\\\])D/', "\\1" . backslashit( $dateweekday_abbrev ), $dateformatstring );
    170         $dateformatstring     = preg_replace( '/([^\\\])F/', "\\1" . backslashit( $datemonth ), $dateformatstring );
    171         $dateformatstring     = preg_replace( '/([^\\\])l/', "\\1" . backslashit( $dateweekday ), $dateformatstring );
    172         $dateformatstring     = preg_replace( '/([^\\\])M/', "\\1" . backslashit( $datemonth_abbrev ), $dateformatstring );
    173         $dateformatstring     = preg_replace( '/([^\\\])a/', "\\1" . backslashit( $datemeridiem ), $dateformatstring );
    174         $dateformatstring     = preg_replace( '/([^\\\])A/', "\\1" . backslashit( $datemeridiem_capital ), $dateformatstring );
    175 
    176         $dateformatstring = substr( $dateformatstring, 1, strlen( $dateformatstring ) - 1 );
    177     }
    178     $timezone_formats    = array( 'P', 'I', 'O', 'T', 'Z', 'e' );
    179     $timezone_formats_re = implode( '|', $timezone_formats );
    180     if ( preg_match( "/$timezone_formats_re/", $dateformatstring ) ) {
    181         $timezone_string = get_option( 'timezone_string' );
    182         if ( false === $timestamp_with_offset && $gmt ) {
    183             $timezone_string = 'UTC';
    184         }
    185         if ( $timezone_string ) {
    186             $timezone_object = timezone_open( $timezone_string );
    187             $date_object     = date_create( null, $timezone_object );
    188             foreach ( $timezone_formats as $timezone_format ) {
    189                 if ( false !== strpos( $dateformatstring, $timezone_format ) ) {
    190                     $formatted        = date_format( $date_object, $timezone_format );
    191                     $dateformatstring = ' ' . $dateformatstring;
    192                     $dateformatstring = preg_replace( "/([^\\\])$timezone_format/", "\\1" . backslashit( $formatted ), $dateformatstring );
    193                     $dateformatstring = substr( $dateformatstring, 1, strlen( $dateformatstring ) - 1 );
    194                 }
     168    /*
     169     * Timestamp with offset is typically produced by a UTC `strtotime()` call on an input without timezone.
     170     * This is the best attempt to reverse that operation into a local time to use.
     171     */
     172    $local_time = gmdate( 'Y-m-d H:i:s', $i );
     173    $gmt_mode   = $gmt && ( false === $timestamp_with_offset );
     174    $timezone   = $gmt_mode ? new DateTimeZone( 'UTC' ) : wp_timezone();
     175    $datetime   = date_create( $local_time, $timezone );
     176
     177    /*
     178     * This is a legacy implementation quirk that the returned timestamp is also with offset.
     179     * Ideally this function should never be used to produce a timestamp.
     180     */
     181    $timestamp_mode = ( 'U' === $dateformatstring );
     182
     183    if ( $timestamp_mode ) {
     184        $new_format = $i;
     185    }
     186
     187    if ( ! $timestamp_mode && ! empty( $wp_locale->month ) && ! empty( $wp_locale->weekday ) ) {
     188        $month   = $wp_locale->get_month( $datetime->format( 'm' ) );
     189        $weekday = $wp_locale->get_weekday( $datetime->format( 'w' ) );
     190
     191        $format_length = strlen( $dateformatstring );
     192
     193        for ( $i = 0; $i < $format_length; $i ++ ) {
     194            switch ( $dateformatstring[ $i ] ) {
     195                case 'D':
     196                    $new_format .= backslashit( $wp_locale->get_weekday_abbrev( $weekday ) );
     197                    break;
     198                case 'F':
     199                    $new_format .= backslashit( $month );
     200                    break;
     201                case 'l':
     202                    $new_format .= backslashit( $weekday );
     203                    break;
     204                case 'M':
     205                    $new_format .= backslashit( $wp_locale->get_month_abbrev( $month ) );
     206                    break;
     207                case 'a':
     208                    $new_format .= backslashit( $wp_locale->get_meridiem( $datetime->format( 'a' ) ) );
     209                    break;
     210                case 'A':
     211                    $new_format .= backslashit( $wp_locale->get_meridiem( $datetime->format( 'A' ) ) );
     212                    break;
     213                case '\\':
     214                    $new_format .= $dateformatstring[ $i ];
     215
     216                    // If character follows a slash, we add it without translating.
     217                    if ( $i < $format_length ) {
     218                        $new_format .= $dateformatstring[ ++$i ];
     219                    }
     220                    break;
     221                default:
     222                    $new_format .= $dateformatstring[ $i ];
     223                    break;
    195224            }
    196         } else {
    197             $offset = get_option( 'gmt_offset' );
    198             foreach ( $timezone_formats as $timezone_format ) {
    199                 if ( 'I' === $timezone_format ) {
    200                     continue;
    201                 }
    202 
    203                 if ( false !== strpos( $dateformatstring, $timezone_format ) ) {
    204                     if ( 'Z' === $timezone_format ) {
    205                         $formatted = (string) ( $offset * HOUR_IN_SECONDS );
    206                     } else {
    207                         $prefix    = '';
    208                         $hours     = (int) $offset;
    209                         $separator = '';
    210                         $minutes   = abs( ( $offset - $hours ) * 60 );
    211 
    212                         if ( 'T' === $timezone_format ) {
    213                             $prefix = 'GMT';
    214                         } elseif ( 'e' === $timezone_format || 'P' === $timezone_format ) {
    215                             $separator = ':';
    216                         }
    217 
    218                         $formatted = sprintf( '%s%+03d%s%02d', $prefix, $hours, $separator, $minutes );
    219                     }
    220 
    221                     $dateformatstring = ' ' . $dateformatstring;
    222                     $dateformatstring = preg_replace( "/([^\\\])$timezone_format/", "\\1" . backslashit( $formatted ), $dateformatstring );
    223                     $dateformatstring = substr( $dateformatstring, 1 );
    224                 }
    225             }
    226         }
    227     }
    228     $j = gmdate( $dateformatstring, $i );
     225        }
     226    }
     227
     228    $j = $datetime->format( $new_format );
    229229
    230230    /**
     
    240240     */
    241241    $j = apply_filters( 'date_i18n', $j, $req_format, $i, $gmt );
     242
    242243    return $j;
    243244}
     
    15541555     */
    15551556    echo apply_filters( 'robots_txt', $output, $public );
     1557}
     1558
     1559/**
     1560 * Display the robots.txt file content.
     1561 *
     1562 * The echo content should be with usage of the permalinks or for creating the
     1563 * robots.txt file.
     1564 *
     1565 * @since 5.3.0
     1566 */
     1567function do_favicon() {
     1568    /**
     1569     * Fires when serving the favicon.ico file.
     1570     *
     1571     * @since 5.3.0
     1572     */
     1573    do_action( 'do_faviconico' );
     1574
     1575    wp_safe_redirect( esc_url( get_site_icon_url( 32, admin_url( 'images/w-logo-blue.png' ) ) ) );
     1576
     1577}
     1578
     1579/**
     1580 * Don't load all of WordPress when handling a favicon.ico request.
     1581 *
     1582 * Instead, send the headers for a zero-length favicon and bail.
     1583 *
     1584 * @since 3.0.0
     1585 */
     1586function wp_favicon_request() {
     1587    if ( '/favicon.ico' == $_SERVER['REQUEST_URI'] ) {
     1588        header( 'Content-Type: image/vnd.microsoft.icon' );
     1589        exit;
     1590    }
    15561591}
    15571592
  • trunk/tests/phpunit/tests/date/dateI18n.php

    r45424 r45882  
    1616    public function test_date_should_be_in_gmt() {
    1717        $this->assertEquals( strtotime( gmdate( DATE_RFC3339 ) ), strtotime( date_i18n( DATE_RFC3339, false, true ) ), 'The dates should be equal', 2 );
    18     }
    19 
    20     public function test_custom_timestamp_ignores_gmt_setting() {
    21         $this->assertEquals( '2012-12-01 00:00:00', date_i18n( 'Y-m-d H:i:s', strtotime( '2012-12-01 00:00:00' ) ) );
    2218    }
    2319
     
    8682
    8783    /**
     84     * @ticket 20973
     85     *
    8886     * @dataProvider data_formats
    89      * @ticket 20973
    9087     */
    9188    public function test_date_i18n_handles_shorthand_formats( $short, $full ) {
     
    108105        );
    109106    }
     107
     108    /**
     109     * @ticket 25768
     110     */
     111    public function test_should_return_wp_timestamp() {
     112        update_option( 'timezone_string', 'Europe/Kiev' );
     113
     114        $datetime     = new DateTimeImmutable( 'now', wp_timezone() );
     115        $timestamp    = $datetime->getTimestamp();
     116        $wp_timestamp = $timestamp + $datetime->getOffset();
     117
     118        $this->assertEquals( $wp_timestamp, date_i18n( 'U' ), 2 );
     119        $this->assertEquals( $timestamp, date_i18n( 'U', false, true ), 2 );
     120        $this->assertEquals( $wp_timestamp, date_i18n( 'U', $wp_timestamp ) );
     121    }
     122
     123    /**
     124     * @ticket 43530
     125     */
     126    public function test_swatch_internet_time_with_wp_timestamp() {
     127        update_option( 'timezone_string', 'America/Regina' );
     128
     129        $this->assertEquals( gmdate( 'B' ), date_i18n( 'B' ) );
     130    }
     131
     132    /**
     133     * @ticket 25768
     134     */
     135    public function test_should_handle_escaped_formats() {
     136        $format = 'D | \D | \\D | \\\D | \\\\D | \\\\\D | \\\\\\D';
     137
     138        $this->assertEquals( gmdate( $format ), date_i18n( $format ) );
     139    }
     140
     141    /**
     142     * @ticket 25768
     143     *
     144     * @dataProvider dst_times
     145     *
     146     * @param string $time     Time to test in Y-m-d H:i:s format.
     147     * @param string $timezone PHP timezone string to use.
     148     */
     149    public function test_should_handle_dst( $time, $timezone ) {
     150        update_option( 'timezone_string', $timezone );
     151
     152        $timezone     = new DateTimeZone( $timezone );
     153        $datetime     = new DateTime( $time, $timezone );
     154        $wp_timestamp = strtotime( $time );
     155        $format       = 'I ' . DATE_RFC3339;
     156
     157        $this->assertEquals( $datetime->format( $format ), date_i18n( $format, $wp_timestamp ) );
     158    }
     159
     160    public function dst_times() {
     161        return array(
     162            'Before DST start' => array( '2019-03-31 02:59:00', 'Europe/Kiev' ),
     163            'After DST start'  => array( '2019-03-31 04:01:00', 'Europe/Kiev' ),
     164            'Before DST end'   => array( '2019-10-27 02:59:00', 'Europe/Kiev' ),
     165            'After DST end'    => array( '2019-10-27 04:01:00', 'Europe/Kiev' ),
     166        );
     167    }
    110168}
Note: See TracChangeset for help on using the changeset viewer.