Make WordPress Core

Changeset 45882


Ignore:
Timestamp:
08/23/2019 12:56:21 AM (7 years 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.