diff --git a/vendor/magento/module-quote/Model/CartLockedException.php b/vendor/magento/module-quote/Model/CartLockedException.php new file mode 100644 index 0000000000000..ca1d52b535da7 --- /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 0000000000000..0bd4f1e71ad45 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartMutexInterface.php @@ -0,0 +1,36 @@ +eventManager = $eventManager; $this->submitQuoteValidator = $submitQuoteValidator; @@ -269,8 +268,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); } /** @@ -396,10 +395,27 @@ 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) */ - public function placeOrder($cartId, PaymentInterface $paymentMethod = null) + private function placeOrderRun($cartId, PaymentInterface $paymentMethod = null) { $quote = $this->quoteRepository->getActive($cartId); $customer = $quote->getCustomer(); @@ -613,14 +629,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ] ); - $lockedName = self::LOCK_PREFIX . $quote->getId(); - if ($this->lockManager->isLocked($lockedName)) { - throw new LocalizedException(__( - 'A server error stopped your order from being placed. Please try to place your order again.' - )); - } try { - $this->lockManager->lock($lockedName, self::LOCK_TIMEOUT); $order = $this->orderManagement->place($order); $quote->setIsActive(false); $this->eventManager->dispatch( @@ -631,9 +640,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ] ); $this->quoteRepository->save($quote); - $this->lockManager->unlock($lockedName); } catch (\Exception $e) { - $this->lockManager->unlock($lockedName); $this->rollbackAddresses($quote, $order, $e); throw $e; } @@ -649,6 +656,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ protected function _prepareCustomerQuote($quote) { diff --git a/vendor/magento/module-quote/Model/QuoteRepository.php b/vendor/magento/module-quote/Model/QuoteRepository.php index b1bef834197aa..552949a100825 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\Quote\Api\CartRepositoryInterface; @@ -93,6 +94,11 @@ class QuoteRepository implements CartRepositoryInterface */ private $cartFactory; + /** + * @var RequestSafetyInterface + */ + private $requestSafety; + /** * Constructor * @@ -104,6 +110,7 @@ class QuoteRepository implements CartRepositoryInterface * @param CollectionProcessorInterface|null $collectionProcessor * @param QuoteCollectionFactory|null $quoteCollectionFactory * @param CartInterfaceFactory|null $cartFactory + * @param RequestSafetyInterface|null $requestSafety * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -114,7 +121,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; @@ -125,6 +133,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); } /** @@ -165,6 +174,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); @@ -172,11 +182,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); @@ -240,6 +272,28 @@ protected function loadQuote($loadMethod, $loadField, $identifier, array $shared 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 abe0ff802c03b..902c52bbdc73f 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 98e6ca5b6c367..b4204297b4ab2 100644 --- a/vendor/magento/module-quote/i18n/en_US.csv +++ b/vendor/magento/module-quote/i18n/en_US.csv @@ -69,5 +69,6 @@ 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" "Please check input parameters.","Please check input parameters."