diff --git a/vendor/magento/module-quote/Model/CartLockedException.php b/vendor/magento/module-quote/Model/CartLockedException.php new file mode 100644 index 000000000000..ca1d52b535da --- /dev/null +++ b/vendor/magento/module-quote/Model/CartLockedException.php @@ -0,0 +1,29 @@ +lockManager = $lockManager; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(int $id, callable $callable, array $args = []) + { + $lockName = 'cart_lock_' . $id; + + if (!$this->lockManager->lock($lockName, 0)) { + $this->logger->critical( + 'The cart is locked for processing, the request has been aborted. Quote ID: ' . $id + ); + throw new CartLockedException( + __('The cart is locked for processing. Please try again later.') + ); + } + + try { + $result = $callable(...$args); + } finally { + $this->lockManager->unlock($lockName); + } + + return $result; + } +} diff --git a/vendor/magento/module-quote/Model/CartMutexInterface.php b/vendor/magento/module-quote/Model/CartMutexInterface.php new file mode 100644 index 000000000000..0bd4f1e71ad4 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartMutexInterface.php @@ -0,0 +1,36 @@ +eventManager = $eventManager; $this->submitQuoteValidator = $submitQuoteValidator; @@ -270,8 +269,8 @@ public function __construct( ->get(RequestInterface::class); $this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance() ->get(RemoteAddress::class); - $this->lockManager = $lockManager ?: ObjectManager::getInstance() - ->get(LockManagerInterface::class); + $this->cartMutex = $cartMutex + ?? ObjectManager::getInstance()->get(CartMutexInterface::class); } /** @@ -397,10 +396,28 @@ protected function createCustomerCart($customerId, $storeId) /** * @inheritdoc + */ + public function placeOrder($cartId, PaymentInterface $paymentMethod = null) + { + return $this->cartMutex->execute( + (int)$cartId, + \Closure::fromCallable([$this, 'placeOrderRun']), + [$cartId, $paymentMethod] + ); + } + + /** + * Places an order for a specified cart. + * + * @param int $cartId The cart ID. + * @param PaymentInterface|null $paymentMethod + * @throws CouldNotSaveException + * @return int Order ID. * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ - public function placeOrder($cartId, PaymentInterface $paymentMethod = null) + private function placeOrderRun($cartId, PaymentInterface $paymentMethod = null) { $quote = $this->quoteRepository->getActive($cartId); $customer = $quote->getCustomer(); @@ -614,12 +631,6 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ] ); - $lockedName = self::LOCK_PREFIX . $quote->getId(); - if (!$this->lockManager->lock($lockedName, self::LOCK_TIMEOUT)) { - throw new LocalizedException(__( - 'A server error stopped your order from being placed. Please try to place your order again.' - )); - } try { $order = $this->orderManagement->place($order); $quote->setIsActive(false); @@ -632,7 +643,6 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - $this->lockManager->unlock($lockedName); $this->rollbackAddresses($quote, $order, $e); throw $e; } diff --git a/vendor/magento/module-quote/Model/QuoteRepository.php b/vendor/magento/module-quote/Model/QuoteRepository.php index c836578bcc25..db5d1a551a43 100644 --- a/vendor/magento/module-quote/Model/QuoteRepository.php +++ b/vendor/magento/module-quote/Model/QuoteRepository.php @@ -12,6 +12,7 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestSafetyInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManager\ResetAfterRequestInterface; @@ -96,6 +97,11 @@ class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInter */ private $cartFactory; + /** + * @var RequestSafetyInterface + */ + private $requestSafety; + /** * Constructor * @@ -107,6 +113,7 @@ class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInter * @param CollectionProcessorInterface|null $collectionProcessor * @param QuoteCollectionFactory|null $quoteCollectionFactory * @param CartInterfaceFactory|null $cartFactory + * @param RequestSafetyInterface|null $requestSafety * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -117,7 +124,8 @@ public function __construct( JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, QuoteCollectionFactory $quoteCollectionFactory = null, - CartInterfaceFactory $cartFactory = null + CartInterfaceFactory $cartFactory = null, + RequestSafetyInterface $requestSafety = null ) { $this->quoteFactory = $quoteFactory; $this->storeManager = $storeManager; @@ -128,6 +136,7 @@ public function __construct( $this->quoteCollectionFactory = $quoteCollectionFactory ?: ObjectManager::getInstance() ->get(QuoteCollectionFactory::class); $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class); + $this->requestSafety = $requestSafety ?: ObjectManager::getInstance()->get(RequestSafetyInterface::class); } /** @@ -178,6 +187,7 @@ public function getForCustomer($customerId, array $sharedStoreIds = []) */ public function getActive($cartId, array $sharedStoreIds = []) { + $this->validateCachedActiveQuote((int)$cartId); $quote = $this->get($cartId, $sharedStoreIds); if (!$quote->getIsActive()) { throw NoSuchEntityException::singleField('cartId', $cartId); @@ -185,11 +195,33 @@ public function getActive($cartId, array $sharedStoreIds = []) return $quote; } + /** + * Validates if cached quote is still active. + * + * @param int $cartId + * @return void + * @throws NoSuchEntityException + */ + private function validateCachedActiveQuote(int $cartId): void + { + if (isset($this->quotesById[$cartId]) && !$this->requestSafety->isSafeMethod()) { + $quote = $this->cartFactory->create(); + if (is_callable([$quote, 'setSharedStoreIds'])) { + $quote->setSharedStoreIds(['*']); + } + $quote->loadActive($cartId); + if (!$quote->getIsActive()) { + throw NoSuchEntityException::singleField('cartId', $cartId); + } + } + } + /** * @inheritdoc */ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) { + $this->validateCachedCustomerActiveQuote((int)$customerId); $quote = $this->getForCustomer($customerId, $sharedStoreIds); if (!$quote->getIsActive()) { throw NoSuchEntityException::singleField('customerId', $customerId); @@ -197,6 +229,28 @@ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) return $quote; } + /** + * Validates if cached customer quote is still active. + * + * @param int $customerId + * @return void + * @throws NoSuchEntityException + */ + private function validateCachedCustomerActiveQuote(int $customerId): void + { + if (isset($this->quotesByCustomerId[$customerId]) && !$this->requestSafety->isSafeMethod()) { + $quoteId = $this->quotesByCustomerId[$customerId]->getId(); + $quote = $this->cartFactory->create(); + if (is_callable([$quote, 'setSharedStoreIds'])) { + $quote->setSharedStoreIds(['*']); + } + $quote->loadActive($quoteId); + if (!$quote->getIsActive()) { + throw NoSuchEntityException::singleField('customerId', $customerId); + } + } + } + /** * @inheritdoc */ diff --git a/vendor/magento/module-quote/etc/di.xml b/vendor/magento/module-quote/etc/di.xml index 496996d77541..316f4426f203 100644 --- a/vendor/magento/module-quote/etc/di.xml +++ b/vendor/magento/module-quote/etc/di.xml @@ -45,6 +45,7 @@ + diff --git a/vendor/magento/module-quote/i18n/en_US.csv b/vendor/magento/module-quote/i18n/en_US.csv index 6563651ba5ac..65c12c26fc6d 100644 --- a/vendor/magento/module-quote/i18n/en_US.csv +++ b/vendor/magento/module-quote/i18n/en_US.csv @@ -69,6 +69,7 @@ Carts,Carts "Validated Country Code","Validated Country Code" "Validated Vat Number","Validated Vat Number" "Invalid Quote Item id %1","Invalid Quote Item id %1" +"The cart is locked for processing. Please try again later.","The cart is locked for processing. Please try again later." "Invalid quote address id %1","Invalid quote address id %1" "Number above 0 is required for the limit","Number above 0 is required for the limit" "Please select a valid rate limit period in seconds: %1.","Please select a valid rate limit period in seconds: %1."