diff --git a/lib/Cake/Test/Case/Utility/CakeTimeTest.php b/lib/Cake/Test/Case/Utility/CakeTimeTest.php index b358b1b48..4bc9f8a62 100644 --- a/lib/Cake/Test/Case/Utility/CakeTimeTest.php +++ b/lib/Cake/Test/Case/Utility/CakeTimeTest.php @@ -421,6 +421,18 @@ class CakeTimeTest extends CakeTestCase { $this->_restoreSystemTimezone(); } + public function testNiceTimezoneConversion() { + date_default_timezone_set('Europe/Copenhagen'); // server timezone + $clientTimeZone = new DateTimeZone('Asia/Bangkok'); + $clientDateTime = new DateTime('2019-01-31 10:00:00', $clientTimeZone); + // Convert to UTC. + $actual = CakeTime::nice($clientDateTime, 'UTC', '%Y-%m-%d %H:%M:%S'); + $clientDateTime->setTimezone(new DateTimeZone('UTC')); + $expected = $clientDateTime->format('Y-m-d H:i:s'); + $this->assertEquals($expected, $actual); + $this->_restoreSystemTimezone(); + } + /** * testNiceShort method * @@ -966,23 +978,44 @@ class CakeTimeTest extends CakeTestCase { */ public function testFromStringWithDateTime() { date_default_timezone_set('UTC'); - $date = new DateTime('+1 hour', new DateTimeZone('America/New_York')); $result = $this->Time->fromString($date, 'UTC'); $date->setTimezone(new DateTimeZone('UTC')); $expected = $date->format('U') + $date->getOffset(); - $this->assertWithinMargin($expected, $result, 1); + $this->_restoreSystemTimezone(); + } + public function testFromStringWithDateTimeAsia() { date_default_timezone_set('Australia/Melbourne'); - $date = new DateTime('+1 hour', new DateTimeZone('America/New_York')); $result = $this->Time->fromString($date, 'Asia/Kuwait'); - $date->setTimezone(new DateTimeZone('Asia/Kuwait')); $expected = $date->format('U') + $date->getOffset(); $this->assertWithinMargin($expected, $result, 1); + $this->_restoreSystemTimezone(); + } + public function testFromStringTimezoneConversionToUTC() { + date_default_timezone_set('Europe/Copenhagen'); // server timezone + $clientTimeZone = new DateTimeZone('Asia/Bangkok'); + $clientDateTime = new DateTime('2019-01-31 10:00:00', $clientTimeZone); + // Convert to UTC. + $actual = CakeTime::fromString($clientDateTime, 'UTC'); + $clientDateTime->setTimezone(new DateTimeZone('UTC')); + $expected = $clientDateTime->getTimestamp() + $clientDateTime->getOffset(); // 1548903600 + $this->assertEquals($expected, $actual); + $this->_restoreSystemTimezone(); + } + + public function testFromStringUTCtoCopenhagen() { + date_default_timezone_set('UTC'); // server timezone + $clientTimeZone = new DateTimeZone('UTC'); + $clientDateTime = new DateTime('2012-01-01 10:00:00', $clientTimeZone); + $actual = CakeTime::fromString($clientDateTime, 'Europe/Copenhagen'); + $clientDateTime->setTimezone(new DateTimeZone('Europe/Copenhagen')); + $expected = $clientDateTime->getTimestamp() + $clientDateTime->getOffset(); // 1325415600 + $this->assertEquals($expected, $actual); $this->_restoreSystemTimezone(); } @@ -998,6 +1031,24 @@ class CakeTimeTest extends CakeTestCase { $this->assertEquals($result, $date->format('U')); } + public function testConvertToBangkok() { + $serverTimeZoneName = 'Europe/Copenhagen'; + date_default_timezone_set($serverTimeZoneName); + + $serverTimeZone = new DateTimeZone($serverTimeZoneName); + $DateTime = new DateTime('2019-01-31 04:00:00', $serverTimeZone); + $serverTimestamp = $DateTime->getTimestamp() + $DateTime->getOffset(); // 1548907200 + + $clientTimeZoneName = 'Asia/Bangkok'; + $clientTimeZone = new DateTimeZone($clientTimeZoneName); + $DateTime->setTimezone($clientTimeZone); + $expected = $DateTime->getTimestamp() + $DateTime->getOffset(); // 1548928800 + + $actual = CakeTime::convert($serverTimestamp, $clientTimeZoneName); + $this->assertEquals($expected, $actual); + $this->_restoreSystemTimezone(); + } + /** * test converting time specifiers using a time definition localfe file * @@ -1149,6 +1200,28 @@ class CakeTimeTest extends CakeTestCase { $this->assertEquals($expected, $result); } + public function testI18nFormatTimezoneConversionToUTC() { + date_default_timezone_set('Europe/Copenhagen'); // server timezone + $clientTimeZone = new DateTimeZone('Asia/Bangkok'); + $clientDateTime = new DateTime('2019-01-31 10:00:00', $clientTimeZone); + // Convert to UTC. + $actual = CakeTime::i18nFormat($clientDateTime, '%Y-%m-%d %H:%M:%S', false, 'UTC'); + $clientDateTime->setTimezone(new DateTimeZone('UTC')); + $expected = $clientDateTime->format('Y-m-d H:i:s'); + $this->assertEquals($expected, $actual); + $this->_restoreSystemTimezone(); + } + + public function testI18nFormatUTCtoCopenhagen() { + date_default_timezone_set('UTC'); + $clientTimeZone = new DateTimeZone('UTC'); + $clientDateTime = new DateTime('2012-01-01 10:00:00', $clientTimeZone); + $actual = CakeTime::i18nFormat($clientDateTime, '%Y-%m-%d %H:%M', false, 'Europe/Copenhagen'); + $clientDateTime->setTimezone(new DateTimeZone('Europe/Copenhagen')); + $expected = $clientDateTime->format('Y-m-d H:i'); + $this->assertEquals($expected, $actual); + } + /** * test new format() syntax which inverts first and second parameters * @@ -1217,7 +1290,7 @@ class CakeTimeTest extends CakeTestCase { * * @return void */ - public function testCorrectTimezoneConversion() { + public function testCorrectTimezoneConversionAsString() { date_default_timezone_set('UTC'); $date = '2012-01-01 10:00:00'; $converted = CakeTime::format($date, '%Y-%m-%d %H:%M', '', 'Europe/Copenhagen'); @@ -1226,4 +1299,27 @@ class CakeTimeTest extends CakeTestCase { $this->assertEquals($expected->format('Y-m-d H:i'), $converted); } + public function testCorrectTimezoneConversionAsObject() { + date_default_timezone_set('UTC'); + $clientTimeZone = new DateTimeZone('UTC'); + $date = '2012-01-01 10:00:00'; + $clientDateTime = new DateTime($date, $clientTimeZone); + $converted = CakeTime::format($clientDateTime, '%Y-%m-%d %H:%M', '', 'Europe/Copenhagen'); + $clientDateTime->setTimezone(new DateTimeZone('Europe/Copenhagen')); + $expected = $clientDateTime->format('Y-m-d H:i'); + $this->assertEquals($expected, $converted); + } + + public function testFormatTimezoneConversionToUTC() { + date_default_timezone_set('Europe/Copenhagen'); // server timezone + $clientTimeZone = new DateTimeZone('Asia/Bangkok'); + $clientDateTime = new DateTime('2019-01-31 10:00:00', $clientTimeZone); + // Convert to UTC. + $actual = CakeTime::format($clientDateTime, '%Y-%m-%d %H:%M:%S', false, 'UTC'); + $clientDateTime->setTimezone(new DateTimeZone('UTC')); + $expected = $clientDateTime->format('Y-m-d H:i:s'); + $this->assertEquals($expected, $actual); + $this->_restoreSystemTimezone(); + } + } diff --git a/lib/Cake/Utility/CakeTime.php b/lib/Cake/Utility/CakeTime.php index 2a1de55f3..63911bfde 100644 --- a/lib/Cake/Utility/CakeTime.php +++ b/lib/Cake/Utility/CakeTime.php @@ -239,9 +239,9 @@ class CakeTime { /** * Converts given time (in server's time zone) to user's local time, given his/her timezone. * - * @param string $serverTime UNIX timestamp - * @param string|DateTimeZone $timezone User's timezone string or DateTimeZone object - * @return int UNIX timestamp + * @param int $serverTime Server's timestamp. + * @param string|DateTimeZone $timezone User's timezone string or DateTimeZone object. + * @return int User's timezone timestamp. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::convert */ public static function convert($serverTime, $timezone) { @@ -303,11 +303,11 @@ class CakeTime { } /** - * Returns a UNIX timestamp, given either a UNIX timestamp or a valid strtotime() date string. + * Returns a timestamp, given either a UNIX timestamp or a valid strtotime() date string. * * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Parsed timestamp + * @return int|false Parsed given timezone timestamp. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::fromString */ public static function fromString($dateString, $timezone = null) { @@ -354,22 +354,22 @@ class CakeTime { * See http://php.net/manual/en/function.strftime.php for information on formatting * using locale strings. * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object * @param string $format The format to use. If null, `CakeTime::$niceFormat` is used * @return string Formatted date string * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::nice */ - public static function nice($dateString = null, $timezone = null, $format = null) { - if (!$dateString) { - $dateString = time(); + public static function nice($date = null, $timezone = null, $format = null) { + if (!$date) { + $date = time(); } - $date = static::fromString($dateString, $timezone); - + $timestamp = static::fromString($date, $timezone); if (!$format) { $format = static::$niceFormat; } - return static::_strftime(static::convertSpecifiers($format, $date), $date); + $convertedFormat = static::convertSpecifiers($format, $timestamp); + return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); } /** @@ -382,28 +382,31 @@ class CakeTime { * If $dateString's year is the current year, the returned string does not * include mention of the year. * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object * @return string Described, relative date string * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::niceShort */ - public static function niceShort($dateString = null, $timezone = null) { - if (!$dateString) { - $dateString = time(); + public static function niceShort($date = null, $timezone = null) { + if (!$date) { + $date = time(); } - $date = static::fromString($dateString, $timezone); + $timestamp = static::fromString($date, $timezone); - if (static::isToday($dateString, $timezone)) { - return __d('cake', 'Today, %s', static::_strftime("%H:%M", $date)); + if (static::isToday($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Today, %s', $formattedDate); } - if (static::wasYesterday($dateString, $timezone)) { - return __d('cake', 'Yesterday, %s', static::_strftime("%H:%M", $date)); + if (static::wasYesterday($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Yesterday, %s', $formattedDate); } - if (static::isTomorrow($dateString, $timezone)) { - return __d('cake', 'Tomorrow, %s', static::_strftime("%H:%M", $date)); + if (static::isTomorrow($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Tomorrow, %s', $formattedDate); } - $d = static::_strftime("%w", $date); + $d = static::_strftimeWithTimezone("%w", $timestamp, $date, $timezone); $day = array( __d('cake', 'Sunday'), __d('cake', 'Monday'), @@ -413,18 +416,21 @@ class CakeTime { __d('cake', 'Friday'), __d('cake', 'Saturday') ); - if (static::wasWithinLast('7 days', $dateString, $timezone)) { - return sprintf('%s %s', $day[$d], static::_strftime(static::$niceShortFormat, $date)); + if (static::wasWithinLast('7 days', $date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); + return sprintf('%s %s', $day[$d], $formattedDate); } - if (static::isWithinNext('7 days', $dateString, $timezone)) { - return __d('cake', 'On %s %s', $day[$d], static::_strftime(static::$niceShortFormat, $date)); + if (static::isWithinNext('7 days', $date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); + return __d('cake', 'On %s %s', $day[$d], $formattedDate); } $y = ''; - if (!static::isThisYear($date)) { + if (!static::isThisYear($timestamp)) { $y = ' %Y'; } - return static::_strftime(static::convertSpecifiers("%b %eS{$y}, %H:%M", $date), $date); + $format = static::convertSpecifiers("%b %eS{$y}, %H:%M", $timestamp); + return static::_strftimeWithTimezone($format, $timestamp, $date, $timezone); } /** @@ -1055,17 +1061,18 @@ class CakeTime { * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::i18nFormat */ public static function i18nFormat($date, $format = null, $default = false, $timezone = null) { - $date = static::fromString($date, $timezone); - if ($date === false && $default !== false) { + $timestamp = static::fromString($date, $timezone); + if ($timestamp === false && $default !== false) { return $default; } - if ($date === false) { + if ($timestamp === false) { return ''; } if (empty($format)) { $format = '%x'; } - return static::_strftime(static::convertSpecifiers($format, $date), $date); + $convertedFormat = static::convertSpecifiers($format, $timestamp); + return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); } /** @@ -1156,13 +1163,12 @@ class CakeTime { * Handles utf8_encoding the result of strftime when necessary. * * @param string $format Format string. - * @param int $date Timestamp to format. + * @param int $timestamp Timestamp to format. * @return string formatted string with correct encoding. */ - protected static function _strftime($format, $date) { - $format = strftime($format, $date); + protected static function _strftime($format, $timestamp) { + $format = strftime($format, $timestamp); $encoding = Configure::read('App.encoding'); - if (!empty($encoding) && $encoding === 'UTF-8') { if (function_exists('mb_check_encoding')) { $valid = mb_check_encoding($format, $encoding); @@ -1176,4 +1182,29 @@ class CakeTime { return $format; } +/** + * Multibyte wrapper for strftime. + * + * Adjusts the timezone when necessary before formatting the time. + * + * @param string $format Format string. + * @param int $timestamp Timestamp to format. + * @param int|string|DateTime $date Timestamp, strtotime() valid string or DateTime object. + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object. + * @return string Formatted date string with correct encoding. + */ + protected static function _strftimeWithTimezone($format, $timestamp, $date, $timezone) { + $serverTimeZone = date_default_timezone_get(); + if ( + !empty($timezone) && + $date instanceof DateTime && + $date->getTimezone()->getName() != $serverTimeZone + ) { + date_default_timezone_set($timezone); + } + $result = static::_strftime($format, $timestamp); + date_default_timezone_set($serverTimeZone); + return $result; + } + }