From 6e7b1243f30af7b85d7dff44df4c86d852dec7df Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 22 Aug 2024 23:25:17 +0000 Subject: [PATCH] Date/Time: Prevent type errors in `current_time()`. Prevents a potential type error when calling `current_time( 'timestamp' )` by casting `get_option( 'gmt_offset' )` to a float prior to performing calculations with the value. This mainly accounts for incorrect storage of values, such as an empty string or city name. Follow up to [45856], [55054], [55598]. Props hellofromtonya, peterwilsoncc, rarst, costdev, Nick_theGeek, SergeyBiryukov, johnbillion, desrosj, reputeinfosystems, audrasjb, oglekler. Fixes #57035. git-svn-id: https://develop.svn.wordpress.org/trunk@58923 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 2 +- tests/phpunit/tests/date/currentTime.php | 127 +++++++++++++++++++++-- 2 files changed, 118 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 16be10713f0dd..6ddd11f0715a1 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -73,7 +73,7 @@ function mysql2date( $format, $date, $translate = true ) { function current_time( $type, $gmt = 0 ) { // Don't use non-GMT timestamp, unless you know the difference and really need to. if ( 'timestamp' === $type || 'U' === $type ) { - return $gmt ? time() : time() + (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); + return $gmt ? time() : time() + (int) ( (float) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); } if ( 'mysql' === $type ) { diff --git a/tests/phpunit/tests/date/currentTime.php b/tests/phpunit/tests/date/currentTime.php index b308f4918ff5b..a41ea258dbe37 100644 --- a/tests/phpunit/tests/date/currentTime.php +++ b/tests/phpunit/tests/date/currentTime.php @@ -91,9 +91,14 @@ public function test_should_work_with_changed_timezone() { /** * @ticket 40653 + * @ticket 57998 + * + * @dataProvider data_timezones + * + * @param string $timezone The timezone to test. */ - public function test_should_return_wp_timestamp() { - update_option( 'timezone_string', 'Europe/Helsinki' ); + public function test_should_return_wp_timestamp( $timezone ) { + update_option( 'timezone_string', $timezone ); $timestamp = time(); $datetime = new DateTime( '@' . $timestamp ); @@ -101,24 +106,29 @@ public function test_should_return_wp_timestamp() { $wp_timestamp = $timestamp + $datetime->getOffset(); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC - $this->assertEqualsWithDelta( $timestamp, current_time( 'timestamp', true ), 2, 'The dates should be equal' ); + $this->assertEqualsWithDelta( $timestamp, current_time( 'timestamp', true ), 2, 'When passing "timestamp", the date should be equal to time()' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC - $this->assertEqualsWithDelta( $timestamp, current_time( 'U', true ), 2, 'The dates should be equal' ); + $this->assertEqualsWithDelta( $timestamp, current_time( 'U', true ), 2, 'When passing "U", the date should be equal to time()' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested - $this->assertEqualsWithDelta( $wp_timestamp, current_time( 'timestamp' ), 2, 'The dates should be equal' ); + $this->assertEqualsWithDelta( $wp_timestamp, current_time( 'timestamp' ), 2, 'When passing "timestamp", the date should be equal to calculated timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested - $this->assertEqualsWithDelta( $wp_timestamp, current_time( 'U' ), 2, 'The dates should be equal' ); + $this->assertEqualsWithDelta( $wp_timestamp, current_time( 'U' ), 2, 'When passing "U", the date should be equal to calculated timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested - $this->assertIsInt( current_time( 'timestamp' ) ); + $this->assertIsInt( current_time( 'timestamp' ), 'The returned timestamp should be an integer' ); } /** * @ticket 40653 + * @ticket 57998 + * + * @dataProvider data_timezones + * + * @param string $timezone The timezone to test. */ - public function test_should_return_correct_local_time() { - update_option( 'timezone_string', 'Europe/Helsinki' ); + public function test_should_return_correct_local_time( $timezone ) { + update_option( 'timezone_string', $timezone ); $timestamp = time(); $datetime_local = new DateTime( '@' . $timestamp ); @@ -127,7 +137,20 @@ public function test_should_return_correct_local_time() { $datetime_utc->setTimezone( new DateTimeZone( 'UTC' ) ); $this->assertEqualsWithDelta( strtotime( $datetime_local->format( DATE_W3C ) ), strtotime( current_time( DATE_W3C ) ), 2, 'The dates should be equal' ); - $this->assertEqualsWithDelta( strtotime( $datetime_utc->format( DATE_W3C ) ), strtotime( current_time( DATE_W3C, true ) ), 2, 'The dates should be equal' ); + $this->assertEqualsWithDelta( strtotime( $datetime_utc->format( DATE_W3C ) ), strtotime( current_time( DATE_W3C, true ) ), 2, 'When passing "timestamp", the dates should be equal' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_timezones() { + return array( + array( 'Europe/Helsinki' ), + array( 'Indian/Antananarivo' ), + array( 'Australia/Adelaide' ), + ); } /** @@ -158,4 +181,88 @@ public function test_should_work_with_deprecated_timezone() { $this->assertSame( gmdate( $format ), $current_time_gmt, 'The dates should be equal [3]' ); $this->assertSame( $datetime->format( $format ), $current_time, 'The dates should be equal [4]' ); } + + /** + * Ensures an empty offset does not cause a type error. + * + * @ticket 57998 + */ + public function test_empty_offset_does_not_cause_a_type_error() { + // Ensure `wp_timezone_override_offset()` doesn't override offset. + update_option( 'timezone_string', '' ); + update_option( 'gmt_offset', '' ); + + $expected = time(); + + // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested + $this->assertEqualsWithDelta( $expected, current_time( 'timestamp' ), 2, 'The timestamps should be equal' ); + } + + /** + * Ensures the offset applied in current_time() is correct. + * + * @ticket 57998 + * + * @dataProvider data_partial_hour_timezones_with_timestamp + * + * @param float $partial_hour Partial hour GMT offset to test. + */ + public function test_partial_hour_timezones_with_timestamp( $partial_hour ) { + // Ensure `wp_timezone_override_offset()` doesn't override offset. + update_option( 'timezone_string', '' ); + update_option( 'gmt_offset', $partial_hour ); + + $expected = time() + (int) ( $partial_hour * HOUR_IN_SECONDS ); + + // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested + $this->assertEqualsWithDelta( $expected, current_time( 'timestamp' ), 2, 'The timestamps should be equal' ); + } + + /** + * Tests the tests. + * + * Ensures the offsets match the stated timezones in the data provider. + * + * @ticket 57998 + * + * @dataProvider data_partial_hour_timezones_with_timestamp + * + * @param float $partial_hour Partial hour GMT offset to test. + * @param string $timezone_string Timezone string to test. + */ + public function test_partial_hour_timezones_match_datetime_offset( $partial_hour, $timezone_string ) { + $timezone = new DateTimeZone( $timezone_string ); + $datetime = new DateTime( 'now', $timezone ); + $dst_offset = (int) $datetime->format( 'I' ); + + // Timezone offset in hours. + $offset = $timezone->getOffset( $datetime ) / HOUR_IN_SECONDS; + + /* + * Adjust for daylight saving time. + * + * DST adds an hour to the offset, the partial hour offset + * is set the the standard time offset so this removes the + * DST offset to avoid false negatives. + */ + $offset -= $dst_offset; + + $this->assertSame( $partial_hour, $offset, 'The offset should match to timezone.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_partial_hour_timezones_with_timestamp() { + return array( + '+12:45' => array( 12.75, 'Pacific/Chatham' ), // New Zealand, Chatham Islands. + '+9:30' => array( 9.5, 'Australia/Darwin' ), // Australian Northern Territory. + '+05:30' => array( 5.5, 'Asia/Kolkata' ), // India and Sri Lanka. + '+05:45' => array( 5.75, 'Asia/Kathmandu' ), // Nepal. + '-03:30' => array( -3.50, 'Canada/Newfoundland' ), // Canada, Newfoundland. + '-09:30' => array( -9.50, 'Pacific/Marquesas' ), // French Polynesia, Marquesas Islands. + ); + } }