diff --git a/vendor/magento/module-quote/Model/CartLockedException.php b/vendor/magento/module-quote/Model/CartLockedException.php new file mode 100644 index 0000000..7f2aa87 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartLockedException.php @@ -0,0 +1,18 @@ +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 0000000..6681c63 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartMutexInterface.php @@ -0,0 +1,25 @@ +eventManager = $eventManager; $this->submitQuoteValidator = $submitQuoteValidator; @@ -269,8 +268,8 @@ class QuoteManagement implements CartManagementInterface ->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,28 @@ class QuoteManagement implements CartManagementInterface /** * @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(); @@ -613,14 +630,7 @@ class QuoteManagement implements CartManagementInterface ] ); - $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 +641,7 @@ class QuoteManagement implements CartManagementInterface ] ); $this->quoteRepository->save($quote); - $this->lockManager->unlock($lockedName); } 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 b1bef83..f4694d6 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\CollectionProcessor; 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; @@ -94,6 +95,11 @@ class QuoteRepository implements CartRepositoryInterface private $cartFactory; /** + * @var RequestSafetyInterface + */ + private $requestSafety; + + /** * Constructor * * @param QuoteFactory $quoteFactory @@ -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 @@ class QuoteRepository implements CartRepositoryInterface 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 @@ class QuoteRepository implements CartRepositoryInterface $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 @@ class QuoteRepository implements CartRepositoryInterface */ public function getActive($cartId, array $sharedStoreIds = []) { + $this->validateCachedActiveQuote((int)$cartId); $quote = $this->get($cartId, $sharedStoreIds); if (!$quote->getIsActive()) { throw NoSuchEntityException::singleField('cartId', $cartId); @@ -173,10 +183,32 @@ class QuoteRepository implements CartRepositoryInterface } /** + * 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); @@ -185,6 +217,28 @@ class QuoteRepository implements CartRepositoryInterface } /** + * 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 */ public function save(CartInterface $quote) diff --git a/vendor/magento/module-quote/etc/di.xml b/vendor/magento/module-quote/etc/di.xml index 5ffc82d..cef8eee 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 d96c88b..5d3fe79 100644 --- a/vendor/magento/module-quote/i18n/en_US.csv +++ b/vendor/magento/module-quote/i18n/en_US.csv @@ -69,3 +69,4 @@ 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." diff --git a/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutex.php b/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutex.php deleted file mode 100644 index 2b13086..0000000 --- a/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutex.php +++ /dev/null @@ -1,72 +0,0 @@ -lockManager = $lockManager; - $this->lockWaitTimeout = $lockWaitTimeout; - } - - /** - * @inheritDoc - */ - public function execute(string $maskedId, callable $callable, array $args = []) - { - if (empty($maskedId)) { - throw new \InvalidArgumentException('Quote masked id must be provided'); - } - - if ($this->lockManager->isLocked(self::LOCK_PREFIX . $maskedId)) { - throw new GraphQlAlreadyExistsException( - __('The order has already been placed and is currently processing.') - ); - } - - if ($this->lockManager->lock(self::LOCK_PREFIX . $maskedId, $this->lockWaitTimeout)) { - try { - return $callable(...$args); - } finally { - $this->lockManager->unlock(self::LOCK_PREFIX . $maskedId); - } - } else { - throw new LocalizedException( - __('Could not acquire lock for the quote id: %1', $maskedId) - ); - } - } -} diff --git a/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutexInterface.php b/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutexInterface.php deleted file mode 100644 index 6e4c85d..0000000 --- a/vendor/magento/module-quote-graph-ql/Model/Cart/PlaceOrderMutexInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -getCartForCheckout = $getCartForCheckout; $this->placeOrder = $placeOrder; $this->orderRepository = $orderRepository; $this->errorMessageFormatter = $errorMessageFormatter; - $this->placeOrderMutex = $placeOrderMutex ?: ObjectManager::getInstance()->get(PlaceOrderMutexInterface::class); } /** @@ -80,25 +70,6 @@ class PlaceOrder implements ResolverInterface throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - return $this->placeOrderMutex->execute( - $args['input']['cart_id'], - \Closure::fromCallable([$this, 'run']), - [$field, $context, $info, $args] - ); - } - - /** - * Run the resolver. - * - * @param Field $field - * @param ContextInterface $context - * @param ResolveInfo $info - * @param array|null $args - * @return array[] - * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - */ - private function run(Field $field, ContextInterface $context, ResolveInfo $info, ?array $args): array - { $maskedCartId = $args['input']['cart_id']; $userId = (int)$context->getUserId(); $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/vendor/magento/module-quote-graph-ql/etc/di.xml b/vendor/magento/module-quote-graph-ql/etc/di.xml index 63eb001..4b55681 100644 --- a/vendor/magento/module-quote-graph-ql/etc/di.xml +++ b/vendor/magento/module-quote-graph-ql/etc/di.xml @@ -9,7 +9,6 @@ -