diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
index ad400620af816..6ebb777f0bb44 100644
--- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
+++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
@@ -165,37 +165,34 @@ public function execute()
*/
private function sanitizeProductAttributes($attributesData)
{
+ $normalised = [];
foreach ($attributesData as $attributeCode => $value) {
if ($attributeCode === ProductAttributeInterface::CODE_HAS_WEIGHT) {
+ $normalised[$attributeCode] = $value;
continue;
}
-
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
-
if (!$attribute->getAttributeId()) {
- unset($attributesData[$attributeCode]);
continue;
}
+ $canonicalCode = $attribute->getAttributeCode();
if ($attribute->getBackendType() === 'datetime') {
- $attributesData[$attributeCode] = $this->filterDate(
+ $value = $this->filterDate(
$value,
$attribute->getFrontendInput() === 'datetime'
);
} elseif ($attribute->getFrontendInput() === 'multiselect') {
- // Check if 'Change' checkbox has been checked by admin for this attribute
- $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode);
+ $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $canonicalCode);
if (!$isChanged) {
- unset($attributesData[$attributeCode]);
continue;
}
- if (is_array($value)) {
- $value = implode(',', $value);
- }
- $attributesData[$attributeCode] = $value;
+ $value = is_array($value) ? implode(',', $value) : $value;
}
+
+ $normalised[$canonicalCode] = $value;
}
- return $attributesData;
+ return $normalised;
}
/**
diff --git a/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php
index e2a608f73c528..7f13ba7584dd4 100644
--- a/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php
+++ b/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php
@@ -9,15 +9,19 @@
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\State\ExpiredException;
use Magento\Framework\Phrase;
/**
* Get Customer By reset password token
+ *
* @SuppressWarnings(PHPMD.LongVariable)
* @deprecated Rp Tokens cannot be looked up directly
+ * @see not used anymore
*/
class GetCustomerByToken
{
@@ -32,17 +36,24 @@ class GetCustomerByToken
private $searchCriteriaBuilder;
/**
- * ForgotPassword constructor.
- *
+ * @var CollectionFactory
+ */
+ private $collectionFactory;
+
+ /**
* @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository
* @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
+ * @param CollectionFactory|null $collectionFactory
*/
public function __construct(
CustomerRepositoryInterface $customerRepository,
- SearchCriteriaBuilder $searchCriteriaBuilder
+ SearchCriteriaBuilder $searchCriteriaBuilder,
+ ?CollectionFactory $collectionFactory = null
) {
$this->customerRepository = $customerRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->collectionFactory = $collectionFactory
+ ?? ObjectManager::getInstance()->get(CollectionFactory::class);
}
/**
@@ -53,38 +64,27 @@ public function __construct(
* @return \Magento\Customer\Api\Data\CustomerInterface
* @throws ExpiredException
* @throws NoSuchEntityException
- * @throws \Magento\Framework\Exception\LocalizedException
*/
public function execute(string $resetPasswordToken): CustomerInterface
{
- $this->searchCriteriaBuilder->addFilter(
- 'rp_token',
- $resetPasswordToken
- );
- $this->searchCriteriaBuilder->setPageSize(1);
- $found = $this->customerRepository->getList(
- $this->searchCriteriaBuilder->create()
- );
+ $collection = $this->collectionFactory->create();
+ $collection->addAttributeToFilter('rp_token', $resetPasswordToken);
+ $size = $collection->getSize();
- if ($found->getTotalCount() > 1) {
- //Failed to generated unique RP token
+ if ($size > 1) {
throw new ExpiredException(
new Phrase('Reset password token expired.')
);
}
- if ($found->getTotalCount() === 0) {
- //Customer with such token not found.
+ if ($size === 0) {
throw new NoSuchEntityException(
new Phrase(
'No such entity with rp_token = %value',
- [
- 'value' => $resetPasswordToken
- ]
+ ['value' => $resetPasswordToken]
)
);
}
- //Unique customer found.
- return $found->getItems()[0];
+ return $collection->getFirstItem();
}
}
diff --git a/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php b/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php
new file mode 100644
index 0000000000000..764dc50b76add
--- /dev/null
+++ b/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php
@@ -0,0 +1,69 @@
+getFilterGroups() as $filterGroup) {
+ foreach ($filterGroup->getFilters() as $filter) {
+ if ($this->isRestricted($filter->getField())) {
+ throw new InputException(
+ __('The search criteria filter field is invalid.')
+ );
+ }
+ }
+ }
+ foreach ($searchCriteria->getSortOrders() ?? [] as $sortOrder) {
+ if ($this->isRestricted($sortOrder->getField())) {
+ throw new InputException(
+ __('The search criteria sort field is invalid.')
+ );
+ }
+ }
+ return [$searchCriteria];
+ }
+
+ /**
+ * Check if the given field name matches a restricted field
+ *
+ * @param string|null $field
+ * @return bool
+ */
+ private function isRestricted(?string $field): bool
+ {
+ return $field !== null && in_array(strtolower($field), $this->restrictedFields, true);
+ }
+}
diff --git a/vendor/magento/module-customer/etc/di.xml b/vendor/magento/module-customer/etc/di.xml
index f949e1f5aa1c2..a92555cb2e7d7 100644
--- a/vendor/magento/module-customer/etc/di.xml
+++ b/vendor/magento/module-customer/etc/di.xml
@@ -85,6 +85,15 @@
SessionValidator
+
+
+
+ - password_hash
+ - rp_token
+ - confirmation
+
+
+
Magento\Customer\Model\Address\Config\Proxy
diff --git a/vendor/magento/module-customer/etc/graphql/di.xml b/vendor/magento/module-customer/etc/graphql/di.xml
index ceca494c480e8..7d3acfc7fccdb 100644
--- a/vendor/magento/module-customer/etc/graphql/di.xml
+++ b/vendor/magento/module-customer/etc/graphql/di.xml
@@ -19,4 +19,7 @@
+
+
+
diff --git a/vendor/magento/module-customer/etc/webapi_rest/di.xml b/vendor/magento/module-customer/etc/webapi_rest/di.xml
index 033321ed9541c..00deea97cf69e 100644
--- a/vendor/magento/module-customer/etc/webapi_rest/di.xml
+++ b/vendor/magento/module-customer/etc/webapi_rest/di.xml
@@ -21,6 +21,7 @@
+
diff --git a/vendor/magento/module-customer/etc/webapi_soap/di.xml b/vendor/magento/module-customer/etc/webapi_soap/di.xml
index c71a1ab71174d..f5e5843db528e 100644
--- a/vendor/magento/module-customer/etc/webapi_soap/di.xml
+++ b/vendor/magento/module-customer/etc/webapi_soap/di.xml
@@ -18,6 +18,9 @@
+
+
+
diff --git a/vendor/magento/module-customer/i18n/en_US.csv b/vendor/magento/module-customer/i18n/en_US.csv
index 308cb9b05cb0e..b8c5e2a32bb54 100644
--- a/vendor/magento/module-customer/i18n/en_US.csv
+++ b/vendor/magento/module-customer/i18n/en_US.csv
@@ -551,4 +551,6 @@ T,T
F,F
VAT,VAT
"Something went wrong while logging out customer.","Something went wrong while logging out customer."
+"The search criteria filter field is invalid.","The search criteria filter field is invalid."
+"The search criteria sort field is invalid.","The search criteria sort field is invalid."
"Address not found.","Address not found."
diff --git a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php
index d2fdcb8e315fe..f234ce8312297 100644
--- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php
+++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php
@@ -7,6 +7,10 @@
namespace Magento\Email\Block\Adminhtml\Template;
+use Magento\Email\Model\Template\Validator as TemplateValidator;
+use Magento\Framework\Validation\ValidationException;
+use Magento\Framework\App\ObjectManager;
+
/**
* Email template preview block.
*
@@ -30,20 +34,29 @@ class Preview extends \Magento\Backend\Block\Widget
*/
protected $profilerName = 'email_template_proccessing';
+ /**
+ * @var TemplateValidator
+ */
+ private $templateValidator;
+
/**
* @param \Magento\Backend\Block\Template\Context $context
* @param \Magento\Framework\Filter\Input\MaliciousCode $maliciousCode
* @param \Magento\Email\Model\TemplateFactory $emailFactory
* @param array $data
+ * @param TemplateValidator $templateValidator
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Framework\Filter\Input\MaliciousCode $maliciousCode,
\Magento\Email\Model\TemplateFactory $emailFactory,
- array $data = []
+ array $data = [],
+ ?TemplateValidator $templateValidator = null
) {
$this->_maliciousCode = $maliciousCode;
$this->_emailFactory = $emailFactory;
+ $this->templateValidator = $templateValidator ?:
+ ObjectManager::getInstance()->get(TemplateValidator::class);
parent::__construct($context, $data);
}
@@ -64,10 +77,23 @@ protected function _toHtml()
if ($id = (int)$request->getParam('id')) {
$template->load($id);
} else {
+ $rawText = $request->getParam('text');
+ $rawText = is_string($rawText) ? $rawText : '';
+ $rawStyles = $request->getParam('styles');
+ $rawStyles = is_string($rawStyles) ? $rawStyles : '';
+ try {
+ $this->templateValidator->validate($rawText);
+ if ($rawStyles !== '') {
+ $this->templateValidator->validate($rawStyles);
+ }
+ } catch (ValidationException $e) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Template content validation failed. Please review template syntax.')
+ );
+ }
$template->setTemplateType($request->getParam('type'));
- $template->setTemplateText($this->_maliciousCode->filter($request->getParam('text')));
- $template->setTemplateStyles($request->getParam('styles'));
-
+ $template->setTemplateText($this->_maliciousCode->filter($rawText));
+ $template->setTemplateStyles($rawStyles);
}
\Magento\Framework\Profiler::start($this->profilerName);
diff --git a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php
index 7c5c973c60555..7c48928df3c9e 100644
--- a/vendor/magento/module-email/Model/AbstractTemplate.php
+++ b/vendor/magento/module-email/Model/AbstractTemplate.php
@@ -16,6 +16,8 @@
use Magento\Store\Model\Store;
use Magento\MediaStorage\Helper\File\Storage\Database;
use Magento\Framework\Validation\ValidationException;
+use Magento\Email\Model\Template\Validator as TemplateValidator;
+use Magento\Framework\App\ObjectManager;
/**
* Template model class.
@@ -168,6 +170,11 @@ abstract class AbstractTemplate extends AbstractModel implements TemplateTypesIn
*/
private $fileStorageDatabase;
+ /**
+ * @var TemplateValidator
+ */
+ private $templateValidator;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\View\DesignInterface $design
@@ -183,6 +190,7 @@ abstract class AbstractTemplate extends AbstractModel implements TemplateTypesIn
* @param \Magento\Framework\UrlInterface $urlModel
* @param array $data
* @param Database $fileStorageDatabase
+ * @param TemplateValidator|null $templateValidator
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -200,7 +208,8 @@ public function __construct(
\Magento\Framework\Filter\FilterManager $filterManager,
\Magento\Framework\UrlInterface $urlModel,
array $data = [],
- ?Database $fileStorageDatabase = null
+ ?Database $fileStorageDatabase = null,
+ ?TemplateValidator $templateValidator = null
) {
$this->design = $design;
$this->area = isset($data['area']) ? $data['area'] : null;
@@ -216,6 +225,8 @@ public function __construct(
$this->urlModel = $urlModel;
$this->fileStorageDatabase = $fileStorageDatabase ?:
\Magento\Framework\App\ObjectManager::getInstance()->get(Database::class);
+ $this->templateValidator = $templateValidator ?:
+ ObjectManager::getInstance()->get(TemplateValidator::class);
parent::__construct($context, $registry, null, null, $data);
}
@@ -772,7 +783,7 @@ public function beforeSave()
$templateText = $this->getTemplateText();
if ($templateText) {
try {
- $this->validateTemplateText($templateText);
+ $this->templateValidator->validate($templateText);
} catch (ValidationException $exception) {
throw new ValidationException(
__('Content field contains restricted HTML elements. %1', $exception->getMessage()),
@@ -780,27 +791,18 @@ public function beforeSave()
);
}
}
+ $templateStyles = $this->getTemplateStyles();
+ if ($templateStyles) {
+ try {
+ $this->templateValidator->validate($templateStyles);
+ } catch (ValidationException $exception) {
+ throw new ValidationException(
+ __('Styles field validation failed. %1', $exception->getMessage()),
+ $exception
+ );
+ }
+ }
parent::beforeSave();
return $this;
}
-
- /**
- * Validate template text
- *
- * @param string $content
- * @throws ValidationException
- */
- private function validateTemplateText(string $content): void
- {
- $expression = '/\b(?:src|href|action)\s*=\s*([\'"])(?:[^\'"]*?)https?:\/\/[^\/\s]+\/' .
- '(?:[^\'"]*?)\{\{\s*[^}]+\s*\}\}(?:[^\'"]*?)\1/iu';
-
- if (preg_match($expression, $content)) {
- throw new ValidationException(
- __(
- 'Template directives are not allowed in external URLs.'
- )
- );
- }
- }
}
diff --git a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php
index f86deacad3b4e..70248cec31d6e 100644
--- a/vendor/magento/module-email/Model/Template/Filter.php
+++ b/vendor/magento/module-email/Model/Template/Filter.php
@@ -980,7 +980,14 @@ public function inlinecssDirective($construction)
// If this template is a child of another template, skip processing so that the parent template will process
// this directive. This is important as CSS inlining must operate on the entire HTML document.
if ($this->isChildTemplate()) {
- return $construction[0];
+ try {
+ return $this->deferToParent($construction[0]);
+ } catch (\Exception $e) {
+ throw new MailException(
+ __('Unable to defer the "inlinecss" directive for later processing.'),
+ $e
+ );
+ }
}
$params = $this->getParameters($construction[2]);
diff --git a/vendor/magento/module-email/Model/Template/Validator.php b/vendor/magento/module-email/Model/Template/Validator.php
new file mode 100644
index 0000000000000..3173d7a57d8ab
--- /dev/null
+++ b/vendor/magento/module-email/Model/Template/Validator.php
@@ -0,0 +1,96 @@
+`])+))/iu';
+
+ /**
+ * Regular expressions for CSS url argument containing a template directive
+ *
+ * @var string
+ */
+ private const CSS_URL_VALUE_EXPRESSION =
+ '/\burl\(\s*(?|"([^"]*\{\{[^}]*\}\}[^"]*)"' .
+ '|\'([^\']*\{\{[^}]*\}\}[^\']*)\'' .
+ '|((?:\{\{[^}]*\}\}|[^\s"\')])+))\s*\)/iu';
+
+ /**
+ * Validate template content
+ *
+ * @param string $content
+ *
+ * @throws ValidationException
+ */
+ public function validate(string $content): void
+ {
+ foreach ([self::ATTRIBUTE_VALUE_EXPRESSION, self::CSS_URL_VALUE_EXPRESSION] as $expression) {
+ $matchCount = preg_match_all($expression, $content, $matches);
+ if ($matchCount === false) {
+ throw new ValidationException(
+ __('Template content validation failed. Please review template syntax.')
+ );
+ }
+ foreach ($matches[1] as $value) {
+ if ($this->isForbiddenUrlValue($value)) {
+ throw new ValidationException(
+ __('Template content validation failed. Please review template syntax.')
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Check whether a URL-bearing value resolves to an absolute external target
+ *
+ * @param string $value
+ * @return bool
+ */
+ private function isForbiddenUrlValue(string $value): bool
+ {
+ $normalized = $this->normalizeUrlValue($value);
+ if (strpos($normalized, '{{') === false) {
+ return false;
+ }
+
+ return preg_match('#^https?:/#i', $normalized) === 1
+ || strncmp($normalized, '//', 2) === 0;
+ }
+
+ /**
+ * Normalize a URL-bearing value
+ *
+ * @param string $value
+ * @return string
+ */
+ private function normalizeUrlValue(string $value): string
+ {
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ $value = str_ireplace(['%2f', '%5c'], ['/', '\\'], $value);
+ $value = str_replace('\\', '/', $value);
+
+ return preg_replace('/[\x00-\x20]+/', '', $value) ?? $value;
+ }
+}
diff --git a/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php b/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php
new file mode 100644
index 0000000000000..5ba173320b2ad
--- /dev/null
+++ b/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php
@@ -0,0 +1,58 @@
+persistentHelper->isShoppingCartPersist()
+ && !$this->customerSession->isLoggedIn()
+ && $this->persistentSession->isPersistent()
+ && (int) $this->persistentSession->getSession()->getCustomerId() === $customerId;
+
+ if (!$isOwnedByPersistedCustomer) {
+ $proceed($customerId, $maskedCartId);
+ }
+ }
+}
diff --git a/vendor/magento/module-persistent/etc/di.xml b/vendor/magento/module-persistent/etc/di.xml
index c2a6be533e8ea..cdc03115e443b 100644
--- a/vendor/magento/module-persistent/etc/di.xml
+++ b/vendor/magento/module-persistent/etc/di.xml
@@ -25,6 +25,9 @@
+
+
+
500
diff --git a/vendor/magento/framework/Escaper.php b/vendor/magento/framework/Escaper.php
index 26a1a57adc90b..a1bc7b34473e8 100644
--- a/vendor/magento/framework/Escaper.php
+++ b/vendor/magento/framework/Escaper.php
@@ -389,8 +389,9 @@ public function escapeJsQuote($data, $quote = '\'')
*/
public function escapeXssInUrl($data)
{
- $data = $this->decodeHtmlEntitiesToFixedPoint((string)$data);
+ $data = (string)$data;
$this->getTranslateInline()->processResponseBody($data);
+ $data = $this->decodeHtmlEntitiesToFixedPoint($data);
return htmlspecialchars(
$this->escapeScriptIdentifiers($data),
diff --git a/vendor/magento/framework/Filter/Template.php b/vendor/magento/framework/Filter/Template.php
index 3476aceba1217..2ee66c206d835 100644
--- a/vendor/magento/framework/Filter/Template.php
+++ b/vendor/magento/framework/Filter/Template.php
@@ -18,6 +18,7 @@
use Magento\Framework\Stdlib\StringUtils;
use Magento\Framework\Filter\Template\SignatureProvider;
use Magento\Framework\Filter\Template\FilteringDepthMeter;
+use Magento\Framework\Filter\Template\DeferredDirectiveRegistry;
/**
* Template constructions filter
@@ -102,6 +103,8 @@ class Template implements FilterInterface
/**
* @var SignatureProvider|null
+ * @deprecated Signature-based deferral was superseded by out-of-band deferral via DeferredDirectiveRegistry.
+ * @see DeferredDirectiveRegistry
*/
private $signatureProvider;
@@ -110,6 +113,11 @@ class Template implements FilterInterface
*/
private $filteringDepthMeter;
+ /**
+ * @var DeferredDirectiveRegistry|null
+ */
+ private $deferredDirectiveRegistry;
+
/**
* @param StringUtils $string
* @param array $variables
@@ -117,6 +125,7 @@ class Template implements FilterInterface
* @param VariableResolverInterface|null $variableResolver
* @param SignatureProvider|null $signatureProvider
* @param FilteringDepthMeter|null $filteringDepthMeter
+ * @param DeferredDirectiveRegistry|null $deferredDirectiveRegistry
*/
public function __construct(
StringUtils $string,
@@ -124,7 +133,8 @@ public function __construct(
$directiveProcessors = [],
?VariableResolverInterface $variableResolver = null,
?SignatureProvider $signatureProvider = null,
- ?FilteringDepthMeter $filteringDepthMeter = null
+ ?FilteringDepthMeter $filteringDepthMeter = null,
+ ?DeferredDirectiveRegistry $deferredDirectiveRegistry = null
) {
$this->string = $string;
$this->setVariables($variables);
@@ -138,6 +148,9 @@ public function __construct(
$this->filteringDepthMeter = $filteringDepthMeter ?? ObjectManager::getInstance()
->get(FilteringDepthMeter::class);
+ $this->deferredDirectiveRegistry = $deferredDirectiveRegistry ?? ObjectManager::getInstance()
+ ->get(DeferredDirectiveRegistry::class);
+
if (empty($directiveProcessors)) {
$this->directiveProcessors = [
'depend' => ObjectManager::getInstance()->get(DependDirective::class),
@@ -204,35 +217,11 @@ public function filter($value)
$this->filteringDepthMeter->descend();
// Processing of template directives.
- $templateDirectivesResults = array_unique(
- $this->processDirectives($value),
- SORT_REGULAR
- );
-
- $value = $this->applyDirectivesResults($value, $templateDirectivesResults);
-
- // Processing of deferred directives received from child templates
- // or nested directives.
- $deferredDirectivesResults = array_unique(
- $this->processDirectives($value, true),
- SORT_REGULAR
- );
-
- $value = $this->applyDirectivesResults($value, $deferredDirectivesResults);
-
- if ($this->filteringDepthMeter->showMark() > 1) {
- // Signing own deferred directives (if any).
- $signature = $this->signatureProvider->get();
-
- foreach ($templateDirectivesResults as $result) {
- if ($result['directive'] === $result['output']) {
- $value = str_replace(
- $result['output'],
- $signature . $result['output'] . $signature,
- $value
- );
- }
- }
+ $value = $this->resolveDirectives($value);
+
+ // At the root template, resolve directives that child templates deferred to it.
+ if ($this->filteringDepthMeter->showMark() === 1) {
+ $value = $this->resolveDeferredDirectives($value);
}
$value = $this->afterFilter($value);
@@ -243,17 +232,50 @@ public function filter($value)
}
/**
- * Processes template directives and returns an array that contains results produced by each directive.
+ * Defers a directive to the root template and returns the placeholder that represents it.
+ *
+ * @param string $directive
+ * @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ protected function deferToParent(string $directive): string
+ {
+ return $this->deferredDirectiveRegistry->defer($directive);
+ }
+
+ /**
+ * Resolves directives that child templates deferred to the root template.
+ *
+ * @param string $value
+ * @return string
+ * @throws \Exception
+ */
+ private function resolveDeferredDirectives(string $value): string
+ {
+ foreach ($this->deferredDirectiveRegistry->getAll() as $placeholder => $directive) {
+ if (strpos($value, $placeholder) === false) {
+ continue;
+ }
+
+ $value = str_replace($placeholder, $this->resolveDirectives($directive), $value);
+ }
+
+ $this->deferredDirectiveRegistry->clear();
+
+ return $value;
+ }
+
+ /**
+ * Processes directives and applies the results produced by each directive to the given value.
*
* @param string $value
- * @param bool $isSigned
*
- * @return array
+ * @return string
*
* @throws InvalidArgumentException
* @throws \Magento\Framework\Exception\LocalizedException
*/
- private function processDirectives($value, $isSigned = false): array
+ private function resolveDirectives(string $value): string
{
$results = [];
@@ -266,10 +288,6 @@ private function processDirectives($value, $isSigned = false): array
$pattern = $directiveProcessor->getRegularExpression();
- if ($isSigned) {
- $pattern = $this->embedSignatureIntoPattern($pattern);
- }
-
if (preg_match_all($pattern, $value, $constructions, PREG_SET_ORDER)) {
foreach ($constructions as $construction) {
$replacedValue = $directiveProcessor->process($construction, $this, $this->templateVars);
@@ -290,7 +308,7 @@ private function processDirectives($value, $isSigned = false): array
}
}
- return $results;
+ return $this->applyDirectivesResults($value, array_unique($results, SORT_REGULAR));
}
/**
@@ -328,38 +346,6 @@ private function applyDirectivesResults(string $value, array $results): string
return $value;
}
- /**
- * Modifies given regular expression pattern to be able to recognize signed directives.
- *
- * @param string $pattern
- *
- * @return string
- *
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- private function embedSignatureIntoPattern(string $pattern): string
- {
- $signature = $this->signatureProvider->get();
-
- $closingDelimiters = [
- '(' => ')',
- '{' => '}',
- '[' => ']',
- '<' => '>'
- ];
-
- $closingDelimiter = $openingDelimiter = substr(trim($pattern), 0, 1);
-
- if (array_key_exists($openingDelimiter, $closingDelimiters)) {
- $closingDelimiter = $closingDelimiters[$openingDelimiter];
- }
-
- $pattern = substr_replace($pattern, $signature, strpos($pattern, $openingDelimiter) + 1, 0);
- $pattern = substr_replace($pattern, $signature, strrpos($pattern, $closingDelimiter), 0);
-
- return $pattern;
- }
-
/**
* Runs callbacks that have been added to filter content after directive processing is finished.
*
diff --git a/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php
new file mode 100644
index 0000000000000..cc33f04824b77
--- /dev/null
+++ b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php
@@ -0,0 +1,102 @@
+ deferred directive text.
+ *
+ * @var string[]
+ */
+ private array $directives = [];
+
+ /**
+ * Per-request secret salt used to derive placeholder tokens deterministically.
+ *
+ * @var string|null
+ */
+ private ?string $salt = null;
+
+ /**
+ * @param Random $random
+ */
+ public function __construct(Random $random)
+ {
+ $this->random = $random;
+ }
+
+ /**
+ * Register a directive as deferred and return the placeholder token that represents it.
+ *
+ * @param string $directive
+ * @return string
+ * @throws LocalizedException
+ */
+ public function defer(string $directive): string
+ {
+ if ($this->salt === null) {
+ $this->salt = $this->random->getRandomString(self::TOKEN_LENGTH);
+ }
+
+ $placeholder = ':deferred:' . hash('sha256', $this->salt . $directive) . ':';
+
+ $this->directives[$placeholder] = $directive;
+
+ return $placeholder;
+ }
+
+ /**
+ * Get all registered deferrals keyed by their placeholder token.
+ *
+ * @return string[]
+ */
+ public function getAll(): array
+ {
+ return $this->directives;
+ }
+
+ /**
+ * Forget all registered deferrals.
+ *
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->directives = [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->directives = [];
+ $this->salt = null;
+ }
+}
diff --git a/vendor/magento/framework/Filter/Template/SignatureProvider.php b/vendor/magento/framework/Filter/Template/SignatureProvider.php
index 09a6c5b430555..87adea0bf03c5 100644
--- a/vendor/magento/framework/Filter/Template/SignatureProvider.php
+++ b/vendor/magento/framework/Filter/Template/SignatureProvider.php
@@ -15,6 +15,10 @@
* Provides a signature which should be used to sign deferred directives
* (directives that should be processed in scope of a parent template
* instead of own scope, e.g. {{inlinecss}}).
+ *
+ * @deprecated Signature-based deferral was superseded by out-of-band deferral via
+ * \Magento\Framework\Filter\Template\DeferredDirectiveRegistry.
+ * @see \Magento\Framework\Filter\Template\DeferredDirectiveRegistry
*/
class SignatureProvider implements ResetAfterRequestInterface
{
diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php
index 69ffa04be8e70..83a19a7043c77 100644
--- a/vendor/magento/framework/Lock/Backend/FileLock.php
+++ b/vendor/magento/framework/Lock/Backend/FileLock.php
@@ -133,7 +133,7 @@ public function cleanupOldLocks(): int
continue;
}
- if ($this->isLocked(basename($lockFile))) {
+ if ($this->isLockFileActive($lockFile)) {
continue;
}
@@ -157,25 +157,7 @@ public function cleanupOldLocks(): int
*/
public function isLocked(string $name): bool
{
- $lockFile = $this->getLockPath($name);
- $result = false;
-
- try {
- if ($this->fileDriver->isExists($lockFile)) {
- $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+');
- if ($this->tryToLock($fileResource)) {
- $result = false;
- } else {
- $result = true;
- }
- $this->tryToUnlock($fileResource);
- $this->fileDriver->fileClose($fileResource);
- }
- } catch (FileSystemException $exception) {
- throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception);
- }
-
- return $result;
+ return $this->isFileLocked($this->getLockPath($name));
}
/**
@@ -204,7 +186,51 @@ public function unlock(string $name): bool
*/
private function getLockPath(string $name): string
{
- return $this->path . $name;
+ return $this->path . hash('sha256', $name);
+ }
+
+ /**
+ * Checks whether a lock file at the given absolute path is currently held
+ *
+ * @param string $lockFile Absolute path to the lock file
+ * @return bool
+ * @throws RuntimeException
+ */
+ private function isFileLocked(string $lockFile): bool
+ {
+ $result = false;
+
+ try {
+ if ($this->fileDriver->isExists($lockFile)) {
+ $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+');
+ if ($this->tryToLock($fileResource)) {
+ $result = false;
+ } else {
+ $result = true;
+ }
+ $this->tryToUnlock($fileResource);
+ $this->fileDriver->fileClose($fileResource);
+ }
+ } catch (FileSystemException $exception) {
+ throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks whether a lock file at the given absolute path is actively held.
+ *
+ * @param string $lockFilePath Absolute path to the lock file
+ * @return bool
+ */
+ private function isLockFileActive(string $lockFilePath): bool
+ {
+ try {
+ return $this->isFileLocked($lockFilePath);
+ } catch (RuntimeException) {
+ return true;
+ }
}
/**
diff --git a/vendor/magento/framework/Validator/EmailAddress.php b/vendor/magento/framework/Validator/EmailAddress.php
index 1a607f129877c..ddb35bb07d978 100644
--- a/vendor/magento/framework/Validator/EmailAddress.php
+++ b/vendor/magento/framework/Validator/EmailAddress.php
@@ -9,6 +9,13 @@
class EmailAddress extends LaminasEmailAddress implements ValidatorInterface
{
+ /**
+ * Quoted Pattern
+ *
+ * @var string
+ */
+ private const QUOTED_LOCAL_PART_PATTERN = '/^"/';
+
/**
* @var string[]
*/
@@ -55,4 +62,18 @@ public function setValidateTld(bool $shouldValidate)
{
$this->getHostnameValidator()->setOptions(['useTldCheck' => $shouldValidate]);
}
+
+ /**
+ * @inheritdoc
+ */
+ public function isValid($value)
+ {
+ if (is_string($value) && preg_match(self::QUOTED_LOCAL_PART_PATTERN, $value)) {
+ $this->setValue($value);
+ $this->error(self::INVALID_FORMAT);
+ return false;
+ }
+
+ return parent::isValid($value);
+ }
}
diff --git a/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php
index f3b3a6bc9216e..2061228d568cc 100644
--- a/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php
+++ b/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php
@@ -24,12 +24,19 @@ class ConfigurableWYSIWYGValidator implements WYSIWYGValidatorInterface
. '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i';
/**
- * Pattern to detect malformed syntax
+ * Pattern to detect malformed or unsupported markup syntax.
*
* @var string
*/
private static string $contentFiltrationPattern =
- '#<\w+[^>]*/[^\s/>]+/[^>]*=|<\w+/[^\s>]*on\w+\s*=|<\w+[^>]*//[^>]*=|('
+ . '|<\s*<[a-zA-Z]'
+ . '|\x00'
+ . '|<\w+[^>]*/[^\s/>]+/[^>]*='
+ . '|<\w+/[^\s>]*on\w+\s*='
+ . '|<\w+[^>]*//[^>]*='
+ . '|(? 1;
- $hasOtherMalicious = count($matches[0]) > 0 && !isset($valueCounts['loadHTML($content, LIBXML_HTML_NOIMPLIED);
restore_error_handler();
diff --git a/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php b/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php
index 3ac2c9135ea09..7d9d4577a22a3 100644
--- a/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php
+++ b/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php
@@ -50,7 +50,7 @@ public function sanitize(array $data): array
} elseif (!is_bool($config)
&& !array_key_exists($key, $config)
&& (is_string($datum) || $datum instanceof Phrase)
- && preg_match('/\$\{.+\}/s', (string)$datum)
+ && $this->validateSyntax((string)$datum)
) {
//Templating is not disabled for all properties or for this property specifically
//Property is a string that contains template syntax, so we are disabling its rendering
@@ -65,6 +65,17 @@ public function sanitize(array $data): array
return $data;
}
+ /**
+ * Validate value syntax.
+ *
+ * @param string $value
+ * @return bool
+ */
+ private function validateSyntax(string $value): bool
+ {
+ return strpos($value, '${') !== false && strpos($value, '${{') === false;
+ }
+
/**
* Sanitize a component's metadata.
*