Skip to content

Commit

Permalink
[BUGFIX] Do not convert native DATETIME values in Extbase
Browse files Browse the repository at this point in the history
In contrast to the actual documentation in the code, when using
RDBMS native DATE / TIME / DATETIME fields, the RDBMS does not
adjust these field's values for time zone.
Independent of the actual time zone the DMBS (or the current
session) is using, the data is stored and retrieved "as is".

The only thing that matters is the TZ of PHP, which must be
identical for storing and retrieving the data, otherwise
data is modified.

TYPO3 works just fine like that in Backend, however Extbase'
DataMapper considered the timezone of these fields as "UTC-stored",
which is wrong.

This patch corrects this behaviour to match the rest of the Core.

Resolves: #91240
Releases: master
Change-Id: Idb8e0a6f241aa0ed4e8d4a66129b58e34b9b3292
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64053
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Thorben Kapp <thorben@webcoast.dk>
Tested-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Thorben Kapp <thorben@webcoast.dk>
Reviewed-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Sep 15, 2020
1 parent 6945d8f commit a614783
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 44 deletions.
Expand Up @@ -338,19 +338,18 @@ protected function mapDateTime($value, $storageFormat = null, $targetType = \Dat
{
$dateTimeTypes = QueryHelper::getDateTimeTypes();

// Invalid values are converted to NULL
if (empty($value) || $value === '0000-00-00' || $value === '0000-00-00 00:00:00' || $value === '00:00:00') {
// 0 -> NULL !!!
return null;
}
if (in_array($storageFormat, $dateTimeTypes, true)) {
// native date/datetime/time values are stored in UTC
$utcTimeZone = new \DateTimeZone('UTC');
$utcDateTime = GeneralUtility::makeInstance($targetType, $value, $utcTimeZone);
$currentTimeZone = new \DateTimeZone(date_default_timezone_get());
return $utcDateTime->setTimezone($currentTimeZone);
if (!in_array($storageFormat, $dateTimeTypes, true)) {
// Integer timestamps are also stored "as is" in the database, but are UTC by definition,
// so we convert the timestamp to a ISO representation.
$value = date('c', (int)$value);
}
// integer timestamps are local server time
return GeneralUtility::makeInstance($targetType, date('c', (int)$value));
// All date/datetime/time values are stored in the database "as is", independent of any time zone information.
// It is therefore only important to use the same time zone in PHP when storing and retrieving the values.
return GeneralUtility::makeInstance($targetType, $value);
}

/**
Expand Down Expand Up @@ -736,18 +735,15 @@ public function getPlainValue($input, $columnMap = null)
} elseif ($input instanceof \DateTimeInterface) {
if ($columnMap !== null && $columnMap->getDateTimeStorageFormat() !== null) {
$storageFormat = $columnMap->getDateTimeStorageFormat();
$timeZoneToStore = clone $input;
// set to UTC to store in database
$timeZoneToStore->setTimezone(new \DateTimeZone('UTC'));
switch ($storageFormat) {
case 'datetime':
$parameter = $timeZoneToStore->format('Y-m-d H:i:s');
$parameter = $input->format('Y-m-d H:i:s');
break;
case 'date':
$parameter = $timeZoneToStore->format('Y-m-d');
$parameter = $input->format('Y-m-d');
break;
case 'time':
$parameter = $timeZoneToStore->format('H:i');
$parameter = $input->format('H:i');
break;
default:
throw new \InvalidArgumentException('Column map DateTime format "' . $storageFormat . '" is unknown. Allowed values are date, datetime or time.', 1395353470);
Expand Down
Expand Up @@ -15,7 +15,6 @@

namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Mapper;

use ExtbaseTeam\BlogExample\Domain\Model\Comment;
use ExtbaseTeam\BlogExample\Domain\Model\DateExample;
use ExtbaseTeam\BlogExample\Domain\Model\DateTimeImmutableExample;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
Expand Down Expand Up @@ -59,26 +58,6 @@ protected function setUp(): void
$GLOBALS['BE_USER'] = new BackendUserAuthentication();
}

/**
* @test
*/
public function datetimeObjectsCanBePersistedToDatetimeDatabaseFields()
{
$date = new \DateTime('2016-03-06T12:40:00+01:00');
$comment = new Comment();
$comment->setDate($date);

$this->persistenceManager->add($comment);
$this->persistenceManager->persistAll();
$uid = $this->persistenceManager->getIdentifierByObject($comment);
$this->persistenceManager->clearState();

/** @var Comment $existingComment */
$existingComment = $this->persistenceManager->getObjectByIdentifier($uid, Comment::class);

self::assertEquals($date->getTimestamp(), $existingComment->getDate()->getTimestamp());
}

/**
* @test
*/
Expand Down Expand Up @@ -122,10 +101,10 @@ public function dateValuesAreStoredInUtcInTextDatabaseFields()
/**
* @test
*/
public function dateValuesAreStoredInUtcInDatetimeDatabaseFields()
public function dateValuesAreStoredInLocalTimeInDatetimeDatabaseFields()
{
$example = new DateExample();
$date = new \DateTime('2016-03-06T12:40:00+01:00');
$date = new \DateTime('2016-03-06T12:40:00');
$example->setDatetimeDatetime($date);

$this->persistenceManager->add($example);
Expand Down
Expand Up @@ -361,10 +361,10 @@ public function mapDateTimeHandlesDifferentFieldEvaluationsDataProvider()
return [
'nothing' => [null, null, null],
'timestamp' => [1, null, date('c', 1)],
'empty date' => ['0000-00-00', 'date', null],
'valid date' => ['2013-01-01', 'date', date('c', strtotime('2013-01-01T00:00:00+00:00'))],
'empty datetime' => ['0000-00-00 00:00:00', 'datetime', null],
'valid datetime' => ['2013-01-01 01:02:03', 'datetime', date('c', strtotime('2013-01-01T01:02:03+00:00'))],
'invalid date' => ['0000-00-00', 'date', null],
'valid date' => ['2013-01-01', 'date', date('c', strtotime('2013-01-01 00:00:00'))],
'invalid datetime' => ['0000-00-00 00:00:00', 'datetime', null],
'valid datetime' => ['2013-01-01 01:02:03', 'datetime', date('c', strtotime('2013-01-01 01:02:03'))],
];
}

Expand All @@ -390,6 +390,48 @@ public function mapDateTimeHandlesDifferentFieldEvaluations($value, $storageForm
}
}

/**
* @return array
*/
public function mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZoneDataProvider()
{
return [
'nothing' => [null, null, null],
'timestamp' => [1, null, '@1'],
'invalid date' => ['0000-00-00', 'date', null],
'valid date' => ['2013-01-01', 'date', '2013-01-01T00:00:00'],
'invalid datetime' => ['0000-00-00 00:00:00', 'datetime', null],
'valid datetime' => ['2013-01-01 01:02:03', 'datetime', '2013-01-01T01:02:03'],
];
}

/**
* @param string|int|null $value
* @param string|null $storageFormat
* @param string|null $expectedValue
* @test
* @dataProvider mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZoneDataProvider
*/
public function mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZone($value, ?string $storageFormat, ?string $expectedValue)
{
$originalTimeZone = date_default_timezone_get();
date_default_timezone_set('America/Chicago');
$usedTimeZone = date_default_timezone_get();
/** @var DataMapper|AccessibleObjectInterface|\PHPUnit\Framework\MockObject\MockObject $accessibleDataMapFactory */
$accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);

/** @var $dateTime NULL|\DateTime */
$dateTime = $accessibleDataMapFactory->_call('mapDateTime', $value, $storageFormat);

if ($expectedValue === null) {
self::assertNull($dateTime);
} else {
self::assertEquals(new \DateTime($expectedValue, new \DateTimeZone($usedTimeZone)), $dateTime);
}
// Restore the systems current timezone
date_default_timezone_set($originalTimeZone);
}

/**
* @test
*/
Expand Down Expand Up @@ -417,8 +459,7 @@ public function getPlainValueReturnsCorrectDateTimeFormat()

$columnMap = new ColumnMap('column_name', 'propertyName');
$columnMap->setDateTimeStorageFormat('datetime');
$datetimeAsString = '2013-04-15 09:30:00';
$input = new \DateTime($datetimeAsString, new \DateTimeZone('UTC'));
$input = new \DateTime('2013-04-15 09:30:00');
self::assertEquals('2013-04-15 09:30:00', $subject->getPlainValue($input, $columnMap));
$columnMap->setDateTimeStorageFormat('date');
self::assertEquals('2013-04-15', $subject->getPlainValue($input, $columnMap));
Expand Down

0 comments on commit a614783

Please sign in to comment.