diff --git a/composer.json b/composer.json
index d1976513..6546c92a 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"name": "magento/quality-patches",
"description": "Provides quality patches for Magento 2",
"type": "magento2-component",
- "version": "1.0.25",
+ "version": "1.0.26",
"license": "proprietary",
"repositories": {
"repo": {
diff --git a/patches.json b/patches.json
index 31335f3f..3fe9379c 100644
--- a/patches.json
+++ b/patches.json
@@ -441,6 +441,9 @@
">=2.3.2 <=2.3.3-p1": {
"file": "os/MDVA-30565_2.3.3-p1.patch"
},
+ ">=2.3.4 <=2.3.4-p2": {
+ "file": "os/MDVA-38531_2.3.4-p2.patch"
+ },
">=2.3.6 <2.4.0 || >=2.4.1 <2.4.2": {
"file": "os/MDVA-35423_2.4.1.patch"
}
@@ -1995,6 +1998,9 @@
">=2.3.4 <=2.3.4-p2": {
"file": "os/MDVA-34665_2.3.4-p2_v2.patch"
},
+ ">=2.3.5 <=2.3.5-p2": {
+ "file": "os/MDVA-38535_2.3.5-p1.patch"
+ },
">=2.4.0 <2.4.3": {
"file": "os/MDVA-37350_2.4.1.patch"
}
@@ -2011,6 +2017,9 @@
"Fixes the issue with missing bundled products on category pages.": {
"1.1.4": {
"file": "commerce/MDVA-34665_1.1.4_v2.patch"
+ },
+ ">=1.1.5 <=1.1.5-p1": {
+ "file": "commerce/MDVA-38535_1.1.5.patch"
}
}
},
@@ -2370,5 +2379,61 @@
}
}
}
+ },
+ "MDVA-38468": {
+ "magento/magento2-base": {
+ "Fixes the error when saving CMS pages - Item with the same ID 'PAGE_ID' already exists.": {
+ ">=2.3.2 <=2.3.5-p2": {
+ "file": "os/MDVA-38468_2.3.2-p2.patch"
+ }
+ }
+ },
+ "magento/magento2-ee-base": {
+ "Fixes the error when saving CMS pages - Item with the same ID 'PAGE_ID' already exists.": {
+ ">=2.3.2 <=2.3.5-p2": {
+ "file": "commerce/MDVA-38468_2.3.2-p2.patch"
+ }
+ }
+ }
+ },
+ "MDVA-34680": {
+ "magento/magento2-base": {
+ "Fixes the issue where Customer Account created time is not filtered correctly in customers grid.": {
+ ">=2.3.6 <=2.3.7 || >=2.4.1 <2.4.3": {
+ "file": "os/MDVA-34680_2.4.1.patch"
+ }
+ }
+ }
+ },
+ "MDVA-37068": {
+ "magento/magento2-base": {
+ "Fixes the issue where the incorrect tax rate displays when the shopping cart has only virtual products.": {
+ ">=2.3.1 <2.4.4": {
+ "file": "os/MDVA-37068_2.3.5-p2.patch"
+ }
+ }
+ }
+ },
+ "MDVA-38608": {
+ "magento/magento2-base": {
+ "Fixes the issue where temporary tables are not deleted when the reindex is not finished successfully.": {
+ ">=2.3.0 <2.4.3": {
+ "file": "os/MDVA-38608_2.3.2-p2.patch"
+ }
+ }
+ }
+ },
+ "MDVA-38308": {
+ "magento/magento2-base": {
+ "Fixes the issues related to adding Vimeo videos to products.": {
+ ">=2.3.5 <=2.3.6-p1 || >=2.4.0 <=2.4.1-p1": {
+ "file": "os/MDVA-38308_2.4.1-p1.patch",
+ "require": ["MDVA-35092"]
+ },
+ ">=2.4.2 <2.4.4": {
+ "file": "os/MDVA-38308_2.4.2-p1.patch"
+ }
+ }
+ }
}
}
diff --git a/patches/commerce/MDVA-38468_2.3.2-p2.patch b/patches/commerce/MDVA-38468_2.3.2-p2.patch
new file mode 100644
index 00000000..124b3585
--- /dev/null
+++ b/patches/commerce/MDVA-38468_2.3.2-p2.patch
@@ -0,0 +1,457 @@
+diff --git a/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Hierarchy/Edit/Form.php b/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Hierarchy/Edit/Form.php
+index ec424200e96..578d660a6f2 100644
+--- a/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Hierarchy/Edit/Form.php
++++ b/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Hierarchy/Edit/Form.php
+@@ -527,10 +527,8 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic
+ $this->setData('current_scope_id', $nodeModel->getScopeId());
+
+ $this->setData('use_default_scope', $nodeModel->getIsInherited());
+- $nodeHeritageModel = $nodeModel->getHeritage();
+- $nodes = $nodeHeritageModel->getNodesData();
++ $nodes = $nodeModel->getNodesData();
+ unset($nodeModel);
+- unset($nodeHeritageModel);
+
+ foreach ($nodes as &$node) {
+ $node['assigned_to_store'] = !$this->getData('use_default_scope');
+diff --git a/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Page/Edit/Tab/Hierarchy.php b/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Page/Edit/Tab/Hierarchy.php
+index d70a5f5b70b..eb5343be0af 100644
+--- a/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Page/Edit/Tab/Hierarchy.php
++++ b/vendor/magento/module-versions-cms/Block/Adminhtml/Cms/Page/Edit/Tab/Hierarchy.php
+@@ -5,6 +5,8 @@
+ */
+ namespace Magento\VersionsCms\Block\Adminhtml\Cms\Page\Edit\Tab;
+
++use Magento\Store\Model\StoreManagerInterface;
++
+ /**
+ * Cms Page Edit Hierarchy Tab Block
+ */
+@@ -52,12 +54,18 @@ class Hierarchy extends \Magento\Backend\Block\Template implements \Magento\Back
+ protected $_template = 'page/tab/hierarchy.phtml';
+
+ /**
++ * @var StoreManagerInterface $storeManager
++ */
++ private $storeManager;
++
++ /**
+ * @param \Magento\Backend\Block\Template\Context $context
+ * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder
+ * @param \Magento\Framework\Json\DecoderInterface $jsonDecoder
+ * @param \Magento\VersionsCms\Helper\Hierarchy $cmsHierarchy
+ * @param \Magento\Framework\Registry $registry
+ * @param \Magento\VersionsCms\Model\ResourceModel\Hierarchy\Node\CollectionFactory $nodeCollectionFactory
++ * @param StoreManagerInterface $storeManager
+ * @param array $data
+ */
+ public function __construct(
+@@ -67,6 +75,7 @@ class Hierarchy extends \Magento\Backend\Block\Template implements \Magento\Back
+ \Magento\VersionsCms\Helper\Hierarchy $cmsHierarchy,
+ \Magento\Framework\Registry $registry,
+ \Magento\VersionsCms\Model\ResourceModel\Hierarchy\Node\CollectionFactory $nodeCollectionFactory,
++ StoreManagerInterface $storeManager,
+ array $data = []
+ ) {
+ $this->_jsonDecoder = $jsonDecoder;
+@@ -74,6 +83,7 @@ class Hierarchy extends \Magento\Backend\Block\Template implements \Magento\Back
+ $this->_coreRegistry = $registry;
+ $this->_cmsHierarchy = $cmsHierarchy;
+ $this->_nodeCollectionFactory = $nodeCollectionFactory;
++ $this->storeManager = $storeManager;
+ parent::__construct($context, $data);
+ }
+
+@@ -159,6 +169,7 @@ class Hierarchy extends \Magento\Backend\Block\Template implements \Magento\Back
+ 'node_id' => $item->getId(),
+ 'parent_node_id' => $item->getParentNodeId(),
+ 'label' => $item->getLabel(),
++ 'store_label' => $this->getNodeStoreName((int)$item->getScopeId()),
+ 'page_exists' => (bool)$item->getPageExists(),
+ 'page_id' => $item->getPageId(),
+ 'current_page' => (bool)$item->getCurrentPage(),
+@@ -172,6 +183,22 @@ class Hierarchy extends \Magento\Backend\Block\Template implements \Magento\Back
+ }
+
+ /**
++ * Return store name for node by scope_id
++ *
++ * @param int $scopeId
++ * @return string
++ * @throws \Magento\Framework\Exception\NoSuchEntityException
++ */
++ private function getNodeStoreName(int $scopeId)
++ {
++ $scope = $this->storeManager->getStore($scopeId);
++ if ($scope->getId() === '0') {
++ return 'All Store Views';
++ }
++ return $scope->getName();
++ }
++
++ /**
+ * Return page store ids.
+ *
+ * @param object $node
+diff --git a/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node.php b/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node.php
+index 5c366985940..22d59d7953a 100644
+--- a/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node.php
++++ b/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node.php
+@@ -12,6 +12,7 @@ use Magento\VersionsCms\Model\Hierarchy\NodeFactory;
+
+ /**
+ * Cms Hierarchy Pages Node Resource Model
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ */
+ class Node extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ {
+@@ -309,6 +310,7 @@ class Node extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ $nodes = [];
+ $rowSet = $this->getConnection()->fetchAll($select);
+ foreach ($rowSet as $row) {
++ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $nodes[intval($row['parent_node_id'])][$row[$this->getIdFieldName()]] = $row;
+ }
+
+@@ -779,23 +781,25 @@ class Node extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+
+ /**
+ * Remove node which are representing specified page from defined nodes.
++ *
+ * Which will also remove child nodes by foreign key.
+ *
+ * @param int $pageId
+ * @param int|array $nodes
++ * @param array $scopeIds
+ * @return $this
+ */
+- public function removePageFromNodes($pageId, $nodes)
++ public function removePageFromNodes($pageId, $nodes, $scopeIds = [])
+ {
+- $whereClause = ['page_id = ?' => $pageId, 'parent_node_id IN (?)' => $nodes];
++ $whereClause = empty($scopeIds) ? ['page_id = ?' => $pageId, 'parent_node_id IN (?)' => $nodes]
++ : ['page_id = ?' => $pageId, 'parent_node_id IN (?)' => $nodes, 'scope_id IN (?)' => $scopeIds];
+ $this->getConnection()->delete($this->getMainTable(), $whereClause);
+
+ return $this;
+ }
+
+ /**
+- * Remove nodes defined by id.
+- * Which will also remove their child nodes by foreign key.
++ * Remove nodes defined by id. Which will also remove their child nodes by foreign key.
+ *
+ * @param int|int[] $nodeIds
+ * @return $this
+@@ -807,8 +811,7 @@ class Node extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ }
+
+ /**
+- * Retrieve tree meta data flags from secondary table.
+- * Filtering by root node of passed node.
++ * Retrieve tree meta data flags from secondary table. Filtering by root node of passed node.
+ *
+ * @param \Magento\VersionsCms\Model\Hierarchy\Node $object
+ * @return array
+@@ -824,8 +827,7 @@ class Node extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ }
+
+ /**
+- * Prepare load select but without where part.
+- * So all extra joins to secondary tables will be present.
++ * Prepare load select but without where part. So all extra joins to secondary tables will be present.
+ *
+ * @return \Magento\Framework\DB\Select
+ */
+diff --git a/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node/Collection.php b/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node/Collection.php
+index 2de683d3cd9..3b3be656906 100644
+--- a/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node/Collection.php
++++ b/vendor/magento/module-versions-cms/Model/ResourceModel/Hierarchy/Node/Collection.php
+@@ -245,6 +245,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab
+ $connection->quoteInto($onClause, $page),
+ ['page_exists' => $ifPageExistExpr, 'current_page' => $ifCurrentPageExpr]
+ );
++ $this->getSelect()->group('main_table.node_id');
+
+ $this->setFlag('page_exists_joined', true);
+ }
+diff --git a/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveAfterObserver.php b/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveAfterObserver.php
+index 6029e1dffe2..e83e6fe6b1f 100644
+--- a/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveAfterObserver.php
++++ b/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveAfterObserver.php
+@@ -8,10 +8,18 @@ namespace Magento\VersionsCms\Observer\Backend;
+ use Magento\Cms\Model\Page;
+ use Magento\Framework\Event\Observer as EventObserver;
+ use Magento\Framework\Event\ObserverInterface;
++use Magento\Framework\Exception\LocalizedException;
++use Magento\Store\Model\ScopeInterface;
++use Magento\Store\Model\ScopeResolver;
+ use Magento\VersionsCms\Helper\Hierarchy;
+ use Magento\VersionsCms\Model\Hierarchy\Node as HierarchyNode;
+ use Magento\VersionsCms\Model\ResourceModel\Hierarchy\Node;
++use Magento\VersionsCms\Model\ResourceModel\Hierarchy\Node\Collection;
++use Magento\VersionsCms\Model\ResourceModel\Hierarchy\Node\CollectionFactory;
+
++/**
++ * Create and delete nodes after cms page save
++ */
+ class CmsPageSaveAfterObserver implements ObserverInterface
+ {
+ /**
+@@ -30,18 +38,34 @@ class CmsPageSaveAfterObserver implements ObserverInterface
+ protected $hierarchyNodeResource;
+
+ /**
++ * @var CollectionFactory
++ */
++ private $nodeCollectionFactory;
++
++ /**
++ * @var ScopeResolver
++ */
++ private $scopeResolver;
++
++ /**
+ * @param Hierarchy $cmsHierarchy
+ * @param HierarchyNode $hierarchyNode
+ * @param Node $hierarchyNodeResource
++ * @param CollectionFactory $nodeCollectionFactory
++ * @param ScopeResolver $scopeResolver
+ */
+ public function __construct(
+ Hierarchy $cmsHierarchy,
+ HierarchyNode $hierarchyNode,
+- Node $hierarchyNodeResource
++ Node $hierarchyNodeResource,
++ CollectionFactory $nodeCollectionFactory,
++ ScopeResolver $scopeResolver
+ ) {
+ $this->cmsHierarchy = $cmsHierarchy;
+ $this->hierarchyNode = $hierarchyNode;
+ $this->hierarchyNodeResource = $hierarchyNodeResource;
++ $this->nodeCollectionFactory = $nodeCollectionFactory;
++ $this->scopeResolver = $scopeResolver;
+ }
+
+ /**
+@@ -64,12 +88,7 @@ class CmsPageSaveAfterObserver implements ObserverInterface
+ $this->hierarchyNode->updateRewriteUrls($page);
+ }
+
+- /**
+- * Append page to selected nodes it will remove pages from other nodes
+- * which are not specified in array. So should be called even array is empty!
+- * Returns array of new ids for page nodes array( oldId => newId ).
+- */
+- $this->hierarchyNode->appendPageToNodes($page, $page->getAppendToNodes());
++ $this->appendPageToNodes($page);
+
+ /**
+ * Update sort order for nodes in parent nodes which have current page as child
+@@ -80,4 +99,165 @@ class CmsPageSaveAfterObserver implements ObserverInterface
+
+ return $this;
+ }
++
++ /**
++ * Append page to selected nodes. Removing page nodes with wrong scope after changing store in "Page in Websites"
++ *
++ * @param Page $page
++ * @return $this
++ * @throws LocalizedException
++ */
++ private function appendPageToNodes(Page $page)
++ {
++ $nodes = $page->getAppendToNodes();
++ $parentNodes = $this->getParentNodes($nodes, $page);
++ $pageData = ['page_id' => $page->getId(), 'identifier' => null, 'label' => null];
++ $removeFromNodes = [];
++ $scopeIds = [];
++ foreach ($parentNodes as $parentNode) {
++ /* @var $parentNode HierarchyNode */
++ if (!isset($nodes[$parentNode->getId()])) {
++ //Delete node after uncheck checkbox
++ $removeFromNodes[] = $parentNode->getId();
++ $scopeIds[] = $parentNode->getScopeId();
++ continue;
++ }
++ $nodeScopeId = (int)$parentNode->getScopeId();
++
++ if (!$this->isBelongsToNodeScope($parentNode->getScope(), $nodeScopeId, (array)$page->getStoreId())) {
++ //If parent node scope_id assigned to store which not in "Page In Websites" - delete node
++ $scopeIds[] = $nodeScopeId;
++ $removeFromNodes[] = $parentNode->getId();
++ continue;
++ }
++
++ $requestUrl = $parentNode->getIdentifier() . '/' . $page->getIdentifier();
++ if ($this->isNodeExist($requestUrl, $nodeScopeId, (int)$parentNode->getId(), (int)$page->getId())) {
++ throw new LocalizedException(
++ __(
++ 'This page cannot be assigned to node, because a node or page with'
++ . ' the same URL Key already exists in this tree part.'
++ )
++ );
++ }
++ if (!$this->isNodeExist($requestUrl, $nodeScopeId, (int)$parentNode->getId())) {
++ $sortOrder = $nodes[$parentNode->getId()];
++ $this->createNewNode($parentNode, $pageData, $sortOrder, $page->getIdentifier());
++ }
++ }
++ if (!empty($removeFromNodes) && $nodes !== null && !empty($scopeIds)) {
++ $this->hierarchyNodeResource->removePageFromNodes($page->getId(), $removeFromNodes, $scopeIds);
++ }
++
++ return $this;
++ }
++
++ /**
++ * Check if node scope is "All store view" or it is same as page scope
++ *
++ * @param string $nodeScope
++ * @param int $nodeScopeId
++ * @param array $pageStoreIds
++ * @return bool
++ */
++ private function isBelongsToNodeScope(string $nodeScope, int $nodeScopeId, array $pageStoreIds): bool
++ {
++ if (empty($pageStoreIds)) {
++ return false;
++ }
++
++ foreach ($pageStoreIds as $storeId) {
++ $isScopeValid = $this->scopeResolver->isBelongsToScope(
++ $nodeScope,
++ $nodeScopeId,
++ ScopeInterface::SCOPE_STORE,
++ $storeId
++ );
++ if ($isScopeValid) {
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ /**
++ * Create new page node
++ *
++ * @param HierarchyNode $parentNode
++ * @param array $pageData
++ * @param int $sortOrder
++ * @param string $pageIdentifier
++ * @return mixed
++ */
++ private function createNewNode(HierarchyNode $parentNode, array $pageData, int $sortOrder, string $pageIdentifier)
++ {
++ $newNode = clone $parentNode;
++ if ($parentNode->getScopeId() !== HierarchyNode::NODE_SCOPE_DEFAULT_ID) {
++ $newNode->setScope(HierarchyNode::NODE_SCOPE_STORE);
++ }
++ $newNode->setScopeId($parentNode->getScopeId());
++
++ $newNode->addData(
++ $pageData
++ )->setParentNodeId(
++ $newNode->getId()
++ )->unsetData(
++ $this->hierarchyNode->getIdFieldName()
++ )->setLevel(
++ $newNode->getLevel() + 1
++ )->setSortOrder(
++ $sortOrder
++ )->setRequestUrl(
++ $newNode->getRequestUrl() . '/' . $pageIdentifier
++ )->setXpath(
++ $newNode->getXpath() . '/'
++ );
++ $newNode->save();
++
++ return $newNode;
++ }
++
++ /**
++ * Return parent nodes collection
++ *
++ * @param array $nodes
++ * @param Page $page
++ * @return Collection
++ */
++ private function getParentNodes(array $nodes, Page $page)
++ {
++ $nodesToFilter = ($nodes === null) ? [] : array_keys($nodes);
++ $nodeCollection = $this->nodeCollectionFactory->create();
++ $parentNodes = $nodeCollection->joinPageExistsNodeInfo(
++ $page
++ )->applyPageExistsOrNodeIdFilter(
++ $nodesToFilter,
++ $page
++ );
++
++ return $parentNodes;
++ }
++
++ /**
++ * Check if current page node is exist
++ *
++ * @param string $requestUrl
++ * @param int $scopeId
++ * @param int $parentNodeId
++ * @param int|null $currentPageId
++ * @return bool
++ */
++ private function isNodeExist(string $requestUrl, int $scopeId, int $parentNodeId, ?int $currentPageId = null): bool
++ {
++ $nodeCollection = $this->nodeCollectionFactory->create();
++ $nodeCollection->addFieldToFilter('request_url', $requestUrl)
++ ->addFieldToFilter('scope_id', $scopeId)
++ ->addFieldToFilter('parent_node_id', $parentNodeId);
++
++ if ($currentPageId !== null) {
++ $nodeCollection->addFieldToFilter('page_id', ['neq' => $currentPageId]);
++ }
++ return $nodeCollection->getSize() ? true : false;
++ }
+ }
+diff --git a/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveBeforeObserver.php b/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveBeforeObserver.php
+index 6311cb26db7..e72a43d291c 100644
+--- a/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveBeforeObserver.php
++++ b/vendor/magento/module-versions-cms/Observer/Backend/CmsPageSaveBeforeObserver.php
+@@ -36,14 +36,11 @@ class CmsPageSaveBeforeObserver implements ObserverInterface
+ {
+ /** @var Page $page */
+ $page = $observer->getEvent()->getObject();
+-
+- if (!$page->getId()) {
++ $nodesData = $this->getNodesOrder($page->getNodesData());
++ if (!$page->getId() && empty($nodesData['appendToNodes'])) {
+ // Newly created page should be auto assigned to website root
+ $page->setWebsiteRoot(true);
+ }
+-
+- $nodesData = $this->getNodesOrder($page->getNodesData());
+-
+ $page->setNodesSortOrder($nodesData['sortOrder']);
+ $page->setAppendToNodes($nodesData['appendToNodes']);
+ return $this;
+diff --git a/vendor/magento/module-versions-cms/view/adminhtml/templates/page/tab/hierarchy.phtml b/vendor/magento/module-versions-cms/view/adminhtml/templates/page/tab/hierarchy.phtml
+index 8f2f0bcaac6..27710ef9b68 100644
+--- a/vendor/magento/module-versions-cms/view/adminhtml/templates/page/tab/hierarchy.phtml
++++ b/vendor/magento/module-versions-cms/view/adminhtml/templates/page/tab/hierarchy.phtml
+@@ -75,10 +75,14 @@
+ for (var i = 0, l = this.nodes.length; i < l; i++) {
+ var dd = (this.nodes[i].parent_node_id && this.nodes[i].current_page) ? true : false;
+ var cls = this.nodes[i].current_page ? 'cms-current' : '';
++ var label = this.nodes[i].label.escapeHTML().replace('\'', ''').replace('"', '"')
++ + " ("
++ + this.nodes[i].store_label.escapeHTML().replace('\'', ''').replace('"', '"')
++ + ")";
+ cls += this.nodes[i].page_id ? ' cms_page' : ' cms_node';
+ var node = new Ext.tree.TreeNode({
+ id: this.nodes[i].node_id,
+- text: this.nodes[i].label.escapeHTML().replace('\'', ''').replace('"', '"'),
++ text: label,
+ cls: cls,
+ expanded: this.nodes[i].page_exists,
+ allowDrop: true,
diff --git a/patches/commerce/MDVA-38535_1.1.5.patch b/patches/commerce/MDVA-38535_1.1.5.patch
new file mode 100644
index 00000000..2495695c
--- /dev/null
+++ b/patches/commerce/MDVA-38535_1.1.5.patch
@@ -0,0 +1,855 @@
+diff --git a/vendor/magento/module-inventory-cache/Model/FlushCacheByCategoryIds.php b/vendor/magento/module-inventory-cache/Model/FlushCacheByCategoryIds.php
+new file mode 100644
+index 0000000..8127b9b
+--- /dev/null
++++ b/vendor/magento/module-inventory-cache/Model/FlushCacheByCategoryIds.php
+@@ -0,0 +1,72 @@
++cacheContextFactory = $cacheContextFactory;
++ $this->eventManager = $eventManager;
++ $this->categoryCacheTag = $categoryCacheTag;
++ $this->appCache = $appCache;
++ }
++
++ /**
++ * Clean cache for given category ids.
++ *
++ * @param array $categoryIds
++ * @return void
++ */
++ public function execute(array $categoryIds): void
++ {
++ if ($categoryIds) {
++ $cacheContext = $this->cacheContextFactory->create();
++ $cacheContext->registerEntities($this->categoryCacheTag, $categoryIds);
++ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $cacheContext]);
++ $this->appCache->clean($cacheContext->getIdentities());
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-cache/Model/ResourceModel/GetProductIdsByStockIds.php b/vendor/magento/module-inventory-cache/Model/ResourceModel/GetProductIdsByStockIds.php
+deleted file mode 100644
+index ec148ea..0000000
+--- a/vendor/magento/module-inventory-cache/Model/ResourceModel/GetProductIdsByStockIds.php
++++ /dev/null
+@@ -1,87 +0,0 @@
+-resource = $resource;
+- $this->defaultStockProvider = $defaultStockProvider;
+- $this->stockIndexTableNameResolver = $stockIndexTableNameResolver;
+- $this->productTableName = $productTableName;
+- }
+-
+- /**
+- * Get product ids for given stock form index table.
+- *
+- * @param array $stockIds
+- * @return array
+- */
+- public function execute(array $stockIds): array
+- {
+- $productIds = [[]];
+- foreach ($stockIds as $stockId) {
+- if ($this->defaultStockProvider->getId() === (int)$stockId) {
+- continue;
+- }
+- $stockIndexTableName = $this->stockIndexTableNameResolver->execute($stockId);
+- $connection = $this->resource->getConnection();
+-
+- $sql = $connection->select()
+- ->from(['stock_index' => $stockIndexTableName], [])
+- ->join(
+- ['product' => $this->resource->getTableName($this->productTableName)],
+- 'product.sku = stock_index.' . IndexStructure::SKU,
+- ['product.entity_id']
+- );
+- $productIds[] = $connection->fetchCol($sql);
+- }
+- $productIds = array_merge(...$productIds);
+-
+- return array_unique($productIds);
+- }
+-}
+diff --git a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Source/SourceItemIndexer/CacheFlush.php b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Source/SourceItemIndexer/CacheFlush.php
+deleted file mode 100644
+index 5da92eb..0000000
+--- a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Source/SourceItemIndexer/CacheFlush.php
++++ /dev/null
+@@ -1,55 +0,0 @@
+-flushCacheByIds = $flushCacheByIds;
+- $this->getProductIdsBySourceItemIds = $getProductIdsBySourceItemIds;
+- }
+-
+- /**
+- * Clean cache for specific products after source items reindex.
+- *
+- * @param SourceItemIndexer $subject
+- * @param array $sourceItemIds
+- * @param null $result
+- * @throws \Exception in case catalog product entity type hasn't been initialize.
+- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+- */
+- public function afterExecuteList(SourceItemIndexer $subject, $result, array $sourceItemIds)
+- {
+- $productIds = $this->getProductIdsBySourceItemIds->execute($sourceItemIds);
+- $this->flushCacheByIds->execute($productIds);
+- }
+-}
+diff --git a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/CacheFlush.php b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/CacheFlush.php
+new file mode 100644
+index 0000000..4a45d9e
+--- /dev/null
++++ b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/CacheFlush.php
+@@ -0,0 +1,75 @@
++flushCacheByIds = $flushCacheByIds;
++ $this->getProductIdsBySourceItemIds = $getProductIdsBySourceItemIds;
++ $this->getCategoryIdsByProductIds = $getCategoryIdsByProductIds;
++ $this->flushCategoryByCategoryIds = $flushCategoryByCategoryIds;
++ }
++
++ /**
++ * Clean cache for specific products after source items reindex.
++ *
++ * @param Sync $subject
++ * @param void $result
++ * @param array $sourceItemIds
++ * @throws \Exception in case catalog product entity type hasn't been initialize.
++ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
++ */
++ public function afterExecuteList(Sync $subject, $result, array $sourceItemIds)
++ {
++ $productIds = $this->getProductIdsBySourceItemIds->execute($sourceItemIds);
++ $categoryIds = $this->getCategoryIdsByProductIds->execute($productIds);
++ $this->flushCategoryByCategoryIds->execute($categoryIds);
++ $this->flushCacheByIds->execute($productIds);
++ }
++}
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/StockIndexer/CacheFlush.php b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/StockIndexer/CacheFlush.php
+deleted file mode 100644
+index 8f050f1..0000000
+--- a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/StockIndexer/CacheFlush.php
++++ /dev/null
+@@ -1,61 +0,0 @@
+-flushCacheByProductIds = $flushCacheByProductIds;
+- $this->getProductIdsByStockIds = $getProductIdsForCacheFlush;
+- }
+-
+- /**
+- * Clean cache after non default stock reindex.
+- *
+- * @param StockIndexer $subject
+- * @param callable $proceed
+- * @param array $stockIds
+- * @return void
+- * @throws \Exception in case product entity type hasn't been initialize.
+- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+- */
+- public function aroundExecuteList(StockIndexer $subject, callable $proceed, array $stockIds)
+- {
+- $beforeReindexProductIds = $this->getProductIdsByStockIds->execute($stockIds);
+- $proceed($stockIds);
+- $afterReindexProductIds = $this->getProductIdsByStockIds->execute($stockIds);
+- $productIdsForCacheClean = array_diff($beforeReindexProductIds, $afterReindexProductIds);
+- if ($productIdsForCacheClean) {
+- $this->flushCacheByProductIds->execute($productIdsForCacheClean);
+- }
+- }
+-}
+diff --git a/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/CacheFlush.php b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/CacheFlush.php
+new file mode 100644
+index 0000000..e3ae2c8
+--- /dev/null
++++ b/vendor/magento/module-inventory-cache/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/CacheFlush.php
+@@ -0,0 +1,61 @@
++flushCacheByProductIds = $flushCacheByProductIds;
++ $this->getProductIdsByStockIds = $getProductIdsForCacheFlush;
++ }
++
++ /**
++ * Clean cache after non default stock reindex.
++ *
++ * @param Sync $subject
++ * @param callable $proceed
++ * @param array $stockIds
++ * @return void
++ * @throws \Exception in case product entity type hasn't been initialize.
++ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
++ */
++ public function aroundExecuteList(Sync $subject, callable $proceed, array $stockIds)
++ {
++ $beforeReindexProductIds = $this->getProductIdsByStockIds->execute($stockIds);
++ $proceed($stockIds);
++ $afterReindexProductIds = $this->getProductIdsByStockIds->execute($stockIds);
++ $productIdsForCacheClean = array_diff($beforeReindexProductIds, $afterReindexProductIds);
++ if ($productIdsForCacheClean) {
++ $this->flushCacheByProductIds->execute($productIdsForCacheClean);
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-cache/etc/di.xml b/vendor/magento/module-inventory-cache/etc/di.xml
+index 0b6c102..ee6ed20 100644
+--- a/vendor/magento/module-inventory-cache/etc/di.xml
++++ b/vendor/magento/module-inventory-cache/etc/di.xml
+@@ -6,20 +6,20 @@
+ */
+ -->
+
+-
+-
++
++
+
+-
+-
++
++
+
+-
++
+
+- catalog_product_entity
++ Magento\Catalog\Model\Product::CACHE_TAG
+
+
+-
++
+
+- Magento\Catalog\Model\Product::CACHE_TAG
++ Magento\Catalog\Model\Product::CACHE_PRODUCT_CATEGORY_TAG
+
+
+-
++
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/PriceIndexUpdater.php b/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/PriceIndexUpdater.php
+deleted file mode 100644
+index e5c2988..0000000
+--- a/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/PriceIndexUpdater.php
++++ /dev/null
+@@ -1,57 +0,0 @@
+-priceIndexProcessor = $priceIndexProcessor;
+- $this->productIdsBySourceItemIds = $productIdsBySourceItemIds;
+- }
+-
+- /**
+- * @param SourceItemIndexer $subject
+- * @param $result
+- * @param array $sourceItemIds
+- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+- */
+- public function afterExecuteList(
+- SourceItemIndexer $subject,
+- $result,
+- array $sourceItemIds
+- ): void {
+- $productIds = $this->productIdsBySourceItemIds->execute($sourceItemIds);
+- if (!empty($productIds)) {
+- $this->priceIndexProcessor->reindexList($productIds);
+- }
+- }
+-}
+diff --git a/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/PriceIndexUpdater.php b/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/PriceIndexUpdater.php
+new file mode 100644
+index 0000000..045b90a
+--- /dev/null
++++ b/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/SourceItem/Strategy/Sync/PriceIndexUpdater.php
+@@ -0,0 +1,59 @@
++priceIndexProcessor = $priceIndexProcessor;
++ $this->productIdsBySourceItemIds = $productIdsBySourceItemIds;
++ }
++
++ /**
++ * Reindex product prices.
++ *
++ * @param Sync $subject
++ * @param void $result
++ * @param array $sourceItemIds
++ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
++ */
++ public function afterExecuteList(
++ Sync $subject,
++ $result,
++ array $sourceItemIds
++ ): void {
++ $productIds = $this->productIdsBySourceItemIds->execute($sourceItemIds);
++ if (!empty($productIds)) {
++ $this->priceIndexProcessor->reindexList($productIds);
++ }
++ }
++}
+diff --git a/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/PriceIndexUpdatePlugin.php b/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/PriceIndexUpdatePlugin.php
+new file mode 100644
+index 0000000..4d94111
+--- /dev/null
++++ b/vendor/magento/module-inventory-catalog/Plugin/InventoryIndexer/Indexer/Stock/Strategy/Sync/PriceIndexUpdatePlugin.php
+@@ -0,0 +1,57 @@
++getProductIdsByStockIds = $getProductIdsForCacheFlush;
++ $this->priceIndexProcessor = $priceIndexProcessor;
++ }
++
++ /**
++ * Update prices after non default stock reindex.
++ *
++ * @param Sync $subject
++ * @param void $result
++ * @param array $stockIds
++ * @return void
++ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
++ */
++ public function afterExecuteList(Sync $subject, $result, array $stockIds)
++ {
++ $productIds = $this->getProductIdsByStockIds->execute($stockIds);
++ if (!empty($productIds)) {
++ $this->priceIndexProcessor->reindexList($productIds);
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-catalog/etc/di.xml b/vendor/magento/module-inventory-catalog/etc/di.xml
+index 3859f8f..ec83745 100644
+--- a/vendor/magento/module-inventory-catalog/etc/di.xml
++++ b/vendor/magento/module-inventory-catalog/etc/di.xml
+@@ -20,8 +20,11 @@
+
+
+-
+-
++
++
++
++
++
+
+
+
+diff --git a/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetCategoryIdsByProductIds.php b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetCategoryIdsByProductIds.php
+new file mode 100644
+index 0000000..1ea5bf6
+--- /dev/null
++++ b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetCategoryIdsByProductIds.php
+@@ -0,0 +1,47 @@
++resourceConnection = $resourceConnection;
++ }
++
++ /**
++ * Get category ids for products
++ *
++ * @param array $productIds
++ * @return array
++ */
++ public function execute(array $productIds): array
++ {
++ $connection = $this->resourceConnection->getConnection();
++ $categoryProductTable = $this->resourceConnection->getTableName('catalog_category_product');
++ $select = $connection->select()
++ ->from(['catalog_category_product' => $categoryProductTable], ['category_id'])
++ ->where('product_id IN (?)', $productIds);
++
++ return $connection->fetchCol($select);
++ }
++}
+\ No newline at end of file
+diff --git a/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetProductIdsByStockIds.php b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetProductIdsByStockIds.php
+new file mode 100644
+index 0000000..e8e3a32
+--- /dev/null
++++ b/vendor/magento/module-inventory-indexer/Model/ResourceModel/GetProductIdsByStockIds.php
+@@ -0,0 +1,86 @@
++resource = $resource;
++ $this->defaultStockProvider = $defaultStockProvider;
++ $this->stockIndexTableNameResolver = $stockIndexTableNameResolver;
++ $this->productTableName = $productTableName;
++ }
++
++ /**
++ * Get product ids for given stock form index table.
++ *
++ * @param array $stockIds
++ * @return array
++ */
++ public function execute(array $stockIds): array
++ {
++ $productIds = [[]];
++ foreach ($stockIds as $stockId) {
++ if ($this->defaultStockProvider->getId() === (int)$stockId) {
++ continue;
++ }
++ $stockIndexTableName = $this->stockIndexTableNameResolver->execute($stockId);
++ $connection = $this->resource->getConnection();
++ $sql = $connection->select()
++ ->from(['stock_index' => $stockIndexTableName], [])
++ ->join(
++ ['product' => $this->resource->getTableName($this->productTableName)],
++ 'product.sku = stock_index.' . IndexStructure::SKU,
++ ['product.entity_id']
++ );
++ $productIds[] = $connection->fetchCol($sql);
++ }
++ $productIds = array_merge(...$productIds);
++
++ return array_unique($productIds);
++ }
++}
+diff --git a/vendor/magento/module-inventory-indexer/etc/di.xml b/vendor/magento/module-inventory-indexer/etc/di.xml
+index dd79461..e943b67 100644
+--- a/vendor/magento/module-inventory-indexer/etc/di.xml
++++ b/vendor/magento/module-inventory-indexer/etc/di.xml
+@@ -61,4 +61,9 @@
+ stock_
+
+
++
++
++ catalog_product_entity
++
++
+
diff --git a/patches/os/MDVA-34680_2.4.1.patch b/patches/os/MDVA-34680_2.4.1.patch
new file mode 100644
index 00000000..682cf032
--- /dev/null
+++ b/patches/os/MDVA-34680_2.4.1.patch
@@ -0,0 +1,67 @@
+diff --git a/vendor/magento/module-customer/Model/ResourceModel/Grid/Collection.php b/vendor/magento/module-customer/Model/ResourceModel/Grid/Collection.php
+index 0fab27161ce..e14594daf80 100644
+--- a/vendor/magento/module-customer/Model/ResourceModel/Grid/Collection.php
++++ b/vendor/magento/module-customer/Model/ResourceModel/Grid/Collection.php
+@@ -7,10 +7,12 @@ namespace Magento\Customer\Model\ResourceModel\Grid;
+
+ use Magento\Customer\Model\ResourceModel\Customer;
+ use Magento\Customer\Ui\Component\DataProvider\Document;
++use Magento\Framework\App\ObjectManager;
+ use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy;
+ use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory;
+ use Magento\Framework\Event\ManagerInterface as EventManager;
+ use Magento\Framework\Locale\ResolverInterface;
++use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+ use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult;
+ use Psr\Log\LoggerInterface as Logger;
+
+@@ -25,6 +27,11 @@ class Collection extends SearchResult
+ private $localeResolver;
+
+ /**
++ * @var TimezoneInterface
++ */
++ private $timeZone;
++
++ /**
+ * @inheritdoc
+ */
+ protected $document = Document::class;
+@@ -42,6 +49,7 @@ class Collection extends SearchResult
+ * @param ResolverInterface $localeResolver
+ * @param string $mainTable
+ * @param string $resourceModel
++ * @param TimezoneInterface|null $timeZone
+ */
+ public function __construct(
+ EntityFactory $entityFactory,
+@@ -50,10 +58,13 @@ class Collection extends SearchResult
+ EventManager $eventManager,
+ ResolverInterface $localeResolver,
+ $mainTable = 'customer_grid_flat',
+- $resourceModel = Customer::class
++ $resourceModel = Customer::class,
++ TimezoneInterface $timeZone = null
+ ) {
+ $this->localeResolver = $localeResolver;
+ parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel);
++ $this->timeZone = $timeZone ?: ObjectManager::getInstance()
++ ->get(TimezoneInterface::class);
+ }
+
+ /**
+@@ -81,6 +92,14 @@ class Collection extends SearchResult
+ return $this;
+ }
+
++ if ($field === 'created_at') {
++ if (is_array($condition)) {
++ foreach ($condition as $key => $value) {
++ $condition[$key] = $this->timeZone->convertConfigTimeToUtc($value);
++ }
++ }
++ }
++
+ if (is_string($field) && count(explode('.', $field)) === 1) {
+ $field = 'main_table.' . $field;
+ }
diff --git a/patches/os/MDVA-37068_2.3.5-p2.patch b/patches/os/MDVA-37068_2.3.5-p2.patch
new file mode 100644
index 00000000..1fe7bf8b
--- /dev/null
+++ b/patches/os/MDVA-37068_2.3.5-p2.patch
@@ -0,0 +1,108 @@
+diff --git a/vendor/magento/module-checkout/view/frontend/web/js/model/checkout-data-resolver.js b/vendor/magento/module-checkout/view/frontend/web/js/model/checkout-data-resolver.js
+index 66539ad..b96a01c 100644
+--- a/vendor/magento/module-checkout/view/frontend/web/js/model/checkout-data-resolver.js
++++ b/vendor/magento/module-checkout/view/frontend/web/js/model/checkout-data-resolver.js
+@@ -43,13 +43,6 @@ define([
+ resolveEstimationAddress: function () {
+ var address;
+
+- if (checkoutData.getShippingAddressFromData()) {
+- address = addressConverter.formAddressDataToQuoteAddress(checkoutData.getShippingAddressFromData());
+- selectShippingAddress(address);
+- } else {
+- this.resolveShippingAddress();
+- }
+-
+ if (quote.isVirtual()) {
+ if (checkoutData.getBillingAddressFromData()) {
+ address = addressConverter.formAddressDataToQuoteAddress(
+@@ -59,6 +52,11 @@ define([
+ } else {
+ this.resolveBillingAddress();
+ }
++ } else if (checkoutData.getShippingAddressFromData()) {
++ address = addressConverter.formAddressDataToQuoteAddress(checkoutData.getShippingAddressFromData());
++ selectShippingAddress(address);
++ } else {
++ this.resolveShippingAddress();
+ }
+ },
+
+diff --git a/vendor/magento/module-tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/vendor/magento/module-tax/Model/Sales/Total/Quote/CommonTaxCollector.php
+index 877aec3..7a0d10a 100644
+--- a/vendor/magento/module-tax/Model/Sales/Total/Quote/CommonTaxCollector.php
++++ b/vendor/magento/module-tax/Model/Sales/Total/Quote/CommonTaxCollector.php
+@@ -6,6 +6,7 @@
+
+ namespace Magento\Tax\Model\Sales\Total\Quote;
+
++use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement;
+ use Magento\Customer\Api\Data\AddressInterfaceFactory as CustomerAddressFactory;
+ use Magento\Customer\Api\Data\AddressInterface as CustomerAddress;
+ use Magento\Customer\Api\Data\RegionInterfaceFactory as CustomerAddressRegionFactory;
+@@ -145,6 +146,11 @@ class CommonTaxCollector extends AbstractTotal
+ private $quoteDetailsItemExtensionFactory;
+
+ /**
++ * @var CustomerAccountManagement
++ */
++ private $customerAccountManagement;
++
++ /**
+ * Class constructor
+ *
+ * @param \Magento\Tax\Model\Config $taxConfig
+@@ -156,6 +162,8 @@ class CommonTaxCollector extends AbstractTotal
+ * @param CustomerAddressRegionFactory $customerAddressRegionFactory
+ * @param TaxHelper|null $taxHelper
+ * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory
++ * @param CustomerAccountManagement|null $customerAccountManagement
++ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ \Magento\Tax\Model\Config $taxConfig,
+@@ -166,7 +174,8 @@ class CommonTaxCollector extends AbstractTotal
+ CustomerAddressFactory $customerAddressFactory,
+ CustomerAddressRegionFactory $customerAddressRegionFactory,
+ TaxHelper $taxHelper = null,
+- QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null
++ QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null,
++ ?CustomerAccountManagement $customerAccountManagement = null
+ ) {
+ $this->taxCalculationService = $taxCalculationService;
+ $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory;
+@@ -178,6 +187,8 @@ class CommonTaxCollector extends AbstractTotal
+ $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class);
+ $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?:
+ ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class);
++ $this->customerAccountManagement = $customerAccountManagement ??
++ ObjectManager::getInstance()->get(CustomerAccountManagement::class);
+ }
+
+ /**
+@@ -413,7 +424,24 @@ class CommonTaxCollector extends AbstractTotal
+ public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAddress $address)
+ {
+ $quoteDetails->setBillingAddress($this->mapAddress($address->getQuote()->getBillingAddress()));
+- $quoteDetails->setShippingAddress($this->mapAddress($address));
++ if ($address->getAddressType() === QuoteAddress::ADDRESS_TYPE_BILLING
++ && !$address->getCountryId()
++ && $address->getQuote()->isVirtual()
++ && $address->getQuote()->getCustomerId()
++ ) {
++ $defaultBillingAddress = $this->customerAccountManagement->getDefaultBillingAddress(
++ $address->getQuote()->getCustomerId()
++ );
++ $addressCopy = $address;
++ if ($defaultBillingAddress) {
++ $addressCopy = clone $address;
++ $addressCopy->importCustomerAddressData($defaultBillingAddress);
++ }
++
++ $quoteDetails->setShippingAddress($this->mapAddress($addressCopy));
++ } else {
++ $quoteDetails->setShippingAddress($this->mapAddress($address));
++ }
+ return $quoteDetails;
+ }
+
diff --git a/patches/os/MDVA-38308_2.4.1-p1.patch b/patches/os/MDVA-38308_2.4.1-p1.patch
new file mode 100644
index 00000000..4cd1ed07
--- /dev/null
+++ b/patches/os/MDVA-38308_2.4.1-p1.patch
@@ -0,0 +1,38 @@
+diff --git a/vendor/magento/framework/File/Uploader.php b/vendor/magento/framework/File/Uploader.php
+index 0913ed01571..971161043fb 100644
+--- a/vendor/magento/framework/File/Uploader.php
++++ b/vendor/magento/framework/File/Uploader.php
+@@ -710,20 +710,22 @@ class Uploader
+ */
+ public static function getNewFileName($destinationFile)
+ {
++ /** @var Filesystem $fileSystem */
++ $fileSystem = ObjectManager::getInstance()->get(Filesystem::class);
++ $local = $fileSystem->getDirectoryRead(DirectoryList::ROOT);
++
++ $fileExists = function ($path) use ($local) {
++ return $local->isExist($path);
++ };
++
+ $fileInfo = pathinfo($destinationFile);
+- if (file_exists($destinationFile)) {
+- $index = 1;
+- $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension'];
+- while (file_exists($fileInfo['dirname'] . '/' . $baseName)) {
+- $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension'];
+- $index++;
+- }
+- $destFileName = $baseName;
+- } else {
+- return $fileInfo['basename'];
++ $index = 1;
++ while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) {
++ $fileInfo['basename'] = $fileInfo['filename'] . '_' . ($index++);
++ $fileInfo['basename'] .= isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '';
+ }
+
+- return $destFileName;
++ return $fileInfo['basename'];
+ }
+
+ /**
diff --git a/patches/os/MDVA-38308_2.4.2-p1.patch b/patches/os/MDVA-38308_2.4.2-p1.patch
new file mode 100644
index 00000000..7fb31285
--- /dev/null
+++ b/patches/os/MDVA-38308_2.4.2-p1.patch
@@ -0,0 +1,14 @@
+diff --git a/vendor/magento/framework/File/Uploader.php b/vendor/magento/framework/File/Uploader.php
+index 5e0bf593fef..067e2611b40 100644
+--- a/vendor/magento/framework/File/Uploader.php
++++ b/vendor/magento/framework/File/Uploader.php
+@@ -803,7 +803,8 @@ class Uploader
+ $fileInfo = pathinfo($destinationFile);
+ $index = 1;
+ while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) {
+- $fileInfo['basename'] = $fileInfo['filename'] . '_' . $index++ . '.' . $fileInfo['extension'];
++ $fileInfo['basename'] = $fileInfo['filename'] . '_' . ($index++);
++ $fileInfo['basename'] .= isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '';
+ }
+
+ return $fileInfo['basename'];
diff --git a/patches/os/MDVA-38468_2.3.2-p2.patch b/patches/os/MDVA-38468_2.3.2-p2.patch
new file mode 100644
index 00000000..b1f040bb
--- /dev/null
+++ b/patches/os/MDVA-38468_2.3.2-p2.patch
@@ -0,0 +1,152 @@
+diff --git a/vendor/magento/module-store/Model/ScopeResolver.php b/vendor/magento/module-store/Model/ScopeResolver.php
+new file mode 100644
+index 00000000000..1b505ffd28f
+--- /dev/null
++++ b/vendor/magento/module-store/Model/ScopeResolver.php
+@@ -0,0 +1,146 @@
++scopeTree = $scopeTree;
++ }
++
++ /**
++ * Check is some scope belongs to other scope
++ *
++ * @param string $baseScope
++ * @param int $baseScopeId
++ * @param string $requestedScope
++ * @param int $requestedScopeId
++ * @return bool
++ */
++ public function isBelongsToScope(
++ string $baseScope,
++ int $baseScopeId,
++ string $requestedScope,
++ int $requestedScopeId
++ ) : bool {
++ /* All scopes belongs to All Store Views */
++ if ($baseScope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT) {
++ return true;
++ }
++
++ $scopeNode = $this->getScopeNode($baseScope, $baseScopeId, [$this->scopeTree->get()]);
++ if (empty($scopeNode)) {
++ return false;
++ }
++
++ return $this->isBelongsToScopeRecurse($requestedScope, $requestedScopeId, [$scopeNode]);
++ }
++
++ /**
++ * Check is Belongs some scope to other scope (internal recurse)
++ *
++ * @param string $requestedScope
++ * @param int $requestedScopeId
++ * @param array $tree
++ * @return bool
++ */
++ private function isBelongsToScopeRecurse(
++ string $requestedScope,
++ int $requestedScopeId,
++ array $tree
++ ) : bool {
++ foreach ($tree as $node) {
++ if ($this->isScopeEquals($node['scope'], $requestedScope) && (int)$node['scope_id'] === $requestedScopeId) {
++ return true;
++ }
++ if (!empty($node['scopes'])) {
++ $isBelongsToChild = $this->isBelongsToScopeRecurse(
++ $requestedScope,
++ $requestedScopeId,
++ $node['scopes']
++ );
++ if ($isBelongsToChild) {
++ return $isBelongsToChild;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ /**
++ * Get tree by scope
++ *
++ * @param string $scope
++ * @param int $scopeId
++ * @param array $tree
++ * @return array
++ */
++ private function getScopeNode(string $scope, int $scopeId, array $tree): array
++ {
++ foreach ($tree as $node) {
++ if ($this->isScopeEquals($node['scope'], $scope) && (int)$node['scope_id'] === $scopeId) {
++ return $node;
++ }
++ if (!empty($node['scopes'])) {
++ $found = $this->getScopeNode($scope, $scopeId, $node['scopes']);
++ if (!empty($found)) {
++ return $found;
++ }
++ }
++ }
++
++ return [];
++ }
++
++ /**
++ * Is scope equals with normalize names
++ *
++ * @param string $firstScope
++ * @param string $secondScope
++ * @return bool
++ */
++ private function isScopeEquals(string $firstScope, string $secondScope): bool
++ {
++ return $this->normalizeScopeName($firstScope) === $this->normalizeScopeName($secondScope);
++ }
++
++ /**
++ * Normalize scope name
++ *
++ * @param string $scope
++ * @return string
++ */
++ private function normalizeScopeName(string $scope): string
++ {
++ switch ($scope) {
++ case ScopeInterface::SCOPE_STORES:
++ return ScopeInterface::SCOPE_STORE;
++ case ScopeInterface::SCOPE_WEBSITES:
++ return ScopeInterface::SCOPE_WEBSITE;
++ case ScopeInterface::SCOPE_GROUPS:
++ return ScopeInterface::SCOPE_GROUP;
++ default:
++ return $scope;
++ }
++ }
++}
diff --git a/patches/os/MDVA-38531_2.3.4-p2.patch b/patches/os/MDVA-38531_2.3.4-p2.patch
new file mode 100644
index 00000000..e49c6ec1
--- /dev/null
+++ b/patches/os/MDVA-38531_2.3.4-p2.patch
@@ -0,0 +1,416 @@
+diff --git a/vendor/magento/module-persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php b/vendor/magento/module-persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php
+new file mode 100644
+index 00000000000..e78cae054e6
+--- /dev/null
++++ b/vendor/magento/module-persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php
+@@ -0,0 +1,99 @@
++persistenceDataHelper = $persistenceDataHelper;
++ $this->persistenceSessionHelper = $persistenceSessionHelper;
++ $this->customerSession = $customerSession;
++ $this->quoteManager = $quoteManager;
++ }
++
++ /**
++ * Convert shopping cart from persistent cart to guest cart after shipping information saved
++ *
++ * Check if shopping cart is persistent and customer is not logged in, and only one payment method is available,
++ * then converts the shopping cart guest cart.
++ *
++ * @param GuestShippingInformationManagement $subject
++ * @param PaymentDetailsInterface $result
++ * @return PaymentDetailsInterface
++ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
++ */
++ public function afterSaveAddressInformation(
++ GuestShippingInformationManagement $subject,
++ PaymentDetailsInterface $result
++ ): PaymentDetailsInterface {
++ if ($this->persistenceSessionHelper->isPersistent()
++ && !$this->customerSession->isLoggedIn()
++ && $this->persistenceDataHelper->isShoppingCartPersist()
++ && $this->quoteManager->isPersistent()
++ && count($result->getPaymentMethods()) === 1
++ ) {
++ $this->customerSession->setCustomerId(null);
++ $this->customerSession->setCustomerGroupId(null);
++ $this->quoteManager->convertCustomerCartToGuest();
++ }
++ return $result;
++ }
++}
+diff --git a/vendor/magento/module-persistent/Model/QuoteManager.php b/vendor/magento/module-persistent/Model/QuoteManager.php
+index ebddfe5a436..07ce85a5097 100644
+--- a/vendor/magento/module-persistent/Model/QuoteManager.php
++++ b/vendor/magento/module-persistent/Model/QuoteManager.php
+@@ -5,8 +5,18 @@
+ */
+ namespace Magento\Persistent\Model;
+
++use Magento\Customer\Api\Data\CustomerInterfaceFactory;
++use Magento\Customer\Api\Data\GroupInterface;
++use Magento\Framework\App\ObjectManager;
++use Magento\Persistent\Helper\Data;
++use Magento\Quote\Api\CartRepositoryInterface;
++use Magento\Quote\Api\Data\CartExtensionFactory;
++use Magento\Quote\Api\Data\CartInterface;
++use Magento\Quote\Model\Quote;
++use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor;
++
+ /**
+- * Class QuoteManager
++ * Quote manager model
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
+ */
+@@ -29,7 +39,7 @@ class QuoteManager
+ /**
+ * Persistent data
+ *
+- * @var \Magento\Persistent\Helper\Data
++ * @var Data
+ */
+ protected $persistentData;
+
+@@ -41,26 +51,52 @@ class QuoteManager
+ protected $_setQuotePersistent = true;
+
+ /**
+- * @var \Magento\Quote\Api\CartRepositoryInterface
++ * @var CartRepositoryInterface
+ */
+ protected $quoteRepository;
+
+ /**
++ * @var ShippingAssignmentProcessor
++ */
++ private $shippingAssignmentProcessor;
++ /**
++ * @var CartExtensionFactory
++ */
++ private $cartExtensionFactory;
++
++ /**
++ * @var CustomerInterfaceFactory
++ */
++ private $customerDataFactory;
++
++ /**
+ * @param \Magento\Persistent\Helper\Session $persistentSession
+- * @param \Magento\Persistent\Helper\Data $persistentData
++ * @param Data $persistentData
+ * @param \Magento\Checkout\Model\Session $checkoutSession
+- * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
++ * @param CartRepositoryInterface $quoteRepository
++ * @param CartExtensionFactory|null $cartExtensionFactory
++ * @param ShippingAssignmentProcessor|null $shippingAssignmentProcessor
++ * @param CustomerInterfaceFactory|null $customerDataFactory
+ */
+ public function __construct(
+ \Magento\Persistent\Helper\Session $persistentSession,
+- \Magento\Persistent\Helper\Data $persistentData,
++ Data $persistentData,
+ \Magento\Checkout\Model\Session $checkoutSession,
+- \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
++ CartRepositoryInterface $quoteRepository,
++ ?CartExtensionFactory $cartExtensionFactory = null,
++ ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null,
++ ?CustomerInterfaceFactory $customerDataFactory = null
+ ) {
+ $this->persistentSession = $persistentSession;
+ $this->persistentData = $persistentData;
+ $this->checkoutSession = $checkoutSession;
+ $this->quoteRepository = $quoteRepository;
++ $this->cartExtensionFactory = $cartExtensionFactory
++ ?? ObjectManager::getInstance()->get(CartExtensionFactory::class);
++ $this->shippingAssignmentProcessor = $shippingAssignmentProcessor
++ ?? ObjectManager::getInstance()->get(ShippingAssignmentProcessor::class);
++ $this->customerDataFactory = $customerDataFactory
++ ?? ObjectManager::getInstance()->get(CustomerInterfaceFactory::class);
+ }
+
+ /**
+@@ -71,7 +107,7 @@ class QuoteManager
+ */
+ public function setGuest($checkQuote = false)
+ {
+- /** @var $quote \Magento\Quote\Model\Quote */
++ /** @var $quote Quote */
+ $quote = $this->checkoutSession->getQuote();
+ if ($quote && $quote->getId()) {
+ if ($checkQuote && !$this->persistentData->isShoppingCartPersist() && !$quote->getIsPersistent()) {
+@@ -82,22 +118,41 @@ class QuoteManager
+ $quote->getPaymentsCollection()->walk('delete');
+ $quote->getAddressesCollection()->walk('delete');
+ $this->_setQuotePersistent = false;
++ $this->cleanCustomerData($quote);
+ $quote->setIsActive(true)
+- ->setCustomerId(null)
+- ->setCustomerEmail(null)
+- ->setCustomerFirstname(null)
+- ->setCustomerLastname(null)
+- ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID)
+ ->setIsPersistent(false)
+ ->removeAllAddresses();
+ //Create guest addresses
+ $quote->getShippingAddress();
+ $quote->getBillingAddress();
++ $this->setShippingAssignments($quote);
+ $quote->collectTotals();
+ $this->quoteRepository->save($quote);
+ }
+
+ $this->persistentSession->getSession()->removePersistentCookie();
++ $this->persistentSession->setSession(null);
++ }
++
++ /**
++ * Clear customer data in quote
++ *
++ * @param Quote $quote
++ */
++ private function cleanCustomerData($quote)
++ {
++ /**
++ * Set empty customer object in quote to avoid restore customer id
++ * @see Quote::beforeSave()
++ */
++ if ($quote->getCustomerId()) {
++ $quote->setCustomer($this->customerDataFactory->create());
++ }
++ $quote->setCustomerId(null)
++ ->setCustomerEmail(null)
++ ->setCustomerFirstname(null)
++ ->setCustomerLastname(null)
++ ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID);
+ }
+
+ /**
+@@ -111,7 +166,7 @@ class QuoteManager
+ public function convertCustomerCartToGuest()
+ {
+ $quoteId = $this->checkoutSession->getQuoteId();
+- /** @var $quote \Magento\Quote\Model\Quote */
++ /** @var $quote Quote */
+ $quote = $this->quoteRepository->get($quoteId);
+ if ($quote && $quote->getId()) {
+ $this->_setQuotePersistent = false;
+@@ -126,6 +181,7 @@ class QuoteManager
+ $quote->getAddressesCollection()->walk('setEmail', ['email' => null]);
+ $quote->collectTotals();
+ $this->persistentSession->getSession()->removePersistentCookie();
++ $this->persistentSession->setSession(null);
+ $this->quoteRepository->save($quote);
+ }
+ }
+@@ -144,7 +200,7 @@ class QuoteManager
+ $quote->setIsActive(true)
+ ->setIsPersistent(false)
+ ->setCustomerId(null)
+- ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID);
++ ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID);
+ }
+ }
+
+@@ -157,4 +213,23 @@ class QuoteManager
+ {
+ return $this->_setQuotePersistent;
+ }
++
++ /**
++ * Create shipping assignment for shopping cart
++ *
++ * @param CartInterface $quote
++ */
++ private function setShippingAssignments(CartInterface $quote): void
++ {
++ $shippingAssignments = [];
++ if (!$quote->isVirtual() && $quote->getItemsQty() > 0) {
++ $shippingAssignments[] = $this->shippingAssignmentProcessor->create($quote);
++ }
++ $cartExtension = $quote->getExtensionAttributes();
++ if ($cartExtension === null) {
++ $cartExtension = $this->cartExtensionFactory->create();
++ }
++ $cartExtension->setShippingAssignments($shippingAssignments);
++ $quote->setExtensionAttributes($cartExtension);
++ }
+ }
+diff --git a/vendor/magento/module-persistent/Observer/MakePersistentQuoteGuestObserver.php b/vendor/magento/module-persistent/Observer/MakePersistentQuoteGuestObserver.php
+index f2f9b96fa82..09b1147e27c 100644
+--- a/vendor/magento/module-persistent/Observer/MakePersistentQuoteGuestObserver.php
++++ b/vendor/magento/module-persistent/Observer/MakePersistentQuoteGuestObserver.php
+@@ -1,6 +1,5 @@
+ _persistentSession = $persistentSession;
+ $this->_persistentData = $persistentData;
+ $this->_customerSession = $customerSession;
+- $this->quoteManager = $quoteManager;
++ $this->checkoutSession = $checkoutSession;
+ }
+
+ /**
+@@ -74,7 +73,7 @@ class MakePersistentQuoteGuestObserver implements ObserverInterface
+ if (($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn())
+ || $this->_persistentData->isShoppingCartPersist()
+ ) {
+- $this->quoteManager->setGuest(true);
++ $this->checkoutSession->clearQuote()->clearStorage();
+ }
+ }
+ }
+diff --git a/vendor/magento/module-persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php b/vendor/magento/module-persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php
+index fe754711c91..efc9ecd4c1a 100644
+--- a/vendor/magento/module-persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php
++++ b/vendor/magento/module-persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php
+@@ -10,6 +10,8 @@ use Magento\Framework\Exception\NoSuchEntityException;
+
+ /**
+ * Observer to remove persistent session if guest empties persistent cart previously created and added to by customer.
++ *
++ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
+ */
+ class RemoveGuestPersistenceOnEmptyCartObserver implements ObserverInterface
+ {
+@@ -96,6 +98,8 @@ class RemoveGuestPersistenceOnEmptyCartObserver implements ObserverInterface
+ }
+
+ if (!$cart || $cart->getItemsCount() == 0) {
++ $this->customerSession->setCustomerId(null)
++ ->setCustomerGroupId(null);
+ $this->quoteManager->setGuest();
+ }
+ }
+diff --git a/vendor/magento/module-persistent/etc/frontend/di.xml b/vendor/magento/module-persistent/etc/frontend/di.xml
+index 3c33f8a51c4..24663f5ab4f 100644
+--- a/vendor/magento/module-persistent/etc/frontend/di.xml
++++ b/vendor/magento/module-persistent/etc/frontend/di.xml
+@@ -49,4 +49,9 @@
+
+
+
++
++
++ Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy
++
++
+
+diff --git a/vendor/magento/module-persistent/etc/webapi_rest/di.xml b/vendor/magento/module-persistent/etc/webapi_rest/di.xml
+index e955dd81b19..cb0aec6b460 100644
+--- a/vendor/magento/module-persistent/etc/webapi_rest/di.xml
++++ b/vendor/magento/module-persistent/etc/webapi_rest/di.xml
+@@ -9,4 +9,8 @@
+
+
+
++
++
++
+
+diff --git a/vendor/magento/module-persistent/etc/webapi_soap/di.xml b/vendor/magento/module-persistent/etc/webapi_soap/di.xml
+index e955dd81b19..cb0aec6b460 100644
+--- a/vendor/magento/module-persistent/etc/webapi_soap/di.xml
++++ b/vendor/magento/module-persistent/etc/webapi_soap/di.xml
+@@ -9,4 +9,8 @@
+
+
+
++
++
++
+
diff --git a/patches/os/MDVA-38535_2.3.5-p1.patch b/patches/os/MDVA-38535_2.3.5-p1.patch
new file mode 100644
index 00000000..31dbb445
--- /dev/null
+++ b/patches/os/MDVA-38535_2.3.5-p1.patch
@@ -0,0 +1,47 @@
+diff --git a/vendor/magento/module-catalog-inventory/Model/Indexer/Stock/CacheCleaner.php b/vendor/magento/module-catalog-inventory/Model/Indexer/Stock/CacheCleaner.php
+index b3fa074..94b8dd7 100644
+--- a/vendor/magento/module-catalog-inventory/Model/Indexer/Stock/CacheCleaner.php
++++ b/vendor/magento/module-catalog-inventory/Model/Indexer/Stock/CacheCleaner.php
+@@ -8,6 +8,7 @@
+
+ namespace Magento\CatalogInventory\Model\Indexer\Stock;
+
++use Magento\Catalog\Model\Category;
+ use Magento\CatalogInventory\Api\StockConfigurationInterface;
+ use Magento\Framework\App\ResourceConnection;
+ use Magento\Framework\App\ObjectManager;
+@@ -90,6 +91,11 @@ class CacheCleaner
+ if ($productIds) {
+ $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds));
+ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
++ $categoryIds = $this->getCategoryIdsByProductIds($productIds);
++ if ($categoryIds){
++ $this->cacheContext->registerEntities(Category::CACHE_TAG, array_unique($categoryIds));
++ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
++ }
+ }
+ }
+
+@@ -162,6 +168,22 @@ class CacheCleaner
+ }
+
+ /**
++ * Get category ids for products
++ *
++ * @param array $productIds
++ * @return array
++ */
++ private function getCategoryIdsByProductIds(array $productIds): array
++ {
++ $categoryProductTable = $this->getConnection()->getTableName('catalog_category_product');
++ $select = $this->getConnection()->select()
++ ->from(['catalog_category_product' => $categoryProductTable], ['category_id'])
++ ->where('product_id IN (?)', $productIds);
++
++ return $this->getConnection()->fetchCol($select);
++ }
++
++ /**
+ * Get database connection.
+ *
+ * @return AdapterInterface
diff --git a/patches/os/MDVA-38608_2.3.2-p2.patch b/patches/os/MDVA-38608_2.3.2-p2.patch
new file mode 100644
index 00000000..c33ba8ff
--- /dev/null
+++ b/patches/os/MDVA-38608_2.3.2-p2.patch
@@ -0,0 +1,19 @@
+diff --git a/vendor/magento/module-catalog-rule/Model/Indexer/IndexerTableSwapper.php b/vendor/magento/module-catalog-rule/Model/Indexer/IndexerTableSwapper.php
+index f99f8c50a7f9a..0ddae74ff0a55 100644
+--- a/vendor/magento/module-catalog-rule/Model/Indexer/IndexerTableSwapper.php
++++ b/vendor/magento/module-catalog-rule/Model/Indexer/IndexerTableSwapper.php
+@@ -122,4 +122,14 @@ public function swapIndexTables(array $originalTablesNames)
+ $this->resourceConnection->getConnection()->dropTable($tableName);
+ }
+ }
++
++ /**
++ * Cleanup leftover temporary tables
++ */
++ public function __destruct()
++ {
++ foreach ($this->temporaryTables as $tableName) {
++ $this->resourceConnection->getConnection()->dropTable($tableName);
++ }
++ }
+ }