|
diff --color -ruN catalog-patch-orig/Model/CustomOptions/CustomOptionProcessor.php catalog-patch/Model/CustomOptions/CustomOptionProcessor.php |
|
--- a/Model/CustomOptions/CustomOptionProcessor.php 2026-03-23 14:03:43.362996246 +0000 |
|
+++ b/Model/CustomOptions/CustomOptionProcessor.php 2026-03-23 14:04:24.216349753 +0000 |
|
@@ -5,12 +5,20 @@ |
|
*/ |
|
namespace Magento\Catalog\Model\CustomOptions; |
|
|
|
+use Magento\Catalog\Api\Data\CustomOptionInterface; |
|
+use Magento\Catalog\Api\Data\ProductCustomOptionInterface; |
|
+use Magento\Catalog\Api\ProductRepositoryInterface; |
|
+use Magento\Catalog\Model\Product\Option\Type\File\ImageContentProcessor; |
|
use Magento\Framework\DataObject; |
|
+use Magento\Framework\Exception\NoSuchEntityException; |
|
use Magento\Quote\Api\Data\CartItemInterface; |
|
use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; |
|
use Magento\Quote\Api\Data\ProductOptionExtensionFactory; |
|
use Magento\Quote\Model\Quote\ProductOptionFactory; |
|
|
|
+/** |
|
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
|
+ */ |
|
class CustomOptionProcessor implements CartItemProcessorInterface |
|
{ |
|
/** |
|
@@ -46,18 +54,32 @@ |
|
private $serializer; |
|
|
|
/** |
|
+ * @var ProductRepositoryInterface |
|
+ */ |
|
+ private $productRepository; |
|
+ |
|
+ /** |
|
+ * @var ImageContentProcessor |
|
+ */ |
|
+ private $imageContentProcessor; |
|
+ |
|
+ /** |
|
* @param DataObject\Factory $objectFactory |
|
* @param ProductOptionFactory $productOptionFactory |
|
* @param ProductOptionExtensionFactory $extensionFactory |
|
* @param CustomOptionFactory $customOptionFactory |
|
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer |
|
+ * @param ProductRepositoryInterface|null $productRepository |
|
+ * @param ImageContentProcessor|null $imageContentProcessor |
|
*/ |
|
public function __construct( |
|
\Magento\Framework\DataObject\Factory $objectFactory, |
|
\Magento\Quote\Model\Quote\ProductOptionFactory $productOptionFactory, |
|
\Magento\Quote\Api\Data\ProductOptionExtensionFactory $extensionFactory, |
|
\Magento\Catalog\Model\CustomOptions\CustomOptionFactory $customOptionFactory, |
|
- ?\Magento\Framework\Serialize\Serializer\Json $serializer = null |
|
+ ?\Magento\Framework\Serialize\Serializer\Json $serializer = null, |
|
+ ?ProductRepositoryInterface $productRepository = null, |
|
+ ?ImageContentProcessor $imageContentProcessor = null |
|
) { |
|
$this->objectFactory = $objectFactory; |
|
$this->productOptionFactory = $productOptionFactory; |
|
@@ -65,6 +87,10 @@ |
|
$this->customOptionFactory = $customOptionFactory; |
|
$this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() |
|
->get(\Magento\Framework\Serialize\Serializer\Json::class); |
|
+ $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() |
|
+ ->get(ProductRepositoryInterface::class); |
|
+ $this->imageContentProcessor = $imageContentProcessor ?: \Magento\Framework\App\ObjectManager::getInstance() |
|
+ ->get(ImageContentProcessor::class); |
|
} |
|
|
|
/** |
|
@@ -72,17 +98,19 @@ |
|
*/ |
|
public function convertToBuyRequest(CartItemInterface $cartItem) |
|
{ |
|
- if ($cartItem->getProductOption() |
|
- && $cartItem->getProductOption()->getExtensionAttributes() |
|
- && $cartItem->getProductOption()->getExtensionAttributes()->getCustomOptions()) { |
|
- $customOptions = $cartItem->getProductOption()->getExtensionAttributes()->getCustomOptions(); |
|
- if (!empty($customOptions) && is_array($customOptions)) { |
|
- $requestData = []; |
|
- foreach ($customOptions as $option) { |
|
- $requestData['options'][$option->getOptionId()] = $option->getOptionValue(); |
|
- } |
|
- return $this->objectFactory->create($requestData); |
|
+ $customOptions = $cartItem->getProductOption() |
|
+ ?->getExtensionAttributes() |
|
+ ?->getCustomOptions(); |
|
+ if (!empty($customOptions)) { |
|
+ $productOptions = $this->getProductCustomOptions($cartItem); |
|
+ $requestData = []; |
|
+ foreach ($customOptions as $option) { |
|
+ $requestData['options'][$option->getOptionId()] = $this->getCustomOptionValue( |
|
+ $option, |
|
+ $productOptions[$option->getOptionId()] ?? null |
|
+ ); |
|
} |
|
+ return $this->objectFactory->create($requestData); |
|
} |
|
return null; |
|
} |
|
@@ -99,7 +127,6 @@ |
|
? $cartItem->getProductOption() |
|
: $this->productOptionFactory->create(); |
|
|
|
- /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ |
|
$extensibleAttribute = $productOption->getExtensionAttributes() |
|
? $productOption->getExtensionAttributes() |
|
: $this->extensionFactory->create(); |
|
@@ -136,7 +163,7 @@ |
|
protected function updateOptionsValues(array &$options) |
|
{ |
|
foreach ($options as $optionId => &$optionValue) { |
|
- /** @var \Magento\Catalog\Model\CustomOptions\CustomOption $option */ |
|
+ /** @var CustomOption $option */ |
|
$option = $this->customOptionFactory->create(); |
|
$option->setOptionId($optionId); |
|
if (is_array($optionValue)) { |
|
@@ -204,4 +231,55 @@ |
|
} |
|
return $this->urlBuilder; |
|
} |
|
+ |
|
+ /** |
|
+ * Get Product Options |
|
+ * |
|
+ * @param CartItemInterface $cartItem |
|
+ * @return ProductCustomOptionInterface[] |
|
+ */ |
|
+ private function getProductCustomOptions(CartItemInterface $cartItem): array |
|
+ { |
|
+ try { |
|
+ $product = $this->productRepository->get($cartItem->getSku()); |
|
+ } catch (NoSuchEntityException) { |
|
+ $product = null; |
|
+ } |
|
+ |
|
+ $options = []; |
|
+ foreach ($product?->getHasOptions() ? $product->getOptions() : [] as $option) { |
|
+ $options[$option->getOptionId()] = $option; |
|
+ } |
|
+ return $options; |
|
+ } |
|
+ |
|
+ /** |
|
+ * Get custom option value depending on the type of custom option |
|
+ * |
|
+ * @param CustomOptionInterface $customOption |
|
+ * @param ProductCustomOptionInterface|null $productCustomOption |
|
+ * @return string|array|null |
|
+ */ |
|
+ private function getCustomOptionValue( |
|
+ CustomOptionInterface $customOption, |
|
+ ?ProductCustomOptionInterface $productCustomOption = null |
|
+ ): mixed { |
|
+ if ($customOption->getExtensionAttributes()?->getFileInfo()) { |
|
+ if ($productCustomOption |
|
+ && $productCustomOption->getType() === ProductCustomOptionInterface::OPTION_TYPE_FILE |
|
+ ) { |
|
+ return $this->imageContentProcessor->process( |
|
+ $customOption->getExtensionAttributes()->getFileInfo(), |
|
+ $productCustomOption |
|
+ ); |
|
+ } elseif ($customOption instanceof CustomOption) { |
|
+ // Check if the custom option is an instance of CustomOption for backward compatibility |
|
+ // Bypass CustomOption::getOptionValue as the current implementation would process the file |
|
+ // even if it is not a file option. |
|
+ return $customOption->getData(CustomOptionInterface::OPTION_VALUE); |
|
+ } |
|
+ } |
|
+ |
|
+ return $customOption->getOptionValue(); |
|
+ } |
|
} |
|
diff --color -ruN catalog-patch-orig/Model/Product/Option/Type/File/ImageContentProcessor.php catalog-patch/Model/Product/Option/Type/File/ImageContentProcessor.php |
|
--- a/Model/Product/Option/Type/File/ImageContentProcessor.php 2026-03-23 14:03:43.376996367 +0000 |
|
+++ b/Model/Product/Option/Type/File/ImageContentProcessor.php 2026-03-23 14:04:44.327524119 +0000 |
|
@@ -0,0 +1,163 @@ |
|
+<?php |
|
+/** |
|
+ * Copyright 2025 Adobe |
|
+ * All Rights Reserved. |
|
+ */ |
|
+ |
|
+declare(strict_types=1); |
|
+ |
|
+namespace Magento\Catalog\Model\Product\Option\Type\File; |
|
+ |
|
+use Magento\Catalog\Model\Product\Option; |
|
+use Magento\Framework\Api\Data\ImageContentInterface; |
|
+use Magento\Framework\Api\ImageContentUploaderInterface; |
|
+use Magento\Framework\App\Config\ScopeConfigInterface; |
|
+use Magento\Framework\App\Filesystem\DirectoryList; |
|
+use Magento\Framework\Exception\LocalizedException; |
|
+use Magento\Framework\File\Size; |
|
+use Magento\Framework\Filesystem; |
|
+use Magento\Framework\Filesystem\Io\File as IoFile; |
|
+use Magento\Framework\Image\Factory; |
|
+use Magento\Framework\Validator\ValidatorChainFactory; |
|
+use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; |
|
+ |
|
+class ImageContentProcessor extends Validator |
|
+{ |
|
+ private const QUOTE_PATH = 'custom_options/quote'; |
|
+ private const ORDER_PATH = 'custom_options/order'; |
|
+ |
|
+ /** |
|
+ * @param ScopeConfigInterface $scopeConfig |
|
+ * @param Size $fileSize |
|
+ * @param Filesystem $filesystem |
|
+ * @param NotProtectedExtension $extensionValidator |
|
+ * @param ImageContentUploaderInterface $uploader |
|
+ * @param ValidatorChainFactory $validatorChainFactory |
|
+ * @param Factory $imageFactory |
|
+ * @param IoFile $ioFile |
|
+ * @param string $quotePath |
|
+ * @param string $orderPath |
|
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList) |
|
+ */ |
|
+ public function __construct( |
|
+ ScopeConfigInterface $scopeConfig, |
|
+ Size $fileSize, |
|
+ private readonly Filesystem $filesystem, |
|
+ private readonly NotProtectedExtension $extensionValidator, |
|
+ private readonly ImageContentUploaderInterface $uploader, |
|
+ private readonly ValidatorChainFactory $validatorChainFactory, |
|
+ private readonly Factory $imageFactory, |
|
+ private readonly IoFile $ioFile, |
|
+ private readonly string $quotePath = self::QUOTE_PATH, |
|
+ private readonly string $orderPath = self::ORDER_PATH |
|
+ ) { |
|
+ parent::__construct($scopeConfig, $filesystem, $fileSize); |
|
+ } |
|
+ |
|
+ /** |
|
+ * Process image content for product option and return file information |
|
+ * |
|
+ * @param ImageContentInterface $imageContent |
|
+ * @param Option $option |
|
+ * @return array|null |
|
+ * @throws LocalizedException |
|
+ */ |
|
+ public function process(ImageContentInterface $imageContent, Option $option): ?array |
|
+ { |
|
+ if (!$imageContent->getBase64EncodedData()) { |
|
+ return null; |
|
+ } |
|
+ $this->validateBeforeSaveToTmp($imageContent, $option); |
|
+ |
|
+ $tmpFilename = $this->uploader->saveToTmpDir($imageContent); |
|
+ $validatorChain = $this->validatorChainFactory->create(); |
|
+ $validatorChain = $this->buildImageValidator($validatorChain, $option); |
|
+ |
|
+ $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); |
|
+ if ($validatorChain->isValid($tmpDirectory->getAbsolutePath($tmpFilename))) { |
|
+ $size = $tmpDirectory->stat($tmpFilename)['size']; |
|
+ if (!$size) { |
|
+ throw new LocalizedException(__('The file is empty. Select another file and try again.')); |
|
+ } |
|
+ $imageInstance = $this->imageFactory->create($tmpDirectory->getAbsolutePath($tmpFilename)); |
|
+ $width = $imageInstance->getOriginalWidth(); |
|
+ $height = $imageInstance->getOriginalHeight(); |
|
+ |
|
+ $fileHash = hash('sha256', $tmpDirectory->readFile($tmpFilename)); |
|
+ $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); |
|
+ $filePath = $this->uploader->moveFromTmpDir( |
|
+ $imageContent, |
|
+ $tmpFilename, |
|
+ $mediaDirectory, |
|
+ $this->quotePath |
|
+ ); |
|
+ $fileFullPath = $mediaDirectory->getAbsolutePath($this->quotePath . $filePath); |
|
+ |
|
+ return [ |
|
+ 'type' => $imageContent->getType(), |
|
+ 'title' => $imageContent->getName(), |
|
+ 'quote_path' => $this->quotePath . $filePath, |
|
+ 'order_path' => $this->orderPath . $filePath, |
|
+ 'fullpath' => $fileFullPath, |
|
+ 'size' => $size, |
|
+ 'width' => $width, |
|
+ 'height' => $height, |
|
+ 'secret_key' => substr($fileHash, 0, 20), |
|
+ ]; |
|
+ } elseif ($validatorChain->getMessages()) { |
|
+ $errors = $this->getValidatorErrors( |
|
+ array_keys($validatorChain->getMessages()), |
|
+ [ |
|
+ 'title' => $imageContent->getName(), |
|
+ 'name' => $imageContent->getName(), |
|
+ 'tmp_name' => $tmpDirectory->getAbsolutePath($tmpFilename), |
|
+ 'type' => $imageContent->getType(), |
|
+ 'size' => 0, |
|
+ ], |
|
+ $option |
|
+ ); |
|
+ |
|
+ if (count($errors) > 0) { |
|
+ throw new LocalizedException(__(implode("\n", $errors))); |
|
+ } |
|
+ } |
|
+ |
|
+ return null; |
|
+ } |
|
+ |
|
+ /** |
|
+ * Get extension based on filename |
|
+ * |
|
+ * @param string $filename |
|
+ * @return string|null |
|
+ */ |
|
+ private function getFileExtension(string $filename): ?string |
|
+ { |
|
+ $pathInfo = $this->ioFile->getPathInfo($filename); |
|
+ |
|
+ if (!isset($pathInfo['extension'])) { |
|
+ return null; |
|
+ } |
|
+ return $pathInfo['extension']; |
|
+ } |
|
+ |
|
+ /** |
|
+ * Validate image content before saving to temporary directory |
|
+ * |
|
+ * @param ImageContentInterface $imageContent |
|
+ * @param Option $option |
|
+ * @return void |
|
+ * @throws LocalizedException |
|
+ */ |
|
+ private function validateBeforeSaveToTmp(ImageContentInterface $imageContent, Option $option): void |
|
+ { |
|
+ $extension = $this->getFileExtension($imageContent->getName() ?: ''); |
|
+ if ($extension !== null && (!$extension || !$this->extensionValidator->isValid($extension))) { |
|
+ throw new LocalizedException(__( |
|
+ "The file '%1' for '%2' has an invalid extension.", |
|
+ $imageContent->getName(), |
|
+ $option->getTitle() |
|
+ )); |
|
+ } |
|
+ } |
|
+} |
|
diff --color -ruN catalog-patch-orig/etc/di.xml catalog-patch/etc/di.xml |
|
--- a/etc/di.xml 2026-03-23 14:03:43.363996255 +0000 |
|
+++ b/etc/di.xml 2026-03-23 14:05:02.787684343 +0000 |
|
@@ -1396,4 +1396,10 @@ |
|
<argument name="attributeSetFactory" xsi:type="object">Magento\Catalog\Virtual\Product\Attribute\SetFactory</argument> |
|
</arguments> |
|
</type> |
|
+ <type name="Magento\Catalog\Model\Product\Option\Type\File\ImageContentProcessor"> |
|
+ <arguments> |
|
+ <argument name="quotePath" xsi:type="string">custom_options/quote</argument> |
|
+ <argument name="orderPath" xsi:type="string">custom_options/order</argument> |
|
+ </arguments> |
|
+ </type> |
|
</config> |
Just wanted to say thank you for taking the time and effort to pull these out and provide them as such, much appreciated.