Skip to content

Commit 2687b48

Browse files
authored
Merge pull request #10227 from adobe-commerce-tier-4/PR_2025_11_20_flowers
[Support Tier-4 flowers] 11-20-2025 Regular delivery of bugfixes and improvements
2 parents 5044c28 + 7e75c4b commit 2687b48

File tree

13 files changed

+381
-35
lines changed

13 files changed

+381
-35
lines changed

app/code/Magento/Customer/Block/Widget/Dob.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -410,19 +410,25 @@ public function getTranslatedCalendarConfigJson(): string
410410
$localeData = (new DataBundle())->get($this->localeResolver->getLocale());
411411
$monthsData = $localeData['calendar']['gregorian']['monthNames'];
412412
$daysData = $localeData['calendar']['gregorian']['dayNames'];
413-
413+
$monthsFormat = $monthsData['format'];
414+
$daysFormat = $daysData['format'];
415+
$monthsAbbreviated = $monthsFormat['abbreviated'];
416+
$monthsShort = $monthsAbbreviated ?? $monthsFormat['wide'];
417+
$daysAbbreviated = $daysFormat['abbreviated'];
418+
$daysShort = $daysAbbreviated ?? $daysFormat['wide'];
419+
$daysShortFormat = $daysFormat['short'];
420+
$daysMin = $daysShortFormat ?? $daysShort;
414421
return $this->encoder->encode(
415422
[
416423
'closeText' => __('Done'),
417424
'prevText' => __('Prev'),
418425
'nextText' => __('Next'),
419426
'currentText' => __('Today'),
420-
'monthNames' => array_values(iterator_to_array($monthsData['format']['wide'])),
421-
'monthNamesShort' => array_values(iterator_to_array($monthsData['format']['abbreviated'])),
422-
'dayNames' => array_values(iterator_to_array($daysData['format']['wide'])),
423-
'dayNamesShort' => array_values(iterator_to_array($daysData['format']['abbreviated'])),
424-
'dayNamesMin' =>
425-
array_values(iterator_to_array(($daysData['format']['short']) ?: $daysData['format']['abbreviated'])),
427+
'monthNames' => array_values(iterator_to_array($monthsFormat['wide'])),
428+
'monthNamesShort' => array_values(iterator_to_array($monthsShort)),
429+
'dayNames' => array_values(iterator_to_array($daysFormat['wide'])),
430+
'dayNamesShort' => array_values(iterator_to_array($daysShort)),
431+
'dayNamesMin' => array_values(iterator_to_array($daysMin)),
426432
]
427433
);
428434
}

app/code/Magento/Customer/Plugin/ValidateDobOnSave.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
use Magento\Framework\Exception\InputException;
1515
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
1616
use Magento\Framework\Stdlib\DateTime;
17+
use Magento\Framework\Locale\ResolverInterface;
1718

19+
/**
20+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
21+
*/
1822
class ValidateDobOnSave
1923
{
2024
/**
@@ -27,16 +31,24 @@ class ValidateDobOnSave
2731
*/
2832
private $json;
2933

34+
/**
35+
* @var ResolverInterface
36+
*/
37+
private $localeResolver;
38+
3039
/**
3140
* @param EavConfig $eavConfig
3241
* @param JsonSerializer $json
42+
* @param ResolverInterface $localeResolver
3343
*/
3444
public function __construct(
3545
EavConfig $eavConfig,
36-
JsonSerializer $json
46+
JsonSerializer $json,
47+
ResolverInterface $localeResolver
3748
) {
3849
$this->eavConfig = $eavConfig;
3950
$this->json = $json;
51+
$this->localeResolver = $localeResolver;
4052
}
4153

4254
/**
@@ -67,8 +79,9 @@ public function aroundSave(
6779
}
6880

6981
if ($dobDate) {
82+
$normalizedDob = $dobDate->format(DateTime::DATE_PHP_FORMAT);
83+
$customer->setDob($normalizedDob);
7084
$attr = $this->eavConfig->getAttribute('customer', 'dob');
71-
7285
$rules = $attr->getData('validate_rules');
7386
if (is_string($rules) && $rules !== '') {
7487
try {
@@ -127,9 +140,21 @@ private function parseDate($value): ?\DateTimeImmutable
127140
$seconds = ($intVal >= 10000000000) ? intdiv($intVal, 1000) : $intVal;
128141
return (new \DateTimeImmutable('@' . $seconds))->setTimezone(new \DateTimeZone('UTC'));
129142
}
143+
$stringValue = (string)$value;
144+
$locale = $this->localeResolver->getLocale();
145+
$formatter = new \IntlDateFormatter(
146+
$locale,
147+
\IntlDateFormatter::SHORT,
148+
\IntlDateFormatter::NONE
149+
);
150+
$formatter->setPattern(DateTime::DATE_INTERNAL_FORMAT);
151+
$timestamp = $formatter->parse($stringValue);
152+
if ($timestamp !== false) {
153+
return new \DateTimeImmutable('@' . $timestamp);
154+
}
130155

131156
try {
132-
return new \DateTimeImmutable((string)$value);
157+
return new \DateTimeImmutable($stringValue);
133158
} catch (\Exception $e) {
134159
return null;
135160
}

app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,17 +447,14 @@ protected function getValidationRuleClass($type)
447447
$validationRule->expects($this->any())
448448
->method('getValue')
449449
->willReturn(strtotime(self::MIN_DATE));
450-
}
451-
elseif ($type=="MAX") {
450+
} elseif ($type=="MAX") {
452451
$validationRule->expects($this->any())
453452
->method('getName')
454453
->willReturn(Dob::MAX_DATE_RANGE_KEY);
455454
$validationRule->expects($this->any())
456455
->method('getValue')
457456
->willReturn(strtotime(self::MAX_DATE));
458457
}
459-
460-
461458
return $validationRule;
462459
}
463460

@@ -713,6 +710,25 @@ public static function getTranslatedCalendarConfigJsonDataProvider()
713710
'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"monthNamesShort":["Jan.","Feb.","M\u00e4rz","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"dayNames":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"dayNamesShort":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"dayNamesMin":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."]}'
714711
// phpcs:enable Generic.Files.LineLength.TooLong
715712
],
713+
[
714+
'locale' => 'ar_SA',
715+
'expectedArray' => [
716+
'closeText' => 'Done',
717+
'prevText' => 'Prev',
718+
'nextText' => 'Next',
719+
'currentText' => 'Today',
720+
'monthNames' => ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر',
721+
'أكتوبر', 'نوفمبر', 'ديسمبر'],
722+
'monthNamesShort' => ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس',
723+
'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'],
724+
'dayNames' => ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
725+
'dayNamesShort' => ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
726+
'dayNamesMin' => ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'],
727+
],
728+
// phpcs:disable Generic.Files.LineLength.TooLong
729+
'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"],"monthNamesShort":["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"],"dayNames":["\u0627\u0644\u0623\u062d\u062f","\u0627\u0644\u0627\u062b\u0646\u064a\u0646","\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621","\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621","\u0627\u0644\u062e\u0645\u064a\u0633","\u0627\u0644\u062c\u0645\u0639\u0629","\u0627\u0644\u0633\u0628\u062a"],"dayNamesShort":["\u0627\u0644\u0623\u062d\u062f","\u0627\u0644\u0627\u062b\u0646\u064a\u0646","\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621","\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621","\u0627\u0644\u062e\u0645\u064a\u0633","\u0627\u0644\u062c\u0645\u0639\u0629","\u0627\u0644\u0633\u0628\u062a"],"dayNamesMin":["\u0623\u062d\u062f","\u0625\u062b\u0646\u064a\u0646","\u062b\u0644\u0627\u062b\u0627\u0621","\u0623\u0631\u0628\u0639\u0627\u0621","\u062e\u0645\u064a\u0633","\u062c\u0645\u0639\u0629","\u0633\u0628\u062a"]}'
730+
// phpcs:enable Generic.Files.LineLength.TooLong
731+
],
716732
];
717733
}
718734
}

app/code/Magento/Customer/Test/Unit/Plugin/ValidateDobOnSaveTest.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
1818
use PHPUnit\Framework\MockObject\MockObject;
1919
use PHPUnit\Framework\TestCase;
20+
use Magento\Framework\Locale\ResolverInterface;
2021

2122
/**
2223
* Unit test for validate date of birth plugin
@@ -42,10 +43,13 @@ protected function setUp(): void
4243
$this->eavConfig = $this->createMock(EavConfig::class);
4344
$this->json = $this->createMock(JsonSerializer::class);
4445
$this->repo = $this->createMock(CustomerRepositoryInterface::class);
46+
$localeResolver = $this->createMock(ResolverInterface::class);
47+
$localeResolver->method('getLocale')->willReturn('en_US');
4548

4649
$this->plugin = new ValidateDobOnSave(
4750
$this->eavConfig,
48-
$this->json
51+
$this->json,
52+
$localeResolver
4953
);
5054
}
5155

@@ -322,6 +326,21 @@ public function testDobZeroEpochInvalidThrows(): void
322326
$this->assertFalse($called);
323327
}
324328

329+
public function testDobIsNormalized(): void
330+
{
331+
$dob = '2005-12-01';
332+
$customer = $this->createMock(CustomerInterface::class);
333+
$customer->method('getDob')->willReturn($dob);
334+
$customer->expects($this->once())
335+
->method('setDob')
336+
->with('2005-12-01');
337+
$this->mockAttributeRulesArray(['date_range_min' => '1980-01-01', 'date_range_max' => '2010-12-31']);
338+
$called = false;
339+
$proceed = $this->proceedPlugin($called, $customer);
340+
$this->plugin->aroundSave($this->repo, $proceed, $customer);
341+
$this->assertTrue($called);
342+
}
343+
325344
/**
326345
* Create a proceed closure that marks $called and returns either the given $result or the original $customer.
327346
*

app/code/Magento/PageCache/Model/App/Response/HttpPlugin.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ public function beforeSendResponse(HttpResponse $subject)
4343

4444
$currentVary = $this->context->getVaryString();
4545
$varyCookie = $this->request->get(HttpResponse::COOKIE_VARY_STRING);
46-
if ($currentVary !== $varyCookie) {
47-
//prevent caching with the old vary cookie
46+
if (isset($varyCookie) && ($currentVary !== $varyCookie)) {
4847
$subject->setNoCacheHeaders();
4948
}
5049
$subject->sendVary();

app/code/Magento/PageCache/Test/Unit/Model/App/Response/HttpPluginTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,16 @@ public function testBeforeSendResponseVaryMismatch()
9292

9393
$this->httpPlugin->beforeSendResponse($responseMock);
9494
}
95+
96+
public function testBeforeSendResponseVaryNotSet()
97+
{
98+
/** @var HttpResponse|MockObject $responseMock */
99+
$this->context->expects($this->any())->method('getVaryString')->willReturn('currentVary');
100+
$this->request->expects($this->any())->method('get')->willReturn(null);
101+
/** @var HttpResponse|MockObject $responseMock */
102+
$responseMock = $this->createMock(HttpResponse::class);
103+
$responseMock->expects($this->never())->method('setNoCacheHeaders');
104+
$responseMock->expects($this->once())->method('sendVary');
105+
$this->httpPlugin->beforeSendResponse($responseMock);
106+
}
95107
}

app/code/Magento/Ui/view/base/web/js/grid/columns/date.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ define([
4343
var date;
4444

4545
if (this.storeLocale !== undefined) {
46-
moment.locale(this.storeLocale, utils.extend({}, this.calendarConfig));
46+
moment.updateLocale(this.storeLocale, utils.extend({}, this.calendarConfig));
4747
}
4848

4949
date = moment.utc(this._super());

app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ protected function doFindOneByData(array $data)
112112
$result = null;
113113
$requestPath = $data[UrlRewrite::REQUEST_PATH];
114114
$decodedRequestPath = urldecode($requestPath);
115+
116+
// Validate UTF-8 encoding to prevent collation errors and reject malicious input
117+
if (!$this->isValidRequestPath($decodedRequestPath)) {
118+
return null;
119+
}
120+
115121
$data[UrlRewrite::REQUEST_PATH] = array_unique(
116122
[
117123
rtrim($requestPath, '/'),
@@ -130,6 +136,39 @@ protected function doFindOneByData(array $data)
130136
return $this->connection->fetchRow($this->prepareSelect($data));
131137
}
132138

139+
/**
140+
* Validate request path for valid UTF-8 and prevent collation errors
141+
*
142+
* This method checks for:
143+
* - Invalid UTF-8 sequences (e.g., overlong encodings like %c0%ae)
144+
* - 4-byte UTF-8 characters (emojis) that may cause collation mismatches
145+
* - Non-printable and control characters
146+
*
147+
* @param string $path
148+
* @return bool
149+
*/
150+
private function isValidRequestPath(string $path): bool
151+
{
152+
// Check for valid UTF-8 encoding
153+
if (!mb_check_encoding($path, 'UTF-8')) {
154+
return false;
155+
}
156+
157+
// Check for 4-byte UTF-8 characters (emojis, etc.) that can cause collation issues
158+
// 4-byte UTF-8 chars start with bytes 0xF0-0xF7 (11110xxx in binary)
159+
if (preg_match('/[\x{10000}-\x{10FFFF}]/u', $path)) {
160+
return false;
161+
}
162+
163+
// Check for control characters and other problematic characters
164+
// Allow: letters, numbers, hyphen, underscore, period, forward slash, and common URL-safe chars
165+
if (preg_match('/[\x00-\x1F\x7F]/', $path)) {
166+
return false;
167+
}
168+
169+
return true;
170+
}
171+
133172
/**
134173
* Extract most relevant url rewrite from url rewrites list
135174
*

0 commit comments

Comments
 (0)