From a7c21560a5f16afb981bfb74547c0a949dd266b5 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 00:24:18 -0400 Subject: [PATCH 01/13] Update modFile and modFileHandler Replace strftime with modManagerDateFormatter --- core/src/Revolution/File/modFile.php | 8 ++++---- core/src/Revolution/File/modFileHandler.php | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/src/Revolution/File/modFile.php b/core/src/Revolution/File/modFile.php index 8f96776ffc3..6b0d7c84d27 100644 --- a/core/src/Revolution/File/modFile.php +++ b/core/src/Revolution/File/modFile.php @@ -188,9 +188,9 @@ public function getSize() * * @return string The formatted time */ - public function getLastAccessed($timeFormat = '%b %d, %Y %I:%M:%S %p') + public function getLastAccessed($timeFormat = 'M d, Y h:i:s A') { - return strftime($timeFormat, fileatime($this->path)); + return $this->fileHandler->formatter->format(fileatime($this->path), $timeFormat); } /** @@ -200,9 +200,9 @@ public function getLastAccessed($timeFormat = '%b %d, %Y %I:%M:%S %p') * * @return string The formatted time */ - public function getLastModified($timeFormat = '%b %d, %Y %I:%M:%S %p') + public function getLastModified($timeFormat = 'M d, Y h:i:s A') { - return strftime($timeFormat, filemtime($this->path)); + return $this->fileHandler->formatter->format(filemtime($this->path), $timeFormat); } /** diff --git a/core/src/Revolution/File/modFileHandler.php b/core/src/Revolution/File/modFileHandler.php index b2ea85028a6..dfd6c1bb0f6 100644 --- a/core/src/Revolution/File/modFileHandler.php +++ b/core/src/Revolution/File/modFileHandler.php @@ -10,7 +10,7 @@ namespace MODX\Revolution\File; - +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modContext; use MODX\Revolution\modX; @@ -30,6 +30,11 @@ class modFileHandler { * @var modContext|null $context */ public $context = null; + /** + * Manager date formatter + * @var modManagerDateFormatter $formatter + */ + public $formatter = null; /** * The constructor for the modFileHandler class @@ -44,6 +49,7 @@ function __construct(modX &$modx, array $config = []) { $this->config['context'] = $this->modx->context->get('key'); } $this->context = $this->modx->getContext($this->config['context']); + $this->formatter = new modManagerDateFormatter($this->modx); } /** From ad41fc3bcbd4016c743884dfddc4e8e040fffb9e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 00:38:25 -0400 Subject: [PATCH 02/13] Format modFile and modFileHandler Code quality fixes --- core/src/Revolution/File/modFile.php | 3 +-- core/src/Revolution/File/modFileHandler.php | 28 ++++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/src/Revolution/File/modFile.php b/core/src/Revolution/File/modFile.php index 6b0d7c84d27..3472a791d88 100644 --- a/core/src/Revolution/File/modFile.php +++ b/core/src/Revolution/File/modFile.php @@ -1,4 +1,5 @@ fileHandler->modx->getService('archive', 'compression.xPDOZip', XPDO_CORE_PATH, $this->path)) { diff --git a/core/src/Revolution/File/modFileHandler.php b/core/src/Revolution/File/modFileHandler.php index dfd6c1bb0f6..2e3f1a51cbe 100644 --- a/core/src/Revolution/File/modFileHandler.php +++ b/core/src/Revolution/File/modFileHandler.php @@ -1,4 +1,5 @@ modx =& $modx; $this->config = array_merge($this->config, $this->modx->_userConfig, $config); if (!isset($this->config['context'])) { @@ -63,7 +66,8 @@ function __construct(modX &$modx, array $config = []) { * of the object as the specified class. * @return mixed The appropriate modFile/modDirectory object */ - public function make($path, array $options = [], $overrideClass = '') { + public function make($path, array $options = [], $overrideClass = '') + { $path = $this->sanitizePath($path); if (!empty($overrideClass)) { @@ -85,7 +89,8 @@ public function make($path, array $options = [], $overrideClass = '') { * * @return string The base path */ - public function getBasePath() { + public function getBasePath() + { $basePath = ''; /* expand placeholders */ @@ -106,7 +111,8 @@ public function getBasePath() { * * @return string The base URL */ - public function getBaseUrl() { + public function getBaseUrl() + { $baseUrl = ''; /* expand placeholders */ @@ -128,7 +134,8 @@ public function getBaseUrl() { * @param string $path The path to clean * @return string The sanitized path */ - public function sanitizePath($path) { + public function sanitizePath($path) + { return preg_replace(["/\.*[\/|\\\]/i", "/[\/|\\\]+/i"], ['/', '/'], $path); } @@ -138,7 +145,8 @@ public function sanitizePath($path) { * @param string $path * @return string The postfixed path */ - public function postfixSlash($path) { + public function postfixSlash($path) + { $len = strlen($path); if (substr($path, $len - 1, $len) != '/') { $path .= '/'; @@ -152,7 +160,8 @@ public function postfixSlash($path) { * @param string $fileName The path for a file * @return string The directory path of the given file */ - public function getDirectoryFromFile($fileName) { + public function getDirectoryFromFile($fileName) + { $dir = dirname($fileName); return $this->postfixSlash($dir); } @@ -163,7 +172,8 @@ public function getDirectoryFromFile($fileName) { * @param string $file * @return boolean True if a binary file. */ - public function isBinary($file) { + public function isBinary($file) + { if (!file_exists($file) || !is_file($file)) { return false; } From 351a6d66db27de98f4e95e789dcd019d611d88ca Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 00:40:57 -0400 Subject: [PATCH 03/13] Update modFile.php Update doc blocks, ref datetime instead of strftime format --- core/src/Revolution/File/modFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/Revolution/File/modFile.php b/core/src/Revolution/File/modFile.php index 3472a791d88..9f3d73034eb 100644 --- a/core/src/Revolution/File/modFile.php +++ b/core/src/Revolution/File/modFile.php @@ -183,7 +183,7 @@ public function getSize() /** * Gets the last accessed time of the file * - * @param string $timeFormat The format, in strftime format, of the time + * @param string $timeFormat The format, in datetime format, of the time * * @return string The formatted time */ @@ -195,7 +195,7 @@ public function getLastAccessed($timeFormat = 'M d, Y h:i:s A') /** * Gets the last modified time of the file * - * @param string $timeFormat The format, in strftime format, of the time + * @param string $timeFormat The format, in datetime format, of the time * * @return string The formatted time */ From 012b1b66ec8050eab93bcb9f448c42ca965ef1b8 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 13:08:42 -0400 Subject: [PATCH 04/13] Update modFileHandler.php Make how formatter is instantiated consistent with other usages --- core/src/Revolution/File/modFileHandler.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/src/Revolution/File/modFileHandler.php b/core/src/Revolution/File/modFileHandler.php index 2e3f1a51cbe..3134fcf082e 100644 --- a/core/src/Revolution/File/modFileHandler.php +++ b/core/src/Revolution/File/modFileHandler.php @@ -32,11 +32,8 @@ class modFileHandler * @var modContext|null $context */ public $context = null; - /** - * Manager date formatter - * @var modManagerDateFormatter $formatter - */ - public $formatter = null; + + protected modManagerDateFormatter $formatter; /** * The constructor for the modFileHandler class @@ -52,7 +49,7 @@ public function __construct(modX &$modx, array $config = []) $this->config['context'] = $this->modx->context->get('key'); } $this->context = $this->modx->getContext($this->config['context']); - $this->formatter = new modManagerDateFormatter($this->modx); + $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); } /** From 97c16a525f2df010a48f58058ecab2b925b489d4 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 13:10:10 -0400 Subject: [PATCH 05/13] Update data.class.php Replace strftime with formatter --- .../controllers/default/resource/data.class.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/manager/controllers/default/resource/data.class.php b/manager/controllers/default/resource/data.class.php index 887fb1965c2..7e468bc0f3a 100644 --- a/manager/controllers/default/resource/data.class.php +++ b/manager/controllers/default/resource/data.class.php @@ -9,6 +9,7 @@ * files found in the top-level directory of this distribution. */ +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modResource; use xPDO\xPDO; use xPDO\Cache\xPDOCacheManager; @@ -28,6 +29,7 @@ class ResourceDataManagerController extends ResourceManagerController /** @var string $previewUrl */ public $previewUrl; + private modManagerDateFormatter $formatter; /** * Check for any permissions or requirements to load page @@ -78,6 +80,7 @@ public function loadCustomCssJs() */ public function process(array $scriptProperties = []) { + $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); $placeholders = []; $id = (int)$this->scriptProperties['id']; @@ -96,8 +99,16 @@ public function process(array $scriptProperties = []) $this->resource->getOne('Template'); $server_offset_time = intval($this->modx->getOption('server_offset_time', null, 0)); - $this->resource->set('createdon_adjusted', strftime('%c', $this->resource->get('createdon') + $server_offset_time)); - $this->resource->set('editedon_adjusted', strftime('%c', $this->resource->get('editedon') + $server_offset_time)); + $this->resource->set('createdon_adjusted', $this->formatter->formatResourceDate( + $this->resource->get('createdon'), + 'created', + false + )); + $this->resource->set('editedon_adjusted', $this->formatter->formatResourceDate( + $this->resource->get('editedon'), + 'edited', + false + )); $this->resource->_contextKey = $this->resource->get('context_key'); $buffer = $this->modx->cacheManager->get($this->resource->getCacheKey(), [ From 0085c5aca0ecad57c657dcbcb232bc32c843c383 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 16:28:31 -0400 Subject: [PATCH 06/13] Update modTransportProvider.php Replace strftime with formatter --- core/src/Revolution/Transport/modTransportProvider.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/Revolution/Transport/modTransportProvider.php b/core/src/Revolution/Transport/modTransportProvider.php index ccd5a804267..51556b3830d 100644 --- a/core/src/Revolution/Transport/modTransportProvider.php +++ b/core/src/Revolution/Transport/modTransportProvider.php @@ -2,6 +2,7 @@ namespace MODX\Revolution\Transport; +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modX; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; @@ -334,6 +335,8 @@ public function transfer($signature, $target = null, array $args = []) */ public function find(array $search = []) { + $formatter = $this->xpdo->services->get(modManagerDateFormatter::class); + $results = []; $where = array_merge([ @@ -342,7 +345,6 @@ public function find(array $search = []) 'sorter' => false, 'start' => 0, 'limit' => 10, - 'dateFormat' => '%b %d, %Y', 'supportsSeparator' => ', ', ], $search); $where['page'] = !empty($where['start']) ? round($where['start'] / $where['limit']) : 0; @@ -364,7 +366,7 @@ public function find(array $search = []) $installed = $this->xpdo->getObject(modTransportPackage::class, (string)$package->signature); $versionCompiled = rtrim((string)$package->version . '-' . (string)$package->release, '-'); - $releasedon = strftime($this->arg('dateFormat', $where), strtotime((string)$package->releasedon)); + $releasedon = $formatter->formatDate(strtotime((string)$package->releasedon)); $supports = ''; foreach ($package->supports as $support) { From b5734539c35979e4057435b5f0ce817bdcd9caf9 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 May 2025 16:30:42 -0400 Subject: [PATCH 07/13] Update modTransportProvider.php Code style/quality fixes --- .../Revolution/Transport/modTransportProvider.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/Revolution/Transport/modTransportProvider.php b/core/src/Revolution/Transport/modTransportProvider.php index 51556b3830d..516c0364782 100644 --- a/core/src/Revolution/Transport/modTransportProvider.php +++ b/core/src/Revolution/Transport/modTransportProvider.php @@ -152,7 +152,7 @@ public function stats(array $args = []) 'url' => (string)$xml->url, 'id' => (string)$package->id, 'name' => (string)$package->name, - 'downloads' => number_format((integer)$package->downloads, 0), + 'downloads' => number_format((int)$package->downloads, 0), ]; } /** @var SimpleXMLElement $package */ @@ -384,13 +384,13 @@ public function find(array $search = []) 'createdon' => (string)$package->createdon, 'editedon' => (string)$package->editedon, 'name' => (string)$package->name, - 'downloads' => number_format((integer)$package->downloads, 0), + 'downloads' => number_format((int)$package->downloads, 0), 'releasedon' => $releasedon, 'screenshot' => (string)$package->screenshot, 'thumbnail' => !empty($package->thumbnail) ? (string)$package->thumbnail : (string)$package->screenshot, 'license' => (string)$package->license, 'minimum_supports' => (string)$package->minimum_supports, - 'breaks_at' => (integer)$package->breaks_at != 10000000 ? (string)$package->breaks_at : '', + 'breaks_at' => (int)$package->breaks_at != 10000000 ? (string)$package->breaks_at : '', 'supports_db' => (string)$package->supports_db, 'location' => (string)$package->location, 'version-compiled' => $versionCompiled, @@ -476,8 +476,11 @@ protected function args(array $args = []) 'supports' => $this->xpdo->version['code_name'] . '-' . $this->xpdo->version['full_version'], 'http_host' => $this->xpdo->getOption('http_host'), 'php_version' => PHP_VERSION, - 'language' => $this->xpdo->getOption('manager_language', $_SESSION, - $this->xpdo->getOption('cultureKey', null, 'en')), + 'language' => $this->xpdo->getOption( + 'manager_language', + $_SESSION, + $this->xpdo->getOption('cultureKey', null, 'en') + ), ]; return array_merge($baseArgs, $args); @@ -514,7 +517,7 @@ public function request(string $path, string $method = 'GET', array $params = [] // Add params to the body if this is a POST request if ($method === 'POST') { - $request = $request->withHeader('Content-Type','application/x-www-form-urlencoded'); + $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write(http_build_query($params)); } From 1133352c3fd3028201d3245648682854cf226d8e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 31 May 2025 16:12:26 -0400 Subject: [PATCH 08/13] TV-related updates Replace strftime with formatter and add backward compat support for existing TVs using strftime formatting strings (new formatter static method to convert these to their datetime equivalents) --- core/lexicon/en/tv_widget.inc.php | 2 +- .../Formatter/modManagerDateFormatter.php | 70 +++++++++++++++++++ .../Configs/GetInputPropertyConfigs.php | 51 +++++++++----- .../Renders/web/output/date.class.php | 6 +- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/core/lexicon/en/tv_widget.inc.php b/core/lexicon/en/tv_widget.inc.php index ca16427662f..79abff2a150 100644 --- a/core/lexicon/en/tv_widget.inc.php +++ b/core/lexicon/en/tv_widget.inc.php @@ -48,7 +48,7 @@ $_lang['combo_typeahead_delay_desc'] = 'Milliseconds before a matched option is shown. (Default: 250)'; $_lang['date'] = 'Date'; $_lang['date_format'] = 'Date Format'; -$_lang['date_format_desc'] = 'Enter a format using php’s strftime syntax. +$_lang['date_format_desc'] = 'Enter a format using php’s datetime syntax.
Common examples include:
  • [[+example_1a]] ([[+example_1b]]) (default format)
  • diff --git a/core/src/Revolution/Formatter/modManagerDateFormatter.php b/core/src/Revolution/Formatter/modManagerDateFormatter.php index b89a0fa421a..70ab6f7f6d8 100644 --- a/core/src/Revolution/Formatter/modManagerDateFormatter.php +++ b/core/src/Revolution/Formatter/modManagerDateFormatter.php @@ -111,6 +111,71 @@ protected function parseValue($value, bool $useOffset = false): ?int return $value + $offset; } + /** + * Convert from one date formatting syntax to another + * @param string $format The full formatting string to convert + * @param string $from The current syntax used in $format + * @param string $to The target syntax + * @return string The converted formatting string + */ + public static function convertDateFormat(string &$format, string $from = 'strftime', string $to = 'datetime'): string + { + $format = trim($format); + $strftimeToDatetimeMap = [ + '%a' => 'D', + '%A' => 'l', + '%d' => 'd', + '%e' => 'j', + '%j' => 'z', // 001 to 366 => 0 to 365 + '%u' => 'N', + '%w' => 'w', + '%U' => 'W', // general match, see strftime + '%V' => 'W', // general match, see strftime + '%W' => 'W', + '%b' => 'M', + '%h' => 'M', // general match, %h is localized version of %b + '%B' => 'F', + '%m' => 'm', + '%C' => '**', // 2-digit century, no datetime equivalent + '%g' => 'y', // general match, see strftime + '%G' => 'Y', // general match, see strftime + '%y' => 'y', + '%Y' => 'Y', + '%H' => 'H', + '%k' => 'G', + '%I' => 'h', + '%l' => 'g', + '%M' => 'i', + '%p' => 'A', + '%P' => 'a', + '%S' => 's', + '%z' => 'Z', + '%Z' => 'T', + '%s' => 'U', + // compound formats + '%r' => 'h:i:s A', + '%R' => 'H:i', + '%T' => 'H:i:s', + '%X' => 'h:i:s', // locale unsupported in datetime, see strftime + '%c' => 'c', // locale unsupported in datetime, see strftime + '%D' => 'm/d/y', + '%F' => 'Y-m-d', + '%x' => 'm/d/y', // locale unsupported in datetime, see strftime + // characters + '%n' => ' ', // newline, \n only works within double quoted string + '%t' => ' ', // tab, \t only works within double quoted string + '%%' => '%' + ]; + $map = $strftimeToDatetimeMap; + + if ($from === 'strftime' && preg_match_all('/%[\w]/', $format, $parts, PREG_PATTERN_ORDER)) { + foreach ($parts[0] as $part) { + $format = str_replace($part, $map[$part], $format); + } + } + return $format; + } + /** * Transforms a date/time-related value using the specified DateTime format * @param string|int $value The value to transform (a Unix timestamp or mysql-format string) @@ -127,6 +192,11 @@ public function format($value, string $format, bool $useOffset = false, ?string return $emptyValue === null ? $this->managerDateEmptyDisplay : $emptyValue; } + // For now, only strftime to datetime is anticipated + if (strpos($format, '%') !== false) { + self::convertDateFormat($format); + } + return date($format, $value); } diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php b/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php index 0272710ee87..c94b47f7417 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php @@ -11,6 +11,7 @@ namespace MODX\Revolution\Processors\Element\TemplateVar\Configs; +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modNamespace; use MODX\Revolution\Processors\Processor; use MODX\Revolution\modTemplateVar; @@ -29,7 +30,6 @@ */ class GetInputPropertyConfigs extends Processor { - public $propertiesKey = 'input_properties'; public $configDirectory = 'inputproperties'; public $onPropertiesListEvent = 'OnTVInputPropertiesList'; @@ -70,6 +70,8 @@ public function setHelpContent(array $fieldKeys, bool $expandHelp) private function setExampleData() { /* Date example */ + $now = time(); + $formatter = $this->modx->services->get(modManagerDateFormatter::class); $formatDefault = 'Y-m-d'; $formatCurrent = $this->modx->getOption('manager_date_format'); $seps = '-/. '; @@ -85,6 +87,10 @@ private function setExampleData() $timestampAheadAlt = strtotime('+3 months 8 days'); $nextYear = date('Y') + 1; + /* + Some usages of date kept here (instead of using the $formatter), + as evenutal localization of these values would have no effect + */ $this->exampleData['disabled_dates_desc'] = [ 'format_current' => date($formatCurrent), 'format_default' => date($formatDefault), @@ -92,31 +98,40 @@ private function setExampleData() ',' . date($formatDefault, strtotime("+7 days")), 'example_2a' => date($formatWithoutYear, $timestampAheadOneMonth) . ',' . date($formatWithoutYear, $timestampAheadAlt), - 'example_2b' => date("F jS", $timestampAheadOneMonth), - 'example_2c' => date("F jS", $timestampAheadAlt), + 'example_2b' => $formatter->format($timestampAheadOneMonth, 'F jS'), + 'example_2c' => $formatter->format($timestampAheadAlt, 'F jS'), 'example_3a' => '^' . date("Y"), 'example_3b' => date("Y"), 'example_4a' => date($formatRegexAllDays, $timestampAheadOneMonth), - 'example_4b' => date("F Y", $timestampAheadOneMonth), + 'example_4b' => $formatter->format($timestampAheadOneMonth, 'F Y'), 'example_5' => '03-..$', 'example_6a' => $nextYear . '.03.15', 'example_6b' => $nextYear . '\\\.03\\\.15' ]; + + $format1 = 'l, d F Y'; + $format2 = 'D, M j, Y'; + $format3 = 'm/d/Y'; + $format4 = 'Y-m-d'; + $format5 = 'Y-m-d H:i:s'; + $format6 = 'M j, Y'; + $format7 = 'j M Y g:i A'; + $this->exampleData['date_format_desc'] = [ - 'example_1a' => '%A %d, %B %Y', - 'example_1b' => strftime('%A %d, %B %Y'), - 'example_2a' => '%a, %b %e, %Y', - 'example_2b' => strftime('%a, %b %e, %Y'), - 'example_3a' => '%m/%d/%Y', - 'example_3b' => strftime('%m/%d/%Y'), - 'example_4a' => '%Y-%m-%d', - 'example_4b' => strftime('%Y-%m-%d'), - 'example_5a' => '%Y-%m-%d %T', - 'example_5b' => strftime('%Y-%m-%d %T'), - 'example_6a' => '%b %e, %Y', - 'example_6b' => strftime('%b %e, %Y'), - 'example_7a' => '%e %h %Y %l:%M %p', - 'example_7b' => strftime('%e %h %Y %l:%M %p') + 'example_1a' => $format1, + 'example_1b' => $formatter->format($now, $format1), + 'example_2a' => $format2, + 'example_2b' => $formatter->format($now, $format2), + 'example_3a' => $format3, + 'example_3b' => $formatter->format($now, $format3), + 'example_4a' => $format4, + 'example_4b' => $formatter->format($now, $format4), + 'example_5a' => $format5, + 'example_5b' => $formatter->format($now, $format5), + 'example_6a' => $format6, + 'example_6b' => $formatter->format($now, $format6), + 'example_7a' => $format7, + 'example_7b' => $formatter->format($now, $format7) ]; /* Resource list example */ diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php index 12b170e3f32..86eac64128f 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php @@ -8,6 +8,7 @@ * files found in the top-level directory of this distribution. */ +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modTemplateVarOutputRender; /** @@ -20,8 +21,9 @@ */ class modTemplateVarOutputRenderDate extends modTemplateVarOutputRender { public function process($value,array $params = []) { + $formatter = $this->modx->services->get(modManagerDateFormatter::class); /* default properties */ - $params['format'] = !empty($params['format']) ? $params['format'] : "%A %d, %B %Y"; + $params['format'] = !empty($params['format']) ? $params['format'] : 'l, d F Y'; /* fix for 2.0.0-pl bug where 1=yes and 0=no */ $params['default'] = !empty($params['default']) && in_array($params['default'], ['yes',1,'1']) ? 1 : 0; @@ -38,7 +40,7 @@ public function process($value,array $params = []) { } /* return formatted time */ - return strftime($params['format'],$timestamp); + return $formatter->format($timestamp, $params['format']); } } return 'modTemplateVarOutputRenderDate'; From 505f0c46fd95fd07e30dd1cf14485da57f17aad7 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 31 May 2025 22:53:05 -0400 Subject: [PATCH 09/13] Update date.class.php Code style/quality fixes --- .../Renders/web/output/date.class.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php index 86eac64128f..1c3f02b596d 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php @@ -1,4 +1,5 @@ modx->services->get(modManagerDateFormatter::class); /* default properties */ $params['format'] = !empty($params['format']) ? $params['format'] : 'l, d F Y'; /* fix for 2.0.0-pl bug where 1=yes and 0=no */ $params['default'] = !empty($params['default']) && in_array($params['default'], ['yes',1,'1']) ? 1 : 0; - $value= $this->tv->parseInput($value); + $value = $this->tv->parseInput($value); /* if not using current time and no value, return */ - if (empty($value) && empty($params['default'])) return ''; - + if (empty($value) && empty($params['default'])) { + return ''; + } /* if using current, and value empty, get current time */ if (!empty($params['default']) && empty($value)) { $timestamp = time(); } else { /* otherwise get timestamp */ - $timestamp= strtotime($value); + $timestamp = strtotime($value); } /* return formatted time */ From 90d160fb8e6257ca935040292e14b0e094fd58f2 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 31 May 2025 23:23:49 -0400 Subject: [PATCH 10/13] TV-related follow up Fix default format in date and update format conflict prevention between string and date output filter types --- .../Element/TemplateVar/Configs/mgr/properties/date.php | 4 ++-- .../Element/TemplateVar/Configs/mgr/properties/string.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/date.php b/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/date.php index c8a2c722b05..9cb1b644bee 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/date.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/date.php @@ -15,14 +15,14 @@ # Set values $useDefault = $params['default'] === 'true' || $params['default'] === 1 ? 'true' : 'false' ; -$defaultFormat = "'%A %d, %B %Y'"; +$defaultFormat = "'l, d F Y'"; $format = !empty($params['format']) ? json_encode($params['format']) : $defaultFormat ; /* The date and string output properties share the same 'format' parameter, which is problematic when switching between the two; reset to the default value in this case. */ -$format = strpos($format, '%') === false ? $defaultFormat : $format ; +$format = in_array($params['format'], ['Upper Case', 'Lower Case', 'Sentence Case', 'Capitalize']) ? $defaultFormat : $format ; # Set help descriptions $descKeys = [ diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/string.php b/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/string.php index 610683e2026..2fe68702f2f 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/string.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Configs/mgr/properties/string.php @@ -20,7 +20,7 @@ problematic when switching between the two; reset to the default value in this case. */ -$format = strpos($format, '%') !== false ? "''" : $format ; +$format = !in_array($params['format'], ['Upper Case', 'Lower Case', 'Sentence Case', 'Capitalize']) ? "''" : $format ; # Set help descriptions $descKeys = [ From 6c43562e1c3d095f8583df654d890e4f1368864e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 31 May 2025 23:24:18 -0400 Subject: [PATCH 11/13] Update modOutputFilter.php Replace strftime with formatter --- .../Revolution/Filters/modOutputFilter.php | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/core/src/Revolution/Filters/modOutputFilter.php b/core/src/Revolution/Filters/modOutputFilter.php index 146af8dafbe..a424fecf840 100644 --- a/core/src/Revolution/Filters/modOutputFilter.php +++ b/core/src/Revolution/Filters/modOutputFilter.php @@ -12,6 +12,7 @@ use Exception; +use MODX\Revolution\Formatter\modManagerDateFormatter; use MODX\Revolution\modElement; use MODX\Revolution\modTag; use MODX\Revolution\modTemplateVar; @@ -30,12 +31,15 @@ class modOutputFilter */ public $modx = null; + private modManagerDateFormatter $formatter; + /** * @param modX $modx A reference to the modX instance */ function __construct(modX &$modx) { $this->modx = &$modx; + $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); } /** @@ -445,17 +449,20 @@ public function filter(&$element) $output = nl2br($output); break; - case 'strftime': + case 'strftime': /** @deprecated Removal of strftime filter option tbd */ case 'date': - /* See PHP's strftime - http://www.php.net/manual/en/function.strftime.php */ + /* See PHP's datetime - https://www.php.net/manual/en/datetime.format.php */ if (empty($m_val)) { - $m_val = "%A, %d %B %Y %H:%M:%S"; + $m_val = 'l, d F Y H:i:s'; /* @todo this should be modx default date/time format? Lexicon? */ } if (($value = filter_var($output, FILTER_VALIDATE_INT)) === false) { $value = strtotime($output); } - $output = ($value !== false) ? strftime($m_val, $value) : ''; + $output = ($value !== false) + ? $this->formatter->format($value, $m_val) + : '' + ; break; case 'strtotime': @@ -472,18 +479,24 @@ public function filter(&$element) $this->modx->getService('lexicon', 'modLexicon'); } $this->modx->lexicon->load('filters'); - if (empty($m_val)) { - $m_val = '%b %e'; - } if (!empty($output)) { + $defaultTimeFormat = 'h:i A'; $time = strtotime($output); if ($time >= strtotime('today')) { - $output = $this->modx->lexicon('today_at', ['time' => strftime('%I:%M %p', $time)]); + $output = $this->modx->lexicon( + 'today_at', + ['time' => $this->formatter->format($time, $defaultTimeFormat)] + ); } elseif ($time >= strtotime('yesterday')) { - $output = $this->modx->lexicon('yesterday_at', - ['time' => strftime('%I:%M %p', $time)]); + $output = $this->modx->lexicon( + 'yesterday_at', + ['time' => $this->formatter->format($time, $defaultTimeFormat)] + ); } else { - $output = strftime($m_val, $time); + if (empty($m_val)) { + $m_val = 'M j'; + } + $output = $this->formatter->format($time, $m_val); } } else { $output = '—'; From d4bc468a91c9f3500254e297cfb473e543de4d38 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Sat, 31 May 2025 23:34:09 -0400 Subject: [PATCH 12/13] Update modOutputFilter.php Code style/quality fixes --- .../Revolution/Filters/modOutputFilter.php | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/core/src/Revolution/Filters/modOutputFilter.php b/core/src/Revolution/Filters/modOutputFilter.php index a424fecf840..1eba6753834 100644 --- a/core/src/Revolution/Filters/modOutputFilter.php +++ b/core/src/Revolution/Filters/modOutputFilter.php @@ -1,4 +1,5 @@ modx = &$modx; $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); @@ -49,7 +49,7 @@ function __construct(modX &$modx) */ public function filter(&$element) { - $usemb = function_exists('mb_strlen') && (boolean)$this->modx->getOption('use_multibyte', null, false); + $usemb = function_exists('mb_strlen') && (bool)$this->modx->getOption('use_multibyte', null, false); $encoding = $this->modx->getOption('modx_charset', null, 'UTF-8'); $output = &$element->_output; @@ -61,7 +61,6 @@ public function filter(&$element) $condition = []; for ($i = 0; $i < $count; $i++) { - $m_cmd = trim($modifier_cmd[$i]); $m_val = $modifier_value[$i]; @@ -127,7 +126,7 @@ public function filter(&$element) $condition[] = intval(stripos($output, $m_val) !== false); break; case 'containsnot': - $condition[] = intval(stripos($output, $m_val) === false);; + $condition[] = intval(stripos($output, $m_val) === false); break; case 'ismember': case 'memberof': @@ -328,12 +327,16 @@ public function filter(&$element) if ($limit < 0) { $limit = 0; } - $breakpoint = $usemb ? mb_strpos($output, " ", $limit, $encoding) : strpos($output, " ", - $limit); + $breakpoint = $usemb + ? mb_strpos($output, ' ', $limit, $encoding) + : strpos($output, ' ', $limit) + ; if (false !== $breakpoint) { if ($breakpoint < $len - 1) { - $partial = $usemb ? mb_substr($output, 0, $breakpoint, $encoding) : substr($output, - 0, $breakpoint); + $partial = $usemb + ? mb_substr($output, 0, $breakpoint, $encoding) + : substr($output, 0, $breakpoint) + ; $output = $partial . $pad; } } @@ -564,32 +567,46 @@ public function filter(&$element) $ago = []; if (!empty($agoTS['years'])) { - $ago[] = $this->modx->lexicon(($agoTS['years'] > 1 ? 'ago_years' : 'ago_year'), - ['time' => $agoTS['years']]); + $ago[] = $this->modx->lexicon( + ($agoTS['years'] > 1 ? 'ago_years' : 'ago_year'), + ['time' => $agoTS['years']] + ); } if (!empty($agoTS['months'])) { - $ago[] = $this->modx->lexicon(($agoTS['months'] > 1 ? 'ago_months' : 'ago_month'), - ['time' => $agoTS['months']]); + $ago[] = $this->modx->lexicon( + ($agoTS['months'] > 1 ? 'ago_months' : 'ago_month'), + ['time' => $agoTS['months']] + ); } if (!empty($agoTS['weeks']) && empty($agoTS['years'])) { - $ago[] = $this->modx->lexicon(($agoTS['weeks'] > 1 ? 'ago_weeks' : 'ago_week'), - ['time' => $agoTS['weeks']]); + $ago[] = $this->modx->lexicon( + ($agoTS['weeks'] > 1 ? 'ago_weeks' : 'ago_week'), + ['time' => $agoTS['weeks']] + ); } if (!empty($agoTS['days']) && empty($agoTS['months']) && empty($agoTS['years'])) { - $ago[] = $this->modx->lexicon(($agoTS['days'] > 1 ? 'ago_days' : 'ago_day'), - ['time' => $agoTS['days']]); + $ago[] = $this->modx->lexicon( + ($agoTS['days'] > 1 ? 'ago_days' : 'ago_day'), + ['time' => $agoTS['days']] + ); } if (!empty($agoTS['hours']) && empty($agoTS['weeks']) && empty($agoTS['months']) && empty($agoTS['years'])) { - $ago[] = $this->modx->lexicon(($agoTS['hours'] > 1 ? 'ago_hours' : 'ago_hour'), - ['time' => $agoTS['hours']]); + $ago[] = $this->modx->lexicon( + ($agoTS['hours'] > 1 ? 'ago_hours' : 'ago_hour'), + ['time' => $agoTS['hours']] + ); } if (!empty($agoTS['minutes']) && empty($agoTS['days']) && empty($agoTS['weeks']) && empty($agoTS['months']) && empty($agoTS['years'])) { - $ago[] = $this->modx->lexicon($agoTS['minutes'] == 1 ? 'ago_minute' : 'ago_minutes', - ['time' => $agoTS['minutes']]); + $ago[] = $this->modx->lexicon( + ($agoTS['minutes'] == 1 ? 'ago_minute' : 'ago_minutes'), + ['time' => $agoTS['minutes']] + ); } if (empty($ago)) { /* handle <1 min */ - $ago[] = $this->modx->lexicon('ago_seconds', - ['time' => !empty($agoTS['seconds']) ? $agoTS['seconds'] : 0]); + $ago[] = $this->modx->lexicon( + 'ago_seconds', + ['time' => !empty($agoTS['seconds']) ? $agoTS['seconds'] : 0] + ); } $output = implode(', ', $ago); $output = $this->modx->lexicon('ago', ['time' => $output]); @@ -628,12 +645,9 @@ public function filter(&$element) if ($user = $this->modx->getObjectGraph(modUser::class, '{"Profile":{}}', $output)) { $userData = array_merge($user->toArray(), $user->Profile->toArray()); unset($userData['cachepwd'], $userData['salt'], $userData['sessionid'], $userData['password'], $userData['session_stale']); - if (strpos($key, 'extended.') === 0 && isset($userData['extended'][substr($key, - 9)])) { + if (strpos($key, 'extended.') === 0 && isset($userData['extended'][substr($key, 9)])) { $userInfo = $userData['extended'][substr($key, 9)]; - } elseif (strpos($key, - 'remote_data.') === 0 && isset($userData['remote_data'][substr($key, - 12)])) { + } elseif (strpos($key, 'remote_data.') === 0 && isset($userData['remote_data'][substr($key, 12)])) { $userInfo = $userData['remote_data'][substr($key, 12)]; } elseif (isset($userData[$key])) { $userInfo = $userData[$key]; @@ -804,11 +818,9 @@ private static function parseConditions($conditions, $value = null, $default = n return $default; } - if (!$m_con) { return $value; } - } catch (Exception $e) { } From 457ab3d46c1829dc89cf3a9895f29d1c89f00022 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 1 Aug 2025 12:00:26 -0400 Subject: [PATCH 13/13] Support localized frontend dates Progress commit (some work still tbd): Backs out changes to output filters and date tv props that made them non-localized by using php datetime. Adds ability to convert strftime to Intl date. --- .../Revolution/Filters/modOutputFilter.php | 98 ++++++---- .../Formatter/modDateFormatConverter.php | 168 ++++++++++++++++++ .../Formatter/modFrontendDateFormatter.php | 130 ++++++++++++++ .../Formatter/modManagerDateFormatter.php | 151 +++++++++------- .../Formatter/modStrftimeToIntlConverter.php | 26 +++ .../Formatter/strftimeToDatetime.map.php | 47 +++++ .../Formatter/strftimeToIntl.map.php | 47 +++++ .../Configs/GetInputPropertyConfigs.php | 28 +-- .../Renders/web/output/date.class.php | 14 +- core/src/Revolution/modX.php | 27 ++- 10 files changed, 611 insertions(+), 125 deletions(-) create mode 100644 core/src/Revolution/Formatter/modDateFormatConverter.php create mode 100644 core/src/Revolution/Formatter/modFrontendDateFormatter.php create mode 100644 core/src/Revolution/Formatter/modStrftimeToIntlConverter.php create mode 100644 core/src/Revolution/Formatter/strftimeToDatetime.map.php create mode 100644 core/src/Revolution/Formatter/strftimeToIntl.map.php diff --git a/core/src/Revolution/Filters/modOutputFilter.php b/core/src/Revolution/Filters/modOutputFilter.php index 1eba6753834..caea05b1cb3 100644 --- a/core/src/Revolution/Filters/modOutputFilter.php +++ b/core/src/Revolution/Filters/modOutputFilter.php @@ -12,7 +12,7 @@ namespace MODX\Revolution\Filters; use Exception; -use MODX\Revolution\Formatter\modManagerDateFormatter; +use MODX\Revolution\Formatter\modFrontendDateFormatter; use MODX\Revolution\modElement; use MODX\Revolution\modTag; use MODX\Revolution\modTemplateVar; @@ -31,7 +31,7 @@ class modOutputFilter */ public $modx = null; - private modManagerDateFormatter $formatter; + protected modFrontendDateFormatter $formatter; /** * @param modX $modx A reference to the modX instance @@ -39,7 +39,6 @@ class modOutputFilter public function __construct(modX &$modx) { $this->modx = &$modx; - $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); } /** @@ -55,13 +54,24 @@ public function filter(&$element) $output = &$element->_output; $inputFilter = $element->getInputFilter(); if ($inputFilter !== null && $inputFilter->hasCommands()) { - $modifier_cmd = $inputFilter->getCommands(); + $modifier_cmd = array_map('trim', $inputFilter->getCommands()); $modifier_value = $inputFilter->getModifiers(); $count = count($modifier_cmd); $condition = []; + // Load lexicon for filters that potentially require translation + if (count(array_intersect($modifier_cmd, ['date', 'idate', 'strftime', 'fuzzydate', 'ago'])) > 0) { + $cultureKey = $this->modx->getOption('cultureKey', null, 'en'); + $locale = $this->modx->config['locale']; + $lang = !empty($locale) && strlen($locale) >= 2 ? substr($locale, 0, 2) : $cultureKey ; + if (empty($this->modx->lexicon)) { + $this->modx->getService('lexicon', 'modLexicon'); + } + $this->modx->lexicon->load("{$lang}:core:filters"); + } + for ($i = 0; $i < $count; $i++) { - $m_cmd = trim($modifier_cmd[$i]); + $m_cmd = $modifier_cmd[$i]; $m_val = $modifier_value[$i]; $this->log('Processing Modifier: ' . $m_cmd . ' (parameters: ' . $m_val . ')'); @@ -451,19 +461,27 @@ public function filter(&$element) /* See PHP's nl2br - http://www.php.net/manual/en/function.nl2br.php */ $output = nl2br($output); break; + + case 'tabs2spaces': + $spacesPerTab = !empty($m_val) ? (int)$m_val : 2 ; + if (strpos($output, "\t") !== false) { + $replacement = ''; + $i = 0; + while ($i < $spacesPerTab) { + $i++; + $replacement .= ' '; + } + $output = str_replace("\t", $replacement, $output); + } + break; case 'strftime': /** @deprecated Removal of strftime filter option tbd */ case 'date': - /* See PHP's datetime - https://www.php.net/manual/en/datetime.format.php */ - if (empty($m_val)) { - $m_val = 'l, d F Y H:i:s'; - /* @todo this should be modx default date/time format? Lexicon? */ - } - if (($value = filter_var($output, FILTER_VALIDATE_INT)) === false) { - $value = strtotime($output); - } - $output = ($value !== false) - ? $this->formatter->format($value, $m_val) + $format = !empty($m_val) ? $m_val : '%A, %d %B %Y %H:%M:%S' ; + $formatter = new modFrontendDateFormatter($this->modx); + $formatter->setSourceFormat($m_val); + $output = ($output !== false) + ? $formatter->format($output, $format) : '' ; break; @@ -476,45 +494,42 @@ public function filter(&$element) $output = ''; } break; + case 'fuzzydate': - /* displays a "fuzzy" date reference */ - if (empty($this->modx->lexicon)) { - $this->modx->getService('lexicon', 'modLexicon'); - } - $this->modx->lexicon->load('filters'); if (!empty($output)) { - $defaultTimeFormat = 'h:i A'; + $relativeFormat = !empty($m_val) ? $m_val : '%I:%M %p' ; $time = strtotime($output); + $formatter = new modFrontendDateFormatter($this->modx); + $formatter->setSourceFormat($relativeFormat); if ($time >= strtotime('today')) { $output = $this->modx->lexicon( 'today_at', - ['time' => $this->formatter->format($time, $defaultTimeFormat)] + ['time' => $formatter->format($time, $relativeFormat)], + $lang ); } elseif ($time >= strtotime('yesterday')) { $output = $this->modx->lexicon( 'yesterday_at', - ['time' => $this->formatter->format($time, $defaultTimeFormat)] + ['time' => $formatter->format($time, $relativeFormat)], + $lang ); } else { if (empty($m_val)) { - $m_val = 'M j'; + $m_val = '%b %e'; } - $output = $this->formatter->format($time, $m_val); + $formatter->setSourceFormat($m_val); + $output = $formatter->format($time, $m_val); } } else { $output = '—'; } break; + case 'ago': /* calculates relative time ago from a timestamp */ if (empty($output)) { break; } - if (empty($this->modx->lexicon)) { - $this->modx->getService('lexicon', 'modLexicon'); - } - $this->modx->lexicon->load('filters'); - $agoTS = []; $uts['start'] = strtotime($output); $uts['end'] = time(); @@ -569,47 +584,54 @@ public function filter(&$element) if (!empty($agoTS['years'])) { $ago[] = $this->modx->lexicon( ($agoTS['years'] > 1 ? 'ago_years' : 'ago_year'), - ['time' => $agoTS['years']] + ['time' => $agoTS['years']], + $lang ); } if (!empty($agoTS['months'])) { $ago[] = $this->modx->lexicon( ($agoTS['months'] > 1 ? 'ago_months' : 'ago_month'), - ['time' => $agoTS['months']] + ['time' => $agoTS['months']], + $lang ); } if (!empty($agoTS['weeks']) && empty($agoTS['years'])) { $ago[] = $this->modx->lexicon( ($agoTS['weeks'] > 1 ? 'ago_weeks' : 'ago_week'), - ['time' => $agoTS['weeks']] + ['time' => $agoTS['weeks']], + $lang ); } if (!empty($agoTS['days']) && empty($agoTS['months']) && empty($agoTS['years'])) { $ago[] = $this->modx->lexicon( ($agoTS['days'] > 1 ? 'ago_days' : 'ago_day'), - ['time' => $agoTS['days']] + ['time' => $agoTS['days']], + $lang ); } if (!empty($agoTS['hours']) && empty($agoTS['weeks']) && empty($agoTS['months']) && empty($agoTS['years'])) { $ago[] = $this->modx->lexicon( ($agoTS['hours'] > 1 ? 'ago_hours' : 'ago_hour'), - ['time' => $agoTS['hours']] + ['time' => $agoTS['hours']], + $lang ); } if (!empty($agoTS['minutes']) && empty($agoTS['days']) && empty($agoTS['weeks']) && empty($agoTS['months']) && empty($agoTS['years'])) { $ago[] = $this->modx->lexicon( ($agoTS['minutes'] == 1 ? 'ago_minute' : 'ago_minutes'), - ['time' => $agoTS['minutes']] + ['time' => $agoTS['minutes']], + $lang ); } if (empty($ago)) { /* handle <1 min */ $ago[] = $this->modx->lexicon( 'ago_seconds', - ['time' => !empty($agoTS['seconds']) ? $agoTS['seconds'] : 0] + ['time' => !empty($agoTS['seconds']) ? $agoTS['seconds'] : 0], + $lang ); } $output = implode(', ', $ago); - $output = $this->modx->lexicon('ago', ['time' => $output]); + $output = $this->modx->lexicon('ago', ['time' => $output], $lang); break; case 'md5': /* See PHP's md5 - http://www.php.net/manual/en/function.md5.php */ diff --git a/core/src/Revolution/Formatter/modDateFormatConverter.php b/core/src/Revolution/Formatter/modDateFormatConverter.php new file mode 100644 index 00000000000..a33ed14fa99 --- /dev/null +++ b/core/src/Revolution/Formatter/modDateFormatConverter.php @@ -0,0 +1,168 @@ + [ + 'datetime' => 'strftimeToDatetime', + 'intl' => 'strftimeToIntl' + ], + 'datetime' => [ + 'intl' => 'datetimeToIntl' + ] + ]; + + private ?string $mapName; + private array $map = []; + + public function __construct(modX $modx, string $conversionRule = 'strftime->intl') + { + $this->modx =& $modx; + $conversionRule = trim($conversionRule); + if (empty($conversionRule)) { + // log warn + return; + } + if (strpos($conversionRule, '->') === false) { + // log warn + return; + } + $rule = explode('->', $conversionRule); + $this->fromFormat = $rule[0]; + $this->toFormat = $rule[1]; + } + + public function apply(string $format): string + { + $format = trim($format); + $this->originalFormat = $format; + if (!$this->getConversionMap()) { + // log err + return $format; + } + $method = $this->getConversionMethod(); + if (!empty($method) && method_exists($this, $method)) { + $format = $this->$method($format); + $this->modx->log(modX::LOG_LEVEL_ERROR, "\rNew format = {$format}"); + return $format; + } + // log warn + return $format; + } + + private function getConversionMap(): bool + { + $this->mapName = array_key_exists($this->fromFormat, self::FORMAT_CONVERTERS_MAP) && array_key_exists($this->toFormat, self::FORMAT_CONVERTERS_MAP[$this->fromFormat]) + ? self::FORMAT_CONVERTERS_MAP[$this->fromFormat][$this->toFormat] + : null + ; + if (!$this->mapName) { + // log err + return false; + } + $file = $this->mapName . '.map.php'; + $filePath = __DIR__ . '/' . ltrim($file, '/'); + if (!file_exists($filePath)) { + $this->modx->log(modX::LOG_LEVEL_ERROR, "\rMap file at {$filePath} not found, aborting!"); + return false; + } + // $this->modx->log(modX::LOG_LEVEL_ERROR, "\rGetting map file {$file} from {$filePath}..."); + $this->map = require $file; + // $this->modx->log(modX::LOG_LEVEL_ERROR, "\rGot conversion map!"); + return true; + } + + private function getConversionMethod(): string + { + if (!$this->fromFormat || !$this->toFormat) { + // log err + return ''; + } + return strtolower($this->fromFormat) . 'To' . ucfirst(strtolower($this->toFormat)); + } + + private function strftimeToIntl(string $format): string + { + $this->prepareEscapedFormatting($format); + if (preg_match_all('/%[\w]/', $format, $parts, PREG_PATTERN_ORDER)) { + foreach ($parts[0] as $part) { + $replacement = $this->map[$part]; + // Handle pre-defined patterns, defined by {predef:const1:const2} + if (in_array($part, ['%X', '%x', '%c'])) { + $replacement = '{predef:' . implode(':', $replacement) . '}'; + /* + Intl pre-defined format equivalents can not also contain other + patterns or characters. Here, if a strftime pre-defined pattern is + found, all other information in the original format is discarded + to ensure a valid mapping is created. + */ + if (strlen($format) > 2) { + $msg = "[Make into lexicon] A pre-defined strftime format ({$part}) was found in the original format string to be converted ({$this->originalFormat}). Other characters and/or formats in the original format were discarded to ensure a valid mapping to Intl."; + $this->modx->log(modX::LOG_LEVEL_WARN, $msg); + } + $format = $replacement; + break; + } + $format = str_replace($part, $replacement, $format); + } + } + return $format; + } + + private function strftimeToDatetime(string $format): string + { + $this->prepareEscapedFormatting($format); + if (preg_match_all('/%[\w]/', $format, $parts, PREG_PATTERN_ORDER)) { + foreach ($parts[0] as $part) { + $replacement = $this->map[$part]; + $format = str_replace($part, $replacement, $format); + } + } + return $format; + } + + /** + * Provide basic transformation of string literals in formatting pattern + */ + private function prepareEscapedFormatting(string &$format): void + { + if (strpos($format, '%%') !== false) { + preg_match_all('/%%[\w]/', $format, $escapedParts, PREG_PATTERN_ORDER); + foreach ($escapedParts[0] as $escapedPart) { + $replacement = $this->toFormat === 'intl' + ? "'{$escapedPart[0]}{$escapedPart[2]}'" + : $escapedPart[0] . "\\" . $escapedPart[2] + ; + $format = str_replace($escapedPart, $replacement, $format); + } + // If any '%%' sequences remain, they indicate a literal '%' + if (strpos($format, '%%') !== false) { + $format = str_replace('%%', '%', $format); + } + } + } +} diff --git a/core/src/Revolution/Formatter/modFrontendDateFormatter.php b/core/src/Revolution/Formatter/modFrontendDateFormatter.php new file mode 100644 index 00000000000..80ac102922f --- /dev/null +++ b/core/src/Revolution/Formatter/modFrontendDateFormatter.php @@ -0,0 +1,130 @@ +autoConvertStrftime = version_compare(phpversion(), '9.0.0', '>='); + // Hard code testing vals ... + // $this->autoConvertStrftime = true; + // $this->hasIntlDateExt = false; + // End HCV + $lc = setlocale(LC_ALL, null); + $msg = <<hasIntlDateExt} + autoConvertStrftime: {$this->autoConvertStrftime} + locale: {$lc} + culture: {$this->modx->cultureKey} + session lang: {$_SESSION['manager_language']} + LOG; + $this->modx->log(modX::LOG_LEVEL_ERROR, "\r{$msg}"); + // $this->modx->log(modX::LOG_LEVEL_ERROR, "\rSession: " . print_r($_SESSION, true)); + } + + /** + * Transforms a date/time-related value using the specified DateTime format + * @param string|int $value The value to transform (a Unix timestamp or mysql-format string) + * @param string $format The custom format to use when formatting the $value + * @param bool $useOffset Whether to use the offset time (system setting) in the date calculation + * @param string|null $emptyValue The text to show when the $value passed is empty + * @return string The formatted date + */ + public function format($value, string $format, bool $useOffset = false, ?string $emptyValue = null): string + { + $msg = <<sourceFormat} + sourceFormatType: {$this->sourceFormatType} + LOG; + $this->modx->log(modX::LOG_LEVEL_ERROR, "\r{$msg}"); + if (!$this->autoConvertStrftime) { + $this->setDateFn($this->sourceFormatType); + } else { + if ($this->sourceFormatType === 'strftime') { + if ($this->hasIntlDateExt) { + $this->setDateFn('intl'); + $this->setConversionRule(); + } else { + $this->setDateFn('datetime'); + $this->setConversionRule('strftime->datetime'); + } + $this->getConverter(); + // $this->modx->log(modX::LOG_LEVEL_ERROR, "\rGot converter!"); + $format = $this->converter->apply($format); + } + } + return parent::format($value, $format); + } + + /** + * Sets this class's $conversionRule property value + * @param string $rule A string in the form of 'fromPatternType->toPatternType' + * that specifies the source to destination conversion + */ + public function setConversionRule(string $rule = 'strftime->intl'): void + { + $this->conversionRule = $rule; + } + + /** + * Preserves the original formatting string for reference + * @param string $sourceFormat The formatting pattern originally passed in + * to this class's format method (before transformations, if any) + */ + public function setSourceFormat(string $sourceFormat): void + { + if (strpos($sourceFormat, '%') !== false) { + $this->setSourceFormatType('strftime'); + } + $this->sourceFormat = trim($sourceFormat); + } + + public function setSourceFormatType(string $formatType): void + { + // optional: set 'strftime' or 'datetime', could be 'intl' but that's typically going to be the preferred destination format + $this->sourceFormatType = trim($formatType); + } + + public function setDestinationFormatType(string $formatType): void + { + // optional: set 'intl' or 'datetime' + $this->destinationFormatType = trim($formatType); + } + + /** + * Gets an instance of modDateFormatConverter + */ + private function getConverter(): void + { + $this->converter = new modDateFormatConverter($this->modx, $this->conversionRule); + } +} diff --git a/core/src/Revolution/Formatter/modManagerDateFormatter.php b/core/src/Revolution/Formatter/modManagerDateFormatter.php index 70ab6f7f6d8..a1910af518c 100644 --- a/core/src/Revolution/Formatter/modManagerDateFormatter.php +++ b/core/src/Revolution/Formatter/modManagerDateFormatter.php @@ -11,6 +11,7 @@ namespace MODX\Revolution\Formatter; +use IntlDateFormatter; use MODX\Revolution\modX; /** @@ -57,18 +58,32 @@ class modManagerDateFormatter null ]; + /** + * @var bool $hasIntlDateExt Whether the Intl extension's IntlDateFormatter is available + */ + protected bool $hasIntlDateExt = false; + + /** + * @var string $dateFn An identifier specifying which date formatting function to use + */ + protected string $dateFn = 'date'; + + /** * @var string $managerDateEmptyDisplay The text (if any) to show for empty dates */ private string $managerDateEmptyDisplay = ''; - public function __construct(modX $modx) { $this->modx =& $modx; $this->managerDateFormat = $this->modx->getOption('manager_date_format', null, 'Y-m-d', true); $this->managerTimeFormat = $this->modx->getOption('manager_time_format', null, 'H:i', true); $this->managerDateEmptyDisplay = $this->modx->getOption('manager_datetime_empty_value', null, '–', true); + + if ($this->modx->hasIntlExtension && class_exists('IntlDateFormatter')) { + $this->hasIntlDateExt = true; + } } public function isEmpty($value): bool @@ -112,68 +127,14 @@ protected function parseValue($value, bool $useOffset = false): ?int } /** - * Convert from one date formatting syntax to another - * @param string $format The full formatting string to convert - * @param string $from The current syntax used in $format - * @param string $to The target syntax - * @return string The converted formatting string + * Sets an identifier for specifying which date formatting function to use + * @param string $formatType The formatting pattern type (datetime, strftime, or intl [unicode/ICU]) */ - public static function convertDateFormat(string &$format, string $from = 'strftime', string $to = 'datetime'): string + protected function setDateFn(string $formatType): void { - $format = trim($format); - $strftimeToDatetimeMap = [ - '%a' => 'D', - '%A' => 'l', - '%d' => 'd', - '%e' => 'j', - '%j' => 'z', // 001 to 366 => 0 to 365 - '%u' => 'N', - '%w' => 'w', - '%U' => 'W', // general match, see strftime - '%V' => 'W', // general match, see strftime - '%W' => 'W', - '%b' => 'M', - '%h' => 'M', // general match, %h is localized version of %b - '%B' => 'F', - '%m' => 'm', - '%C' => '**', // 2-digit century, no datetime equivalent - '%g' => 'y', // general match, see strftime - '%G' => 'Y', // general match, see strftime - '%y' => 'y', - '%Y' => 'Y', - '%H' => 'H', - '%k' => 'G', - '%I' => 'h', - '%l' => 'g', - '%M' => 'i', - '%p' => 'A', - '%P' => 'a', - '%S' => 's', - '%z' => 'Z', - '%Z' => 'T', - '%s' => 'U', - // compound formats - '%r' => 'h:i:s A', - '%R' => 'H:i', - '%T' => 'H:i:s', - '%X' => 'h:i:s', // locale unsupported in datetime, see strftime - '%c' => 'c', // locale unsupported in datetime, see strftime - '%D' => 'm/d/y', - '%F' => 'Y-m-d', - '%x' => 'm/d/y', // locale unsupported in datetime, see strftime - // characters - '%n' => ' ', // newline, \n only works within double quoted string - '%t' => ' ', // tab, \t only works within double quoted string - '%%' => '%' - ]; - $map = $strftimeToDatetimeMap; - - if ($from === 'strftime' && preg_match_all('/%[\w]/', $format, $parts, PREG_PATTERN_ORDER)) { - foreach ($parts[0] as $part) { - $format = str_replace($part, $map[$part], $format); - } - } - return $format; + $formatType = trim($formatType); + $fnId = $formatType === 'datetime' ? 'date' : $formatType ; + $this->dateFn = $fnId; } /** @@ -192,12 +153,72 @@ public function format($value, string $format, bool $useOffset = false, ?string return $emptyValue === null ? $this->managerDateEmptyDisplay : $emptyValue; } - // For now, only strftime to datetime is anticipated - if (strpos($format, '%') !== false) { - self::convertDateFormat($format); + // Handle replacement of space-related patterns in format + if (preg_match('/\*[nt]/', $format)) { + $spacingFormats = ['*n', '*t']; + foreach ($spacingFormats as $spacer) { + $replacementSpacer = ""; + if (strpos($format, $spacer) !== false) { + $newFormat = ""; + $replacementSpacer = $spacer === '*n' ? "\n" : "\t" ; + $parts = explode($spacer, $format); + $numParts = count($parts); + for ($i = 0; $i < $numParts; $i++) { + $newFormat .= "{$parts[$i]}"; + if ($i < $numParts - 1) { + $newFormat .= $replacementSpacer; + } + } + $format = $newFormat; + } + } } - return date($format, $value); + if ($this->dateFn === 'intl') { + $useIntlPredefinedFormats = false; + $locale = class_exists('Locale') + ? \Locale::getDefault() + : 'en_US' + ; + if (strpos($format, '{predef') === 0) { + $useIntlPredefinedFormats = true; + $formatsConfig = trim($format, '{}'); + $formatsConfig = str_replace('predef:', '', $formatsConfig); + $formatsConfig = explode(':', $formatsConfig); + } + $predefinedFormats = $useIntlPredefinedFormats && count($formatsConfig) === 2 + ? [(int)$formatsConfig[0], (int)$formatsConfig[1]] + : [IntlDateFormatter::NONE, IntlDateFormatter::NONE] + ; + $args = [$locale, ...$predefinedFormats]; + try { + $formatter = new IntlDateFormatter(...$args); + $timezone = $formatter->getTimeZoneId(); + $formatter->setTimeZone($timezone); + if (!$useIntlPredefinedFormats) { + $formatter->setPattern($format); + } + // $msg = <<modx->log(modX::LOG_LEVEL_ERROR, "\r{$msg}"); + return $formatter->format($value); + } catch (\Exception $e) { + $msg = 'There was a problem initializing IntlDateFormatter with the following arguments: ' . print_r($args, true); + $this->modx->log(modX::LOG_LEVEL_ERROR, "\r{$msg}"); + return '**'; + } + } else { + /** + * While strftime is still present in modx-supported versions of php, use this + * strategy for dynamically specifying one of two functions: strftime or date + */ + // $this->modx->log(modX::LOG_LEVEL_ERROR, "\rFormatting [{$format}] with {$this->dateFn}"); + return call_user_func($this->dateFn, $format, $value); + } } /** diff --git a/core/src/Revolution/Formatter/modStrftimeToIntlConverter.php b/core/src/Revolution/Formatter/modStrftimeToIntlConverter.php new file mode 100644 index 00000000000..a09a8bbac75 --- /dev/null +++ b/core/src/Revolution/Formatter/modStrftimeToIntlConverter.php @@ -0,0 +1,26 @@ + 'D', + '%A' => 'l', + '%d' => 'd', + '%e' => 'j', + '%j' => 'z', // 001 to 366 => 0 to 365 + '%u' => 'N', + '%w' => 'w', + '%U' => 'W', // general match, see strftime + '%V' => 'W', // general match, see strftime + '%W' => 'W', + '%b' => 'M', + '%h' => 'M', // general match, %h is localized version of %b + '%B' => 'F', + '%m' => 'm', + '%C' => '**', // 2-digit century, no datetime equivalent + '%g' => 'y', // general match, see strftime + '%G' => 'Y', // general match, see strftime + '%y' => 'y', + '%Y' => 'Y', + '%H' => 'H', + '%k' => 'G', + '%I' => 'h', + '%l' => 'g', + '%M' => 'i', + '%p' => 'A', + '%P' => 'a', + '%S' => 's', + '%z' => 'Z', + '%Z' => 'T', + '%s' => 'U', + // compound formats + '%r' => 'h:i:s A', + '%R' => 'H:i', + '%T' => 'H:i:s', + '%X' => 'h:i:s', // locale unsupported in datetime, see strftime + '%c' => 'c', // locale unsupported in datetime, see strftime + '%D' => 'm/d/y', + '%F' => 'Y-m-d', + '%x' => 'm/d/y', // locale unsupported in datetime, see strftime + // characters + '%n' => '*n', // newline, \n only works within double quoted string + '%t' => '*t', // tab, \t only works within double quoted string + '%%' => '%' +]; diff --git a/core/src/Revolution/Formatter/strftimeToIntl.map.php b/core/src/Revolution/Formatter/strftimeToIntl.map.php new file mode 100644 index 00000000000..ef9b25af472 --- /dev/null +++ b/core/src/Revolution/Formatter/strftimeToIntl.map.php @@ -0,0 +1,47 @@ + 'D', + '%A' => 'EEEE', + '%d' => 'dd', + '%e' => 'd', + '%j' => 'DDD', + '%u' => 'e', // general match, 1-7 (Mon-Sun) => 1-7 (Sun-Sat) + '%w' => 'e', // general match, 0-6 (Sun-Sat) => 1-7 (Sun-Sat) + '%U' => 'ww', // general match, see strftime + '%V' => 'ww', // close match, except some leap years + '%W' => 'ww', // general match, see strftime + '%b' => 'MMM', + '%h' => 'MMM', + '%B' => 'MMMM', + '%m' => 'MM', + '%C' => '**', // 2-digit century, no Intl equivalent (would have to be calculated from y) + '%g' => 'YY', + '%G' => 'Y', + '%y' => 'yy', + '%Y' => 'y', + '%H' => 'HH', + '%k' => 'H', + '%I' => 'hh', + '%l' => 'h', + '%M' => 'mm', + '%p' => 'a', + '%P' => 'a', // 'b' is specified in ICU, but doesn't seem to work, using 'a' + '%S' => 'ss', + '%z' => 'Z', + '%Z' => 'z', + '%s' => '**', // no Intl equivalent to display UNIX timestamp + // compound formats + '%r' => 'hh:mm:ss a', + '%R' => 'HH:mm', + '%T' => 'HH:mm:ss', + '%X' => ['date' => \IntlDateFormatter::NONE, 'time' => \IntlDateFormatter::LONG], + '%c' => ['date' => \IntlDateFormatter::MEDIUM, 'time' => \IntlDateFormatter::LONG], + '%D' => 'MM/dd/yy', + '%F' => 'y-MM-dd', + '%x' => ['date' => \IntlDateFormatter::SHORT, 'time' => \IntlDateFormatter::NONE], + // characters + '%n' => '*n', // newline, \n only works within double quoted string + '%t' => '*t', // tab, \t only works within double quoted string + '%%' => "'%'" +]; diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php b/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php index c94b47f7417..15f01ac68eb 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Configs/GetInputPropertyConfigs.php @@ -11,7 +11,7 @@ namespace MODX\Revolution\Processors\Element\TemplateVar\Configs; -use MODX\Revolution\Formatter\modManagerDateFormatter; +use MODX\Revolution\Formatter\modFrontendDateFormatter; use MODX\Revolution\modNamespace; use MODX\Revolution\Processors\Processor; use MODX\Revolution\modTemplateVar; @@ -71,7 +71,7 @@ private function setExampleData() { /* Date example */ $now = time(); - $formatter = $this->modx->services->get(modManagerDateFormatter::class); + $formatter = new modFrontendDateFormatter($this->modx); $formatDefault = 'Y-m-d'; $formatCurrent = $this->modx->getOption('manager_date_format'); $seps = '-/. '; @@ -91,6 +91,10 @@ private function setExampleData() Some usages of date kept here (instead of using the $formatter), as evenutal localization of these values would have no effect */ + $formatter->setSourceFormatType('strftime'); + $format2 = '%B %e'; // Intl: 'MMMM d' | datetime: 'F jS' (S is for ordinal [st, nd, rd], only available in non-localized datetime format) + $format4 = '%B %Y'; // Intl: 'MMMM y' | datetime: 'F Y' + $this->exampleData['disabled_dates_desc'] = [ 'format_current' => date($formatCurrent), 'format_default' => date($formatDefault), @@ -98,24 +102,24 @@ private function setExampleData() ',' . date($formatDefault, strtotime("+7 days")), 'example_2a' => date($formatWithoutYear, $timestampAheadOneMonth) . ',' . date($formatWithoutYear, $timestampAheadAlt), - 'example_2b' => $formatter->format($timestampAheadOneMonth, 'F jS'), - 'example_2c' => $formatter->format($timestampAheadAlt, 'F jS'), + 'example_2b' => $formatter->format($timestampAheadOneMonth, $format2), + 'example_2c' => $formatter->format($timestampAheadAlt, $format2), 'example_3a' => '^' . date("Y"), 'example_3b' => date("Y"), 'example_4a' => date($formatRegexAllDays, $timestampAheadOneMonth), - 'example_4b' => $formatter->format($timestampAheadOneMonth, 'F Y'), + 'example_4b' => $formatter->format($timestampAheadOneMonth, $format4), 'example_5' => '03-..$', 'example_6a' => $nextYear . '.03.15', 'example_6b' => $nextYear . '\\\.03\\\.15' ]; - $format1 = 'l, d F Y'; - $format2 = 'D, M j, Y'; - $format3 = 'm/d/Y'; - $format4 = 'Y-m-d'; - $format5 = 'Y-m-d H:i:s'; - $format6 = 'M j, Y'; - $format7 = 'j M Y g:i A'; + $format1 = '%A, %d %B %Y'; // datetime: 'l, d F Y' + $format2 = '%a, %b %e, %Y'; // datetime: 'D, M j, Y' + $format3 = '%m/%d/%Y'; // datetime: 'm/d/Y' + $format4 = '%Y-%m-%d'; // datetime: 'Y-m-d' + $format5 = '%Y-%m-%d %H:%M:%S'; // datetime: 'Y-m-d H:i:s' + $format6 = '%b %e, %Y'; // datetime: 'M j, Y' + $format7 = '%e %b %Y %l:%M %p'; // datetime: 'j M Y g:i A' $this->exampleData['date_format_desc'] = [ 'example_1a' => $format1, diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php index 1c3f02b596d..0591a57e842 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Renders/web/output/date.class.php @@ -9,7 +9,7 @@ * files found in the top-level directory of this distribution. */ -use MODX\Revolution\Formatter\modManagerDateFormatter; +use MODX\Revolution\Formatter\modFrontendDateFormatter; use MODX\Revolution\modTemplateVarOutputRender; /** @@ -24,10 +24,14 @@ class modTemplateVarOutputRenderDate extends modTemplateVarOutputRender { public function process($value, array $params = []) { - $formatter = $this->modx->services->get(modManagerDateFormatter::class); /* default properties */ - $params['format'] = !empty($params['format']) ? $params['format'] : 'l, d F Y'; - /* fix for 2.0.0-pl bug where 1=yes and 0=no */ + /* + Friday, 01 August 2025: + datetime: l, d F Y + strftime: %A, %d %B %Y + intl: EEEE, dd MMMM y + */ + $params['format'] = !empty($params['format']) ? $params['format'] : '%A, %d %B %Y'; $params['default'] = !empty($params['default']) && in_array($params['default'], ['yes',1,'1']) ? 1 : 0; $value = $this->tv->parseInput($value); @@ -44,6 +48,8 @@ public function process($value, array $params = []) } /* return formatted time */ + $formatter = new modFrontendDateFormatter($this->modx); + $formatter->setSourceFormat($params['format']); return $formatter->format($timestamp, $params['format']); } } diff --git a/core/src/Revolution/modX.php b/core/src/Revolution/modX.php index 086ffd04485..508c738f3a3 100644 --- a/core/src/Revolution/modX.php +++ b/core/src/Revolution/modX.php @@ -292,6 +292,9 @@ class modX extends xPDO { */ private $_deprecations = []; + /** Indicates whether the php internationalization extension is available */ + public bool $hasIntlExtension = false; + /** * Harden the environment against common security flaws. * @@ -475,6 +478,7 @@ public function __construct($configPath= '', $options = null, $driverOptions = n $this->addPackage('MODX\Revolution\Registry\Db', MODX_CORE_PATH . 'src/', null, 'MODX\\'); $this->addPackage('MODX\Revolution\Sources', MODX_CORE_PATH . 'src/', null, 'MODX\\'); $this->addPackage('MODX\Revolution\Transport', MODX_CORE_PATH . 'src/', null, 'MODX\\'); + $this->hasIntlExtension = extension_loaded('intl'); } catch (xPDOException $xe) { $this->sendError('unavailable', ['error_message' => $xe->getMessage()]); } catch (Exception $e) { @@ -2603,13 +2607,18 @@ protected function _initContext($contextKey, $regenerate = false, $options = nul $this->config= array_merge($this->_systemConfig, $this->context->config); $iniTZ = ini_get('date.timezone'); $cfgTZ = $this->getOption('date_timezone', $options, ''); - if (!empty($cfgTZ)) { - if (empty($iniTZ) || $iniTZ !== $cfgTZ) { - date_default_timezone_set($cfgTZ); - } - } elseif (empty($iniTZ)) { - date_default_timezone_set('UTC'); + $targetTZ = empty($iniTZ) && empty($cfgTZ) ? 'UTC' : $iniTZ ; + if (!empty($cfgTZ) && $cfgTZ !== $iniTZ) { + $targetTZ = $cfgTZ; + } + + // $this->log(modX::LOG_LEVEL_ERROR, "Context: {$this->context->key}; targetTZ: {$targetTZ}; iniTZ: {$iniTZ}; cfgTZ: {$cfgTZ}"); + + if (!date_default_timezone_set($targetTZ)) { + $msg = '[_initContext] Failed to set default timezone for the [[+key]] context because the target timezone value [[+targetTZ]] is invalid.'; + $this->log(modX::LOG_LEVEL_ERROR, "\r{$msg}"); } + if ($this->_initialized) { $this->user = null; $this->getUser(); @@ -2678,6 +2687,12 @@ protected function _initCulture($options = null) { if ($result === false) { $this->log(modX::LOG_LEVEL_ERROR, 'Could not set the locale. Please check if the locale ' . $this->getOption('locale', null, $locale) . ' exists on your system'); } + + // $this->log(modX::LOG_LEVEL_ERROR, 'Value for targetLocale: '.$targetLocale); + + if ($this->hasIntlExtension && class_exists('Locale')) { + \Locale::setDefault($targetLocale); + } } $this->services->add('lexicon', new modLexicon($this));