-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Meta Checkout URL Implementation #39667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sol-loup
wants to merge
19
commits into
magento:2.4-develop
Choose a base branch
from
sol-loup:2.4-develop
base: 2.4-develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
a04c158
Adding AddToCartLinkV1 controller and checkout_cart_link route for Me…
sol-loup c345b33
Merge branch '2.4-develop' into 2.4-develop
engcom-Hotel 87aa6ac
Merge branch '2.4-develop' into 2.4-develop
engcom-Hotel f26b9c4
addressing comments
sol-loup 4402ca3
Merge branch '2.4-develop' of https://github.com/sol-loup/magento2 in…
sol-loup f372039
Shifting to a SKU based approach
sol-loup 10e504e
Addressing PR comments
sol-loup 178fa97
Merge branch '2.4-develop' into 2.4-develop
engcom-Hotel 580b049
Fixing lint complaints
sol-loup 63484c2
Merge branch '2.4-develop' of https://github.com/sol-loup/magento2 in…
sol-loup 292df77
Adjusting Adobe Copyright Comment
sol-loup 6b93084
Merge branch '2.4-develop' into 2.4-develop
engcom-Hotel e9e5641
Changing implementation to use quote
sol-loup f60700d
Merge branch '2.4-develop' into 2.4-develop
engcom-Bravo a3eec99
adding configuration values to enable and disable add to cart linking
sol-loup b793d68
Merge branch '2.4-develop' of https://github.com/sol-loup/magento2 in…
sol-loup 704908d
Merge branch '2.4-develop' into 2.4-develop
engcom-Hotel cbe8c02
changes to satisfy feedback
sol-loup 38c6902
Merge branch '2.4-develop' of https://github.com/sol-loup/magento2 in…
sol-loup File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
307 changes: 307 additions & 0 deletions
307
app/code/Magento/Checkout/Controller/Cart/AddToCartLinkV1.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* Copyright 2025 Adobe | ||
* All Rights Reserved. | ||
*/ | ||
|
||
namespace Magento\Checkout\Controller\Cart; | ||
|
||
use Magento\Framework\App\Action\HttpGetActionInterface; | ||
use Magento\Framework\App\RequestInterface; | ||
use Magento\Catalog\Api\ProductRepositoryInterface; | ||
use Magento\Framework\Exception\NoSuchEntityException; | ||
use Magento\Framework\View\Result\PageFactory; | ||
use Magento\Framework\Message\ManagerInterface; | ||
use Magento\Framework\App\Action\Context; | ||
use Magento\Framework\Controller\ResultInterface; | ||
use Magento\Checkout\Model\Session as CheckoutSession; | ||
use Magento\Framework\App\Config\ScopeConfigInterface; | ||
use Magento\Store\Model\ScopeInterface; | ||
use Magento\Framework\Controller\Result\ForwardFactory; | ||
use Magento\Store\Model\StoreManagerInterface; | ||
use Magento\Quote\Api\CartRepositoryInterface; | ||
|
||
/** | ||
* Controller for Meta Checkout URL implementation | ||
* Handles dynamic store scope switching via the 'store' URL parameter. | ||
* | ||
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) | ||
*/ | ||
class AddToCartLinkV1 implements HttpGetActionInterface | ||
{ | ||
/** | ||
* Configuration path for enabling the Add To Cart Link feature. | ||
*/ | ||
const XML_PATH_ENABLE_ADD_TO_CART_LINK = 'checkout/cart/enable_add_to_cart_link_v1'; | ||
|
||
/** | ||
* Maximum number of products that can be processed in a single request to | ||
* prevent abuse (DoS via extremely large cart payloads). | ||
*/ | ||
private const MAX_PRODUCTS_PER_REQUEST = 25; | ||
|
||
/** | ||
* Request instance | ||
* | ||
* @var RequestInterface | ||
*/ | ||
private $_request; | ||
|
||
/** | ||
* Constructor | ||
* | ||
* @param Context $context Context | ||
* @param CheckoutSession $checkoutSession Checkout session | ||
* @param ProductRepositoryInterface $productRepository Product repository | ||
* @param PageFactory $resultPageFactory Result page factory | ||
* @param ManagerInterface $messageManager Message manager | ||
* @param ScopeConfigInterface $scopeConfig Scope configuration | ||
* @param ForwardFactory $resultForwardFactory Result forward factory | ||
* @param StoreManagerInterface $storeManager Store manager | ||
* @param CartRepositoryInterface $cartRepository Cart repository | ||
*/ | ||
public function __construct( | ||
Context $context, | ||
private readonly CheckoutSession $checkoutSession, | ||
private readonly ProductRepositoryInterface $productRepository, | ||
private readonly PageFactory $resultPageFactory, | ||
private readonly ManagerInterface $messageManager, | ||
private readonly ScopeConfigInterface $scopeConfig, | ||
private readonly ForwardFactory $resultForwardFactory, | ||
private readonly StoreManagerInterface $storeManager, | ||
private readonly CartRepositoryInterface $cartRepository | ||
) { | ||
$this->_request = $context->getRequest(); | ||
} | ||
|
||
/** | ||
* Execute action based on request and return result | ||
* Handles store switching and populates cart based on URL parameters. | ||
* | ||
* @return ResultInterface | ||
*/ | ||
public function execute(): ResultInterface | ||
{ | ||
// Handle store switching first | ||
$storeParam = $this->_request->getParam('store'); | ||
$store = null; | ||
try { | ||
if ($storeParam) { | ||
$store = $this->storeManager->getStore($storeParam); | ||
if (!$store->getIsActive()) { | ||
$store = null; | ||
} | ||
} | ||
} catch (\Exception $e) { | ||
$store = null; | ||
} | ||
if (!$store) { | ||
$store = $this->storeManager->getDefaultStoreView(); | ||
} | ||
$this->storeManager->setCurrentStore($store->getId()); | ||
|
||
// Check if the feature is enabled for the current (potentially switched) store scope | ||
if (!$this->scopeConfig->isSetFlag(self::XML_PATH_ENABLE_ADD_TO_CART_LINK, ScopeInterface::SCOPE_STORE)) { | ||
$resultForward = $this->resultForwardFactory->create(); | ||
$resultForward->forward('noroute'); | ||
return $resultForward; | ||
} | ||
|
||
// Get products parameter | ||
$productsParam = $this->_request->getParam('products', ''); | ||
$couponCode = $this->_request->getParam('coupon', ''); | ||
|
||
// Get quote from checkout session (should now reflect the correct store) | ||
try { | ||
$quote = $this->checkoutSession->getQuote(); | ||
} catch (\Exception $e) { | ||
$this->messageManager->addErrorMessage(__('Unable to initialize the shopping cart.')); | ||
return $this->resultPageFactory->create(); | ||
} | ||
|
||
// Ensure quote is associated with the correct store ID after potential switch | ||
try { | ||
$currentStoreId = $this->storeManager->getStore()->getId(); | ||
if ($quote->getStoreId() !== $currentStoreId) { | ||
$quote->setStoreId($currentStoreId); | ||
// May need to reload or recalculate parts of the quote if store change affects it | ||
} | ||
} catch (NoSuchEntityException $e) { | ||
// Store could not be resolved – fall back to default behaviour and continue | ||
} | ||
|
||
// Clear the quote first (required by Meta spec) | ||
$quote->removeAllItems(); | ||
|
||
// Parse products parameter | ||
if (!empty($productsParam)) { | ||
$productItems = $this->_parseProductsParam($productsParam); | ||
|
||
// Enforce maximum allowed items in one request | ||
if (count($productItems) > self::MAX_PRODUCTS_PER_REQUEST) { | ||
$this->messageManager->addErrorMessage( | ||
__('You can only add up to %1 products at once.', self::MAX_PRODUCTS_PER_REQUEST) | ||
); | ||
$productItems = array_slice($productItems, 0, self::MAX_PRODUCTS_PER_REQUEST); | ||
} | ||
|
||
// Add products to quote | ||
foreach ($productItems as $item) { | ||
try { | ||
$productIdentifier = $item['identifier']; | ||
$qty = $item['qty']; | ||
$product = null; | ||
|
||
// Load product within the current store scope | ||
$currentStoreId = $this->storeManager->getStore()->getId(); | ||
|
||
// First try to load by SKU for the current store | ||
try { | ||
// Pass store ID to ensure product is loaded in the correct context if needed | ||
// Note: ProductRepository->get() might not directly accept storeId. | ||
// Custom logic or preference for store-specific SKUs might be required here. | ||
$product = $this->productRepository->get($productIdentifier); | ||
// Verify product is available in the current store | ||
if (!in_array($currentStoreId, $product->getStoreIds())) { | ||
throw new NoSuchEntityException(__('Product is not available in the selected store.')); | ||
} | ||
|
||
} catch (NoSuchEntityException $e) { | ||
// If SKU lookup fails, try by ID (less likely to be store specific identifier) | ||
try { | ||
$product = $this->productRepository->getById((int)$productIdentifier, false, $currentStoreId); | ||
// Verify product is available in the current store | ||
if (!in_array($currentStoreId, $product->getStoreIds())) { | ||
throw new NoSuchEntityException(__('Product is not available in the selected store.')); | ||
} | ||
} catch (NoSuchEntityException $idException) { | ||
// Both SKU and ID lookup failed for the current store | ||
$this->messageManager->addErrorMessage( | ||
__( | ||
'Product with identifier "%1" was not found in the current store.', | ||
$productIdentifier | ||
) | ||
); | ||
continue; | ||
} catch (\InvalidArgumentException $invalidIdException) { | ||
// ID was not an integer | ||
$this->messageManager->addErrorMessage( | ||
__( | ||
'Product identifier "%1" is invalid.', | ||
$productIdentifier | ||
) | ||
); | ||
continue; | ||
} | ||
} | ||
|
||
// Ensure product is salable in the current store context | ||
if (!$product->isSalable()) { | ||
$this->messageManager->addErrorMessage( | ||
__('Product "%1" is currently out of stock or not available for purchase.', $product->getName()) | ||
); | ||
continue; | ||
} | ||
|
||
|
||
// Add product to quote using the product object | ||
// Ensure the request object correctly represents quantity | ||
$requestInfo = new \Magento\Framework\DataObject(['qty' => $qty]); | ||
$quote->addProduct($product, $requestInfo); | ||
|
||
} catch (NoSuchEntityException $e) { | ||
// Catch store specific availability issues | ||
$this->messageManager->addErrorMessage($e->getMessage()); | ||
continue; | ||
} catch (\Exception $e) { | ||
// Other exceptions, log and continue | ||
$this->messageManager->addErrorMessage(__('Could not add product to cart: %1', $e->getMessage())); | ||
// Consider logging $e for debugging | ||
continue; | ||
} | ||
} | ||
|
||
// Save quote and collect totals | ||
$quote->collectTotals(); | ||
try { | ||
$this->cartRepository->save($quote); | ||
} catch (\Exception $e) { | ||
$this->messageManager->addErrorMessage(__('Unable to save cart.')); | ||
} | ||
|
||
// Update checkout session | ||
$this->checkoutSession->setQuoteId($quote->getId()); | ||
} | ||
|
||
// Apply coupon code if provided | ||
if (!empty($couponCode)) { | ||
try { | ||
$quote->setCouponCode($couponCode)->collectTotals(); | ||
$this->cartRepository->save($quote); | ||
|
||
// Check if coupon was actually applied | ||
if ($quote->getCouponCode() !== $couponCode) { | ||
// Coupon might be invalid or not applicable to the current store/cart contents | ||
$this->messageManager->addErrorMessage( | ||
__('The coupon code "%1" is not valid or cannot be applied.', $couponCode) | ||
); | ||
} | ||
} catch (\Exception $e) { | ||
// Log the error for debugging | ||
$this->messageManager->addErrorMessage( | ||
__('Could not apply coupon code "%1".', $couponCode) | ||
); | ||
} | ||
} | ||
|
||
// Render the checkout page directly (not a redirect) | ||
// This ensures the URL parameters remain in the browser address bar | ||
return $this->resultPageFactory->create(); | ||
} | ||
|
||
/** | ||
* Parse the products parameter from the URL | ||
* | ||
* Format: identifier:qty,identifier:qty | ||
* (where identifier can be SKU or product ID) | ||
* | ||
* @param string $productsParam Products parameter string | ||
* | ||
* @return array<int, array<string, mixed>> | ||
*/ | ||
private function _parseProductsParam(string $productsParam): array | ||
{ | ||
$result = []; | ||
$productPairs = explode(',', $productsParam); | ||
|
||
foreach ($productPairs as $pair) { | ||
$pair = trim($pair); // Trim whitespace from each pair | ||
if (empty($pair)) { | ||
continue; // Skip empty entries potentially caused by trailing commas | ||
} | ||
$parts = explode(':', $pair); | ||
if (count($parts) === 2) { | ||
$identifier = trim($parts[0]); | ||
$qty = filter_var($parts[1], FILTER_VALIDATE_INT); // Validate quantity is an integer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent misuse, please add a maximum limit on the number of products that can be processed in one request. |
||
|
||
// Ensure identifier is not empty and quantity is a positive integer | ||
if (!empty($identifier) && $qty !== false && $qty > 0) { | ||
$result[] = [ | ||
'identifier' => $identifier, | ||
'qty' => $qty | ||
]; | ||
} else { | ||
// Log or message invalid format part | ||
$this->messageManager->addWarningMessage(__('Invalid format or quantity for product entry "%1".', $pair)); | ||
} | ||
} else { | ||
// Log or message invalid format pair | ||
$this->messageManager->addWarningMessage(__('Invalid format for product entry "%1". Expected format: identifier:qty.', $pair)); | ||
} | ||
} | ||
|
||
return $result; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$checkoutSession
property is never used, I guess we can remove this.