diff --git a/vendor/magento/module-customer/Block/Widget/Dob.php b/vendor/magento/module-customer/Block/Widget/Dob.php index 8d737df9789f5..7dae5eb930a6d 100644 --- a/vendor/magento/module-customer/Block/Widget/Dob.php +++ b/vendor/magento/module-customer/Block/Widget/Dob.php @@ -10,6 +10,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\Bundle\DataBundle; +use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; /** @@ -171,6 +172,8 @@ protected function getFormFilter() /** * Apply output filter to value * + * Normalizes date to display format with standard numerals to avoid localized numerals (e.g., Arabic) + * * @param string $value * @return string */ @@ -181,7 +184,7 @@ protected function applyOutputFilter($value) $value = date('Y-m-d', $this->getTime()); $value = $filter->outputFilter($value); } - return $value; + return $this->normalizedDobOutput($value); } /** @@ -409,19 +412,25 @@ public function getTranslatedCalendarConfigJson(): string $localeData = (new DataBundle())->get($this->localeResolver->getLocale()); $monthsData = $localeData['calendar']['gregorian']['monthNames']; $daysData = $localeData['calendar']['gregorian']['dayNames']; - + $monthsFormat = $monthsData['format']; + $daysFormat = $daysData['format']; + $monthsAbbreviated = $monthsFormat['abbreviated']; + $monthsShort = $monthsAbbreviated ?? $monthsFormat['wide']; + $daysAbbreviated = $daysFormat['abbreviated']; + $daysShort = $daysAbbreviated ?? $daysFormat['wide']; + $daysShortFormat = $daysFormat['short']; + $daysMin = $daysShortFormat ?? $daysShort; return $this->encoder->encode( [ 'closeText' => __('Done'), 'prevText' => __('Prev'), 'nextText' => __('Next'), 'currentText' => __('Today'), - 'monthNames' => array_values(iterator_to_array($monthsData['format']['wide'])), - 'monthNamesShort' => array_values(iterator_to_array($monthsData['format']['abbreviated'])), - 'dayNames' => array_values(iterator_to_array($daysData['format']['wide'])), - 'dayNamesShort' => array_values(iterator_to_array($daysData['format']['abbreviated'])), - 'dayNamesMin' => - array_values(iterator_to_array(($daysData['format']['short']) ?: $daysData['format']['abbreviated'])), + 'monthNames' => array_values(iterator_to_array($monthsFormat['wide'])), + 'monthNamesShort' => array_values(iterator_to_array($monthsShort)), + 'dayNames' => array_values(iterator_to_array($daysFormat['wide'])), + 'dayNamesShort' => array_values(iterator_to_array($daysShort)), + 'dayNamesMin' => array_values(iterator_to_array($daysMin)), ] ); } @@ -440,4 +449,31 @@ private function setTwoDayPlaces(string $format): string $format ); } + + /** + * Normalize the dob for a proper output on the frontend + * + * Converts localized date format (with potentially localized numerals like Arabic) to standard numerals + * + * @param mixed $value + * @return mixed + */ + private function normalizedDobOutput(mixed $value): mixed + { + if (empty($value)) { + return $value; + } + $locale = $this->localeResolver->getLocale(); + $dateFormat = $this->getDateFormat(); + $dateTime = $this->_localeDate->date($value, $locale, false, false); + $formatter = new \IntlDateFormatter( + Resolver::DEFAULT_LOCALE, + \IntlDateFormatter::NONE, + \IntlDateFormatter::NONE, + $dateTime->getTimezone(), + null, + $dateFormat + ); + return $formatter->format($dateTime); + } } diff --git a/vendor/magento/module-customer/Plugin/ValidateDobOnSave.php b/vendor/magento/module-customer/Plugin/ValidateDobOnSave.php new file mode 100644 index 0000000000000..dc35d8cd32f21 --- /dev/null +++ b/vendor/magento/module-customer/Plugin/ValidateDobOnSave.php @@ -0,0 +1,162 @@ +eavConfig = $eavConfig; + $this->json = $json; + $this->localeResolver = $localeResolver; + } + + /** + * Enforce DOB min/max from attribute validate_rules on every save. + * + * @param CustomerRepositoryInterface $subject + * @param callable $proceed + * @param CustomerInterface $customer + * @param string|null $passwordHash + * @return mixed + * @throws InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSave( + CustomerRepositoryInterface $subject, + callable $proceed, + CustomerInterface $customer, + $passwordHash = null + ) { + $dobRaw = $customer->getDob(); + + $dobDate = $this->parseDate($dobRaw); + if ($dobRaw !== null && $dobRaw !== '' && !$dobDate) { + throw new InputException(__('Date of Birth is invalid.')); + } + + if ($dobDate) { + $normalizedDob = $dobDate->format(DateTime::DATE_PHP_FORMAT); + $customer->setDob($normalizedDob); + $attr = $this->eavConfig->getAttribute('customer', 'dob'); + $rules = $attr->getData('validate_rules'); + if (is_string($rules) && $rules !== '') { + try { + $rules = $this->json->unserialize($rules); + } catch (\InvalidArgumentException $e) { + $rules = []; + } + } + if (!is_array($rules)) { + $rules = (array)$attr->getValidateRules(); + } + + $min = $rules['date_range_min'] ?? $rules['min_date'] ?? null; + $max = $rules['date_range_max'] ?? $rules['max_date'] ?? null; + + $minDate = $this->parseDate($min); + $maxDate = $this->parseDate($max); + + $dobKey = $dobDate->format(DateTime::DATE_PHP_FORMAT); + + if ($minDate && $dobKey < $minDate->format(DateTime::DATE_PHP_FORMAT)) { + throw new InputException(__( + 'Date of Birth must be on or after %1.', + $minDate->format(DateTime::DATE_PHP_FORMAT) + )); + } + if ($maxDate && $dobKey > $maxDate->format(DateTime::DATE_PHP_FORMAT)) { + throw new InputException(__( + 'Date of Birth must be on or before %1.', + $maxDate->format(DateTime::DATE_PHP_FORMAT) + )); + } + } + + return $proceed($customer, $passwordHash); + } + + /** + * Parse a date value into DateTimeImmutable. + * + * @param mixed $value + * @return \DateTimeImmutable|null + * @throws \Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function parseDate($value): ?\DateTimeImmutable + { + if ($value === null || $value === '' || $value === false) { + return null; + } + if (is_int($value) || (is_string($value) && ctype_digit($value))) { + $intVal = (int)$value; + if ($intVal <= 0) { + return null; + } + $seconds = ($intVal >= 10000000000) ? intdiv($intVal, 1000) : $intVal; + return (new \DateTimeImmutable('@' . $seconds))->setTimezone(new \DateTimeZone('UTC')); + } + $stringValue = (string)$value; + $locale = $this->localeResolver->getLocale(); + $formatter = new \IntlDateFormatter( + $locale, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE + ); + $formatter->setPattern(DateTime::DATE_INTERNAL_FORMAT); + $timestamp = $formatter->parse($stringValue); + if ($timestamp !== false) { + return new \DateTimeImmutable('@' . $timestamp); + } + + try { + return new \DateTimeImmutable($stringValue); + } catch (\Exception $e) { + return null; + } + } +} diff --git a/vendor/magento/module-customer/etc/frontend/di.xml b/vendor/magento/module-customer/etc/frontend/di.xml index 827a153e94674..87d2f24fcbfc9 100644 --- a/vendor/magento/module-customer/etc/frontend/di.xml +++ b/vendor/magento/module-customer/etc/frontend/di.xml @@ -1,8 +1,8 @@ @@ -130,4 +130,7 @@ + + + diff --git a/vendor/magento/framework/View/Element/Html/Calendar.php b/vendor/magento/framework/View/Element/Html/Calendar.php index 884488d77a74f..5997a7682e400 100644 --- a/vendor/magento/framework/View/Element/Html/Calendar.php +++ b/vendor/magento/framework/View/Element/Html/Calendar.php @@ -1,7 +1,7 @@ $this->encoder->encode(array_values(iterator_to_array($daysData['format']['wide']))), 'abbreviated' => $this->encoder->encode( - array_values(iterator_to_array($daysData['format']['abbreviated'])) + array_values( + iterator_to_array( + $daysData['format']['abbreviated'] ?? $daysData['format']['wide'] + ) + ) ), ] );