diff --git a/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php b/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php index 8dc920906031dbd607b2bce8531d4401a7c1ed42..53d2444242c3605760bb42b4f345ca4df2498d94 100644 --- a/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php @@ -22,7 +22,10 @@ use TYPO3\CMS\Backend\Form\NodeFactory; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\DateFormatter; use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Localization\Locale; +use TYPO3\CMS\Core\Localization\Locales; use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -195,8 +198,13 @@ abstract class AbstractFormElement extends AbstractNode $option = isset($formatOptions['option']) ? trim($formatOptions['option']) : ''; if ($option) { if (isset($formatOptions['strftime']) && $formatOptions['strftime']) { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $value = @strftime($option, (int)$itemValue); + $user = $this->getBackendUser(); + if ($user->user['lang'] ?? false) { + $locale = GeneralUtility::makeInstance(Locales::class)->createLocale($user->user['lang']); + } else { + $locale = new Locale(); + } + $value = (new DateFormatter())->strftime($option, (int)$itemValue, $locale); } else { $value = date($option, (int)$itemValue); } diff --git a/typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php b/typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php index ec36959c6575caa98ee78b57a4e7575c8468b7c4..124ff1e16c02e48b06714a3ce997130ca9338965 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Tests\Unit\Form\Element; use TYPO3\CMS\Backend\Form\Element\AbstractFormElement; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class AbstractFormElementTest extends UnitTestCase @@ -270,6 +271,7 @@ class AbstractFormElementTest extends UnitTestCase */ public function formatValueWithGivenConfiguration(array $config, ?string $itemValue, string $expectedResult): void { + $GLOBALS['BE_USER'] = new BackendUserAuthentication(); $subject = $this->getAccessibleMock(AbstractFormElement::class, ['render'], [], '', false); $timezoneBackup = date_default_timezone_get(); date_default_timezone_set('UTC'); diff --git a/typo3/sysext/core/Classes/Localization/DateFormatter.php b/typo3/sysext/core/Classes/Localization/DateFormatter.php index a48268e6b6cf92c7f2051dcc6712028911d507a5..be6fa5f72dd358cb8ee80bcafde271cf3a64926a 100644 --- a/typo3/sysext/core/Classes/Localization/DateFormatter.php +++ b/typo3/sysext/core/Classes/Localization/DateFormatter.php @@ -58,4 +58,196 @@ class DateFormatter } return $dateFormatter->format($date) ?: ''; } + + /** + * Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible) + * This provides a cross-platform alternative to strftime() for when it will be removed from PHP. + * Note that output can be slightly different between libc sprintf and this function as it is using ICU. + * + * Original author BohwaZ <https://bohwaz.net/> + * Adapted from https://github.com/alphp/strftime + * MIT licensed + */ + 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; + try { + $timestamp = new \DateTime($timestamp); + } catch (\Exception $e) { + throw new \InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 1679091446, $e); + } + $timestamp->setTimezone(new \DateTimeZone($useUtcTimeZone ? 'UTC' : date_default_timezone_get())); + } + + if (empty($locale)) { + // get current locale + $locale = setlocale(LC_TIME, '0'); + } else { + $locale = (string)$locale; + } + // remove trailing part not supported by ext-intl locale + $locale = preg_replace('/[^\w-].*$/', '', $locale); + + $intl_formats = [ + '%a' => 'EEE', // An abbreviated textual representation of the day Sun through Sat + '%A' => 'EEEE', // A full textual representation of the day Sunday through Saturday + '%b' => 'MMM', // Abbreviated month name, based on the locale Jan through Dec + '%B' => 'MMMM', // Full month name, based on the locale January through December + '%h' => 'MMM', // Abbreviated month name, based on the locale (an alias of %b) Jan through Dec + ]; + + $intl_formatter = function (\DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale): string { + $tz = $timestamp->getTimezone(); + $date_type = \IntlDateFormatter::FULL; + $time_type = \IntlDateFormatter::FULL; + $pattern = ''; + + switch ($format) { + // %c = Preferred date and time stamp based on locale + // Example: Tue Feb 5 00:45:10 2009 for February 5, 2009 at 12:45:10 AM + case '%c': + $date_type = \IntlDateFormatter::LONG; + $time_type = \IntlDateFormatter::SHORT; + break; + + // %x = Preferred date representation based on locale, without the time + // Example: 02/05/09 for February 5, 2009 + case '%x': + $date_type = \IntlDateFormatter::SHORT; + $time_type = \IntlDateFormatter::NONE; + break; + + // Localized time format + case '%X': + $date_type = \IntlDateFormatter::NONE; + $time_type = \IntlDateFormatter::MEDIUM; + break; + + default: + $pattern = $intl_formats[$format]; + } + + // In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and + // the 4th October was followed by the 15th October. + // ICU (including IntlDateFormattter) interprets and formats dates based on this cutover. + // Posix (including strftime) and timelib (including DateTimeImmutable) instead use + // a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever. + // This leads to the same instants in time, as expressed in Unix time, having different representations + // in formatted strings. + // To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past. + $calendar = \IntlGregorianCalendar::createInstance(); + $calendar->setGregorianChange(PHP_INT_MIN); + + return (new \IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp) ?: ''; + }; + + // Same order as https://www.php.net/manual/en/function.strftime.php + $translation_table = [ + // Day + '%a' => $intl_formatter, + '%A' => $intl_formatter, + '%d' => 'd', + '%e' => function (\DateTimeInterface $timestamp, string $_): string { + return sprintf('% 2u', $timestamp->format('j')); + }, + '%j' => function (\DateTimeInterface $timestamp, string $_): string { + // Day number in year, 001 to 366 + return sprintf('%03d', (int)($timestamp->format('z'))+1); + }, + '%u' => 'N', + '%w' => 'w', + + // Week + '%U' => function (\DateTimeInterface $timestamp, string $_): string { + // Number of weeks between date and first Sunday of year + $day = new \DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y'))); + return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); + }, + '%V' => 'W', + '%W' => function (\DateTimeInterface $timestamp, string $_): string { + // Number of weeks between date and first Monday of year + $day = new \DateTime(sprintf('%d-01 Monday', $timestamp->format('Y'))); + return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); + }, + + // Month + '%b' => $intl_formatter, + '%B' => $intl_formatter, + '%h' => $intl_formatter, + '%m' => 'm', + + // Year + '%C' => function (\DateTimeInterface $timestamp, string $_): string { + // Century (-1): 19 for 20th century + return (string)floor($timestamp->format('Y') / 100); + }, + '%g' => function (\DateTimeInterface $timestamp, string $_): string { + return substr($timestamp->format('o'), -2); + }, + '%G' => 'o', + '%y' => 'y', + '%Y' => 'Y', + + // Time + '%H' => 'H', + '%k' => function (\DateTimeInterface $timestamp, string $_): string { + return sprintf('% 2u', $timestamp->format('G')); + }, + '%I' => 'h', + '%l' => function (\DateTimeInterface $timestamp, string $_): string { + return sprintf('% 2u', $timestamp->format('g')); + }, + '%M' => 'i', + '%p' => 'A', // AM PM (this is reversed on purpose!) + '%P' => 'a', // am pm + '%r' => 'h:i:s A', // %I:%M:%S %p + '%R' => 'H:i', // %H:%M + '%S' => 's', + '%T' => 'H:i:s', // %H:%M:%S + '%X' => $intl_formatter, // Preferred time representation based on locale, without the date + + // Timezone + '%z' => 'O', + '%Z' => 'T', + + // Time and Date Stamps + '%c' => $intl_formatter, + '%D' => 'm/d/Y', + '%F' => 'Y-m-d', + '%s' => 'U', + '%x' => $intl_formatter, + ]; + + $out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) { + $prefix = $match[1]; + $char = $match[2]; + $pattern = '%' . $char; + if ($pattern == '%n') { + return "\n"; + } + if ($pattern == '%t') { + return "\t"; + } + + if (!isset($translation_table[$pattern])) { + throw new \InvalidArgumentException(sprintf('Format "%s" is unknown in time format', $pattern), 1679091475); + } + + $replace = $translation_table[$pattern]; + + if (is_string($replace)) { + $result = $timestamp->format($replace); + } else { + $result = $replace($timestamp, $pattern); + } + + return match ($prefix) { + '_' => preg_replace('/\G0(?=.)/', ' ', $result), + '#', '-' => preg_replace('/^0+(?=.)/', '', $result), + default => $result, + }; + }, $format); + return str_replace('%%', '%', $out); + } } diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/RelationTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/RelationTest.php index a02f23e4b33da11cd8604b8977719900679fd2ca..c0ac612e36619f1f94aa2e6c978984e4d92f711d 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/RelationTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/RelationTest.php @@ -555,8 +555,7 @@ class RelationTest extends FunctionalTestCase $tags = clone $post->getTags(); $post->setTags(new ObjectStorage()); - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $newTag = new Tag('INSERTED TAG at position 6 : ' . @strftime('')); + $newTag = new Tag('INSERTED TAG at position 6 : '); $counter = 1; foreach ($tags as $tag) { diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Format/DateViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Format/DateViewHelper.php index 462baa0bbb3ce4e58ad47f661118b5495f5850b3..3fc3fdd852ade70353d0a8c1780f089bb71f5fd6 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Format/DateViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Format/DateViewHelper.php @@ -195,8 +195,9 @@ final class DateViewHelper extends AbstractViewHelper return (new DateFormatter())->format($date, $pattern, $locale); } if (str_contains($format, '%')) { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - return @strftime($format, (int)$date->format('U')); + // @todo: deprecate this syntax in TYPO3 v13. + $locale = $arguments['locale'] ?? self::resolveLocale($renderingContext); + return (new DateFormatter())->format($date, $format, $locale); } return $date->format($format); } diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php index 8d64ec2f4decd814a3e1638a7606cca137267a60..abb029fd17aa954ae2888c41b4b1319b97f2f051 100644 --- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php +++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php @@ -1871,15 +1871,14 @@ class ContentObjectRenderer implements LoggerAwareInterface * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for strftime. * @return string The processed input value - * @todo @todo Replace deprecated strftime/gmstrftime and whole method in php 8.1. Suppress warning in v11. */ public function stdWrap_strftime($content = '', $conf = []) { // Check for zero length string to mimic default case of strtime/gmstrftime $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content; $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT']) - ? @gmstrftime($conf['strftime'] ?? null, $content) - : @strftime($conf['strftime'] ?? null, $content); + ? (new DateFormatter())->strftime($conf['strftime'] ?? '', $content, null, true) + : (new DateFormatter())->strftime($conf['strftime'] ?? '', $content); if (!empty($conf['strftime.']['charset'])) { $output = mb_convert_encoding((string)$content, 'utf-8', trim(strtolower($conf['strftime.']['charset']))); return $output ?: $content; diff --git a/typo3/sysext/impexp/Classes/Export.php b/typo3/sysext/impexp/Classes/Export.php index 973413c2e7d3f1e6e2dbb4f75b47814f9a98f2b0..2f518959ef40db9a024e68912e98aa9322252ac4 100644 --- a/typo3/sysext/impexp/Classes/Export.php +++ b/typo3/sysext/impexp/Classes/Export.php @@ -29,6 +29,9 @@ use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Exception; use TYPO3\CMS\Core\Html\HtmlParser; use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\CMS\Core\Localization\DateFormatter; +use TYPO3\CMS\Core\Localization\Locale; +use TYPO3\CMS\Core\Localization\Locales; use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\Folder; @@ -314,6 +317,12 @@ class Export extends ImportExport */ protected function setMetaData(): void { + $user = $this->getBackendUser(); + if ($user->user['lang'] ?? false) { + $locale = GeneralUtility::makeInstance(Locales::class)->createLocale($user->user['lang']); + } else { + $locale = new Locale(); + } $this->dat['header']['meta'] = [ 'title' => $this->title, 'description' => $this->description, @@ -322,8 +331,7 @@ class Export extends ImportExport 'packager_name' => $this->getBackendUser()->user['realName'], 'packager_email' => $this->getBackendUser()->user['email'], 'TYPO3_version' => (string)GeneralUtility::makeInstance(Typo3Version::class), - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - 'created' => @strftime('%A %e. %B %Y', $GLOBALS['EXEC_TIME']), + 'created' => (new DateFormatter())->format($GLOBALS['EXEC_TIME'], 'EEE d. MMMM y', $locale), ]; } diff --git a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php index fe1014e76377755c6f9f776e3def88c37fcc5897..34cf2e48a89a9f8b3b0c0ee5d1dfa885c9f4dfdc 100644 --- a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php +++ b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php @@ -38,7 +38,10 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\DateFormatter; use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Localization\Locale; +use TYPO3\CMS\Core\Localization\Locales; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageRendererResolver; use TYPO3\CMS\Core\Messaging\FlashMessageService; @@ -861,6 +864,12 @@ class DatabaseIntegrityController { $out = ''; $fields = []; + $user = $this->getBackendUserAuthentication(); + if ($user->user['lang'] ?? false) { + $locale = GeneralUtility::makeInstance(Locales::class)->createLocale($user->user['lang']); + } else { + $locale = new Locale(); + } // Analysing the fields in the table. if (is_array($GLOBALS['TCA'][$table] ?? null)) { $fC = $GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null; @@ -945,18 +954,17 @@ class DatabaseIntegrityController switch ($fields['type']) { case 'date': if ($fieldValue != -1) { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%d-%m-%Y', (int)$fieldValue); + $formatter = new DateFormatter(); + $out = $formatter->format((int)$fieldValue, 'SHORTDATE', $locale); } break; case 'time': if ($fieldValue != -1) { + $formatter = new DateFormatter(); if ($splitString === '<br />') { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%H:%M' . $splitString . '%d-%m-%Y', (int)$fieldValue); + $out = $formatter->format((int)$fieldValue, 'HH:mm\'' . $splitString . '\'dd-MM-yyyy', $locale); } else { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%H:%M %d-%m-%Y', (int)$fieldValue); + $out = $formatter->format((int)$fieldValue, 'HH:mm dd-MM-yyyy', $locale); } } break;