Skip to content

Commit

Permalink
[BUGFIX] Use correct timezones with timestamp based \DateTime
Browse files Browse the repository at this point in the history
As documented by PHP, `\DateTime` or `\DateTimeImmutable`
objects created with a timestamp only ('@12345678') as the
first constructor argument will use UTC as TimeZone.

Creating an object instance without any constructor arguments
will use the `date_default_timezone_get()` timezone for the
object - and setting the timestamp using `setTimestamp()` will
not change the object's timezone.

That means, that formatting the value will be done using
the object Timezone and **NOT** the default PHP Timezone
(`date_default_timezone_get()`).

Consequently, this may lead to wrong interpretation of
the displayed formatted value in the context of another
timezone - if no timezone information (+0000/-0100) is
included.

This change streamlines this for a couple of places were
it is expected to format date, datetime or time values
for the current context timezone by ensuring objects get
the current timezone set instead of `UTC`.

Note: Changing the date default timezone on the fly after
an object has been created will show a similar effect like
using constructor values, even if the object has been
created by using the `setTimestamp()` method.

[1] https://3v4l.org/sGEe2

Resolves: #98045
Resolves: #99627
Releases: main, 12.4, 11.5
Change-Id: I095a0b0b376361e25396647d02727ac08f35cae0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83682
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
  • Loading branch information
liayn authored and sbuerk committed Apr 7, 2024
1 parent 524267c commit 57dcbe8
Show file tree
Hide file tree
Showing 20 changed files with 85 additions and 71 deletions.
5 changes: 0 additions & 5 deletions Build/phpstan/phpstan-baseline.neon
Expand Up @@ -2450,11 +2450,6 @@ parameters:
count: 1
path: ../../typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php

-
message: "#^Negated boolean expression is always false\\.$#"
count: 1
path: ../../typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php

-
message: "#^Constructor of class TYPO3\\\\CMS\\\\Frontend\\\\Plugin\\\\AbstractPlugin has an unused parameter \\$_\\.$#"
count: 1
Expand Down
47 changes: 17 additions & 30 deletions typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php
Expand Up @@ -182,18 +182,16 @@ protected function initializeFrontendPreview(
// Simulate date
$simTime = null;
if ($simulateDate) {
$simTime = $this->parseDate($simulateDate);
if ($simTime) {
$GLOBALS['SIM_EXEC_TIME'] = $simTime;
$GLOBALS['SIM_ACCESS_TIME'] = $simTime - $simTime % 60;
$context->setAspect(
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])
)
);
}
$simTime = max($simulateDate, 60);
$GLOBALS['SIM_EXEC_TIME'] = $simTime;
$GLOBALS['SIM_ACCESS_TIME'] = $simTime - $simTime % 60;
$context->setAspect(
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
(new \DateTimeImmutable())->setTimestamp($simTime)
)
);
}
// simulate usergroup
if ($simulateUserGroup) {
Expand Down Expand Up @@ -230,28 +228,17 @@ public function getJavaScriptFiles(): array
return ['EXT:adminpanel/Resources/Public/JavaScript/modules/preview.js'];
}

/**
* The simulated date needs to be a timestring (UTC)
*
* Simulation date is either set via configuration of AdminPanel (Date and Time Fields) or via ADMCMD_ $_GET
* parameter from backend previews
*/
protected function parseDate(int $simulateDate): ?int
{
try {
$simTime = (new \DateTime('@' . $simulateDate))->getTimestamp();
$simTime = max($simTime, 60);
} catch (\Exception $e) {
$simTime = null;
}
return $simTime;
}

protected function clearPreviewSettings(Context $context): void
{
$GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
$GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
$context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
$context->setAspect(
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
(new \DateTimeImmutable())->setTimestamp($GLOBALS['SIM_EXEC_TIME'])
)
);
$context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
}

Expand Down
7 changes: 6 additions & 1 deletion typo3/sysext/backend/Classes/Http/Application.php
Expand Up @@ -71,7 +71,12 @@ protected function installToolRedirect(ServerRequestInterface $request): Respons
*/
protected function initializeContext(): void
{
$this->context->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])));
$this->context->setAspect(
'date',
new DateTimeAspect(
(new \DateTimeImmutable())->setTimestamp($GLOBALS['EXEC_TIME'])
)
);
$this->context->setAspect('visibility', new VisibilityAspect(true, true));
}
}
7 changes: 5 additions & 2 deletions typo3/sysext/backend/Classes/Routing/PreviewUriBuilder.php
Expand Up @@ -439,7 +439,10 @@ protected function getAdditionalQueryParametersForAccessRestrictedPages(array $p
if ($access['starttime'] > $GLOBALS['EXEC_TIME']) {
// simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
// a URL for it
$dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $access['starttime']));
$dateAspect = GeneralUtility::makeInstance(
DateTimeAspect::class,
(new \DateTimeImmutable())->setTimestamp($access['starttime'])
);
$context->setAspect('date', $dateAspect);
$additionalQueryParameters['ADMCMD_simTime'] = $access['starttime'];
}
Expand All @@ -448,7 +451,7 @@ protected function getAdditionalQueryParametersForAccessRestrictedPages(array $p
// in turn PageRouter will generate a URL for it
$dateAspect = GeneralUtility::makeInstance(
DateTimeAspect::class,
new \DateTimeImmutable('@' . ($access['endtime'] - 1))
(new \DateTimeImmutable())->setTimestamp($access['endtime'] - 1)
);
$context->setAspect('date', $dateAspect);
$additionalQueryParameters['ADMCMD_simTime'] = ($access['endtime'] - 1);
Expand Down
7 changes: 6 additions & 1 deletion typo3/sysext/core/Classes/Console/CommandApplication.php
Expand Up @@ -173,7 +173,12 @@ protected function checkEnvironmentOrDie(): void
*/
protected function initializeContext(): void
{
$this->context->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])));
$this->context->setAspect(
'date',
new DateTimeAspect(
(new \DateTimeImmutable())->setTimestamp($GLOBALS['EXEC_TIME'])
)
);
$this->context->setAspect('visibility', new VisibilityAspect(true, true));
$this->context->setAspect('workspace', new WorkspaceAspect(0));
$this->context->setAspect('backend.user', new UserAspect(null));
Expand Down
7 changes: 6 additions & 1 deletion typo3/sysext/core/Classes/Context/Context.php
Expand Up @@ -101,7 +101,12 @@ public function getAspect(string $name): AspectInterface
// Ensure the default aspects are available, this is mostly necessary for tests to not set up everything
switch ($name) {
case 'date':
$this->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])));
$this->setAspect(
'date',
new DateTimeAspect(
(new \DateTimeImmutable())->setTimestamp($GLOBALS['EXEC_TIME'])
)
);
break;
case 'visibility':
$this->setAspect('visibility', new VisibilityAspect());
Expand Down
4 changes: 2 additions & 2 deletions typo3/sysext/core/Classes/Localization/DateFormatter.php
Expand Up @@ -74,8 +74,8 @@ public function format(mixed $date, string|int $format, string|Locale $locale):
*/
public function strftime(string $format, int|string|\DateTimeInterface|null $timestamp, string|Locale|null $locale = null, $useUtcTimeZone = false): string
{
if (!($timestamp instanceof \DateTimeInterface)) {
$timestamp = is_int($timestamp) ? '@' . $timestamp : (string)$timestamp;
if (!$timestamp instanceof \DateTimeInterface) {
$timestamp = MathUtility::canBeInterpretedAsInteger($timestamp) ? '@' . $timestamp : (string)$timestamp;
try {
$timestamp = new \DateTime($timestamp);
} catch (\Exception $e) {
Expand Down
2 changes: 1 addition & 1 deletion typo3/sysext/core/Classes/Mail/MailMessage.php
Expand Up @@ -91,7 +91,7 @@ public function setSubject($subject): self
*/
public function setDate($date): self
{
return $this->date(new \DateTime('@' . $date));
return $this->date((new \DateTime())->setTimestamp($date));
}

/**
Expand Down
Expand Up @@ -41,8 +41,8 @@ public static function fromArray(array $array): static
new ReportDetails($details ?: []),
$array['summary'] ?? '',
UuidV4::fromString($array['uuid'] ?? ''),
new \DateTimeImmutable('@' . ($array['created'] ?? '0')),
new \DateTimeImmutable('@' . ($array['changed'] ?? '0'))
(new \DateTimeImmutable())->setTimestamp((int)($array['created'] ?? 0)),
(new \DateTimeImmutable())->setTimestamp((int)($array['changed'] ?? 0)),
);
}

Expand Down
Expand Up @@ -49,7 +49,7 @@ public static function fromArray(array $array): static
$array['mutation_identifier'],
$mutationCollection,
$meta ?: [],
new \DateTimeImmutable('@' . ($array['created'] ?? '0')),
(new \DateTimeImmutable())->setTimestamp((int)($array['created'] ?? 0)),
);
}

Expand Down
Expand Up @@ -41,7 +41,7 @@ Build your own :php:`clear_preview` method:
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])
(new \DateTimeImmutable())->setTimestamp($GLOBALS['SIM_EXEC_TIME'])
)
);
$context->setAspect(
Expand Down
16 changes: 16 additions & 0 deletions typo3/sysext/core/Tests/Unit/Context/DateTimeAspectTest.php
Expand Up @@ -53,6 +53,22 @@ public function getTimestampReturnsInteger(): void
self::assertIsInt($timestamp);
}

#[Test]
public function getTimezoneReturnsUtcTimezoneOffsetWhenDateTimeIsInitializedFromUnixTimestamp(): void
{
$dateObject = new \DateTimeImmutable('@12345');
$subject = new DateTimeAspect($dateObject);
self::assertSame('+00:00', $subject->get('timezone'));
}

#[Test]
public function getTimezoneReturnsGivenTimezoneOffsetWhenDateTimeIsInitializedFromIso8601Date(): void
{
$dateObject = new \DateTimeImmutable('2004-02-12T15:19:21+05:00');
$subject = new DateTimeAspect($dateObject);
self::assertSame('+05:00', $subject->get('timezone'));
}

public static function dateFormatValuesDataProvider(): array
{
return [
Expand Down
Expand Up @@ -108,7 +108,7 @@ public function getLastUpdate(): \DateTimeInterface
{
if (file_exists($this->localExtensionListCacheFile) && filesize($this->localExtensionListCacheFile) > 0) {
$mtime = filemtime($this->localExtensionListCacheFile);
return new \DateTimeImmutable('@' . $mtime);
return (new \DateTimeImmutable())->setTimestamp($mtime);
}
// Select a very old date (hint: easter egg)
return new \DateTimeImmutable('1975-04-13');
Expand Down
14 changes: 7 additions & 7 deletions typo3/sysext/fluid/Classes/ViewHelpers/Format/DateViewHelper.php
Expand Up @@ -180,14 +180,14 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl
}

if (!$date instanceof \DateTimeInterface) {
try {
$base = $base instanceof \DateTimeInterface ? (int)$base->format('U') : (int)strtotime((MathUtility::canBeInterpretedAsInteger($base) ? '@' : '') . $base);
$dateTimestamp = strtotime((MathUtility::canBeInterpretedAsInteger($date) ? '@' : '') . $date, $base);
$date = new \DateTime('@' . $dateTimestamp);
$date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
} catch (\Exception $exception) {
throw new Exception('"' . $date . '" could not be parsed by \DateTime constructor: ' . $exception->getMessage(), 1241722579);
$base = $base instanceof \DateTimeInterface
? (int)$base->format('U')
: (int)strtotime((MathUtility::canBeInterpretedAsInteger($base) ? '@' : '') . $base);
$dateTimestamp = strtotime((MathUtility::canBeInterpretedAsInteger($date) ? '@' : '') . $date, $base);
if ($dateTimestamp === false) {
throw new Exception('"' . $date . '" could not be converted to a timestamp. Probably due to a parsing error.', 1241722579);
}
$date = (new \DateTime())->setTimestamp($dateTimestamp);
}

if ($pattern !== null) {
Expand Down
7 changes: 6 additions & 1 deletion typo3/sysext/frontend/Classes/Http/Application.php
Expand Up @@ -72,7 +72,12 @@ protected function installToolRedirect(ServerRequestInterface $request): Respons
*/
protected function initializeContext(): void
{
$this->context->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])));
$this->context->setAspect(
'date',
new DateTimeAspect(
(new \DateTimeImmutable())->setTimestamp($GLOBALS['EXEC_TIME'])
)
);
$this->context->setAspect('visibility', new VisibilityAspect());
$this->context->setAspect('workspace', new WorkspaceAspect(0));
$this->context->setAspect('backend.user', new UserAspect(null));
Expand Down
Expand Up @@ -163,18 +163,13 @@ protected function simulateDate(ServerRequestInterface $request): bool
return false;
}

$simulatedDate = new \DateTimeImmutable('@' . $queryTime);
if (!$simulatedDate) {
return false;
}

$GLOBALS['SIM_EXEC_TIME'] = $queryTime;
$GLOBALS['SIM_ACCESS_TIME'] = $queryTime - $queryTime % 60;
$this->context->setAspect(
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
$simulatedDate
(new \DateTimeImmutable())->setTimestamp($queryTime)
)
);
return true;
Expand Down
2 changes: 1 addition & 1 deletion typo3/sysext/install/Classes/Http/Application.php
Expand Up @@ -52,7 +52,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
*/
protected function initializeContext(): void
{
$this->context->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])));
$this->context->setAspect('date', new DateTimeAspect((new \DateTimeImmutable())->setTimestamp($GLOBALS['EXEC_TIME'])));
$this->context->setAspect('visibility', new VisibilityAspect(true, true, true));
$this->context->setAspect('workspace', new WorkspaceAspect(0));
$this->context->setAspect('backend.user', new UserAspect());
Expand Down
Expand Up @@ -305,7 +305,7 @@ public function setLastUpdatedIsoCode(array $isos)
protected function getFormattedDate($timestamp)
{
if (is_int($timestamp)) {
$date = new \DateTime('@' . $timestamp);
$date = (new \DateTime())->setTimestamp($timestamp);
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
$timestamp = $date->format($format);
}
Expand Down
6 changes: 2 additions & 4 deletions typo3/sysext/redirects/Classes/Repository/Demand.php
Expand Up @@ -117,10 +117,8 @@ public static function fromCommandInput(InputInterface $input): self
$input->hasOption('hitCount') ? (int)$input->getOption('hitCount') : 0,
$input->getOption('days')
? new \DateTimeImmutable($input->getOption('days') . ' days ago')
: new \DateTimeImmutable(
'90 days ago'
),
$input->getOption('creationType') !== null ? (int)($input->getOption('creationType')) : null
: new \DateTimeImmutable('90 days ago'),
$input->hasOption('creationType') ? (int)($input->getOption('creationType')) : null,
);
}

Expand Down
Expand Up @@ -38,6 +38,7 @@
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
use TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand;
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
Expand Down Expand Up @@ -746,14 +747,13 @@ protected function setTaskDataFromRequest(AbstractTask $task, ServerRequestInter
*/
protected function getTimestampFromDateString(string $input): int
{
if (is_numeric($input)) {
if (MathUtility::canBeInterpretedAsInteger($input)) {
// Already looks like a timestamp
return (int)$input;
}
try {
// Convert from ISO 8601 dates
$dateTime = new \DateTime($input);
$value = $dateTime->getTimestamp();
$value = (new \DateTime($input))->getTimestamp();
if ($value !== 0) {
$value -= (int)date('Z', $value);
}
Expand Down

0 comments on commit 57dcbe8

Please sign in to comment.