Skip to content

Commit 2c63173

Browse files
authored
Merge branch '2.4-develop' into AC-15542
2 parents 75cf8e7 + a1c57b2 commit 2c63173

File tree

61 files changed

+1614
-193
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1614
-193
lines changed

app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/>
7070
<!-- Log out -->
7171
<comment userInput="Log out" stepKey="commentLogOut"/>
72+
<deleteData createDataKey="createCategory" stepKey="deletecreateCategory"/>
7273
<actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/>
7374
</after>
7475
<!-- Login to the Storefront as created customer -->

app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
<actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridFilter"/>
8888

8989
<!-- Admin logout -->
90+
<deleteData createDataKey="createCategory" stepKey="deletecreateCategory"/>
9091
<actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/>
9192
</after>
9293

app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
</before>
3333
<after>
3434
<!--Logging out-->
35+
<actionGroup ref="AdminDeleteCategoryByNameActionGroup" stepKey="deleteCategory">
36+
<argument name="categoryName" value="{{SimpleSubCategory.name}}"/>
37+
</actionGroup>
3538
<actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/>
3639
<deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/>
3740
<deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/>

app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckPriceForDynamicBundleProductWithMixedDiscountsTest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@
173173
<deleteData createDataKey="sixthProduct" stepKey="deleteSixthProduct"/>
174174
<deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/>
175175
<actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteCatalogPriceRules"/>
176+
<deleteData createDataKey="category1" stepKey="deletecategory1"/>
177+
<deleteData createDataKey="category2" stepKey="deletecategory2"/>
178+
<deleteData createDataKey="category3" stepKey="deletecategory3"/>
176179
<actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/>
177180
</after>
178181
<!-- Go to storefront category page -->

app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckPriceForFixedBundleProductWithCatalogRuleTest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
<deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/>
148148
<actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteCatalogPriceRules"/>
149149
<!-- logout admin -->
150+
<deleteData createDataKey="category1" stepKey="deletecategory1"/>
150151
<actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/>
151152
</after>
152153
<!-- check product prices on category and product page -->

app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
<argument name="sku" value="Simple"/>
3131
</actionGroup>
3232
<actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="clearFilters"/>
33+
<actionGroup ref="AdminDeleteCategoryByNameActionGroup" stepKey="deleteCategory">
34+
<argument name="categoryName" value="New"/>
35+
</actionGroup>
3336
<actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/>
3437
</after>
3538

app/code/Magento/Catalog/Model/CustomOptions/CustomOptionProcessor.php

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@
33
* Copyright 2015 Adobe
44
* All Rights Reserved.
55
*/
6+
67
namespace Magento\Catalog\Model\CustomOptions;
78

9+
use Magento\Catalog\Api\Data\CustomOptionInterface;
10+
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\Catalog\Model\Product\Option\Type\File\ImageContentProcessor;
813
use Magento\Framework\DataObject;
14+
use Magento\Framework\Exception\NoSuchEntityException;
915
use Magento\Quote\Api\Data\CartItemInterface;
1016
use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface;
1117
use Magento\Quote\Api\Data\ProductOptionExtensionFactory;
1218
use Magento\Quote\Model\Quote\ProductOptionFactory;
1319

20+
/**
21+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
22+
*/
1423
class CustomOptionProcessor implements CartItemProcessorInterface
1524
{
1625
/**
@@ -45,41 +54,63 @@ class CustomOptionProcessor implements CartItemProcessorInterface
4554
*/
4655
private $serializer;
4756

57+
/**
58+
* @var ProductRepositoryInterface
59+
*/
60+
private $productRepository;
61+
62+
/**
63+
* @var ImageContentProcessor
64+
*/
65+
private $imageContentProcessor;
66+
4867
/**
4968
* @param DataObject\Factory $objectFactory
5069
* @param ProductOptionFactory $productOptionFactory
5170
* @param ProductOptionExtensionFactory $extensionFactory
5271
* @param CustomOptionFactory $customOptionFactory
5372
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
73+
* @param ProductRepositoryInterface|null $productRepository
74+
* @param ImageContentProcessor|null $imageContentProcessor
5475
*/
5576
public function __construct(
5677
\Magento\Framework\DataObject\Factory $objectFactory,
5778
\Magento\Quote\Model\Quote\ProductOptionFactory $productOptionFactory,
5879
\Magento\Quote\Api\Data\ProductOptionExtensionFactory $extensionFactory,
5980
\Magento\Catalog\Model\CustomOptions\CustomOptionFactory $customOptionFactory,
60-
?\Magento\Framework\Serialize\Serializer\Json $serializer = null
81+
?\Magento\Framework\Serialize\Serializer\Json $serializer = null,
82+
?ProductRepositoryInterface $productRepository = null,
83+
?ImageContentProcessor $imageContentProcessor = null
6184
) {
6285
$this->objectFactory = $objectFactory;
6386
$this->productOptionFactory = $productOptionFactory;
6487
$this->extensionFactory = $extensionFactory;
6588
$this->customOptionFactory = $customOptionFactory;
6689
$this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()
6790
->get(\Magento\Framework\Serialize\Serializer\Json::class);
91+
$this->productRepository = $productRepository
92+
?: \Magento\Framework\App\ObjectManager::getInstance()
93+
->get(ProductRepositoryInterface::class);
94+
$this->imageContentProcessor = $imageContentProcessor
95+
?: \Magento\Framework\App\ObjectManager::getInstance()
96+
->get(ImageContentProcessor::class);
6897
}
6998

7099
/**
71100
* @inheritDoc
72101
*/
73102
public function convertToBuyRequest(CartItemInterface $cartItem)
74103
{
75-
if ($cartItem->getProductOption()
76-
&& $cartItem->getProductOption()->getExtensionAttributes()
77-
&& $cartItem->getProductOption()->getExtensionAttributes()->getCustomOptions()) {
104+
if ($cartItem->getProductOption()?->getExtensionAttributes()?->getCustomOptions()) {
78105
$customOptions = $cartItem->getProductOption()->getExtensionAttributes()->getCustomOptions();
79-
if (!empty($customOptions) && is_array($customOptions)) {
106+
if (!empty($customOptions)) {
80107
$requestData = [];
108+
$productOptions = $this->getProductCustomOptions($cartItem);
81109
foreach ($customOptions as $option) {
82-
$requestData['options'][$option->getOptionId()] = $option->getOptionValue();
110+
$requestData['options'][$option->getOptionId()] = $this->getCustomOptionValue(
111+
$option,
112+
$productOptions[$option->getOptionId()] ?? null
113+
);
83114
}
84115
return $this->objectFactory->create($requestData);
85116
}
@@ -99,7 +130,6 @@ public function processOptions(CartItemInterface $cartItem)
99130
? $cartItem->getProductOption()
100131
: $this->productOptionFactory->create();
101132

102-
/** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */
103133
$extensibleAttribute = $productOption->getExtensionAttributes()
104134
? $productOption->getExtensionAttributes()
105135
: $this->extensionFactory->create();
@@ -136,7 +166,7 @@ protected function getOptions(CartItemInterface $cartItem)
136166
protected function updateOptionsValues(array &$options)
137167
{
138168
foreach ($options as $optionId => &$optionValue) {
139-
/** @var \Magento\Catalog\Model\CustomOptions\CustomOption $option */
169+
/** @var CustomOption $option */
140170
$option = $this->customOptionFactory->create();
141171
$option->setOptionId($optionId);
142172
if (is_array($optionValue)) {
@@ -204,4 +234,55 @@ private function getUrlBuilder()
204234
}
205235
return $this->urlBuilder;
206236
}
237+
238+
/**
239+
* Get Product Options
240+
*
241+
* @param CartItemInterface $cartItem
242+
* @return ProductCustomOptionInterface[]
243+
*/
244+
private function getProductCustomOptions(CartItemInterface $cartItem): array
245+
{
246+
try {
247+
$product = $this->productRepository->get($cartItem->getSku());
248+
} catch (NoSuchEntityException) {
249+
$product = null;
250+
}
251+
252+
$options = [];
253+
foreach ($product?->getHasOptions() ? $product->getOptions() : [] as $option) {
254+
$options[$option->getOptionId()] = $option;
255+
}
256+
return $options;
257+
}
258+
259+
/**
260+
* Get custom option value depending on the type of custom option
261+
*
262+
* @param CustomOptionInterface $customOption
263+
* @param ProductCustomOptionInterface|null $productCustomOption
264+
* @return string|array|null
265+
*/
266+
private function getCustomOptionValue(
267+
CustomOptionInterface $customOption,
268+
?ProductCustomOptionInterface $productCustomOption = null
269+
): mixed {
270+
if ($customOption->getExtensionAttributes()?->getFileInfo()) {
271+
if ($productCustomOption
272+
&& $productCustomOption->getType() === ProductCustomOptionInterface::OPTION_TYPE_FILE
273+
) {
274+
return $this->imageContentProcessor->process(
275+
$customOption->getExtensionAttributes()->getFileInfo(),
276+
$productCustomOption
277+
);
278+
} elseif ($customOption instanceof CustomOption) {
279+
// Check if the custom option is an instance of CustomOption for backward compatibility
280+
// Bypass CustomOption::getOptionValue as the current implementation would process the file
281+
// even if it is not a file option.
282+
return $customOption->getData(CustomOptionInterface::OPTION_VALUE);
283+
}
284+
}
285+
286+
return $customOption->getOptionValue();
287+
}
207288
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Model\Product\Option\Type\File;
10+
11+
use Magento\Catalog\Model\Product\Option;
12+
use Magento\Framework\Api\Data\ImageContentInterface;
13+
use Magento\Framework\Api\ImageContentUploaderInterface;
14+
use Magento\Framework\App\Config\ScopeConfigInterface;
15+
use Magento\Framework\App\Filesystem\DirectoryList;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Framework\File\Size;
18+
use Magento\Framework\Filesystem;
19+
use Magento\Framework\Filesystem\Io\File as IoFile;
20+
use Magento\Framework\Image\Factory;
21+
use Magento\Framework\Validator\ValidatorChainFactory;
22+
use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension;
23+
24+
class ImageContentProcessor extends Validator
25+
{
26+
private const QUOTE_PATH = 'custom_options/quote';
27+
private const ORDER_PATH = 'custom_options/order';
28+
29+
/**
30+
* @param ScopeConfigInterface $scopeConfig
31+
* @param Size $fileSize
32+
* @param Filesystem $filesystem
33+
* @param NotProtectedExtension $extensionValidator
34+
* @param ImageContentUploaderInterface $uploader
35+
* @param ValidatorChainFactory $validatorChainFactory
36+
* @param Factory $imageFactory
37+
* @param IoFile $ioFile
38+
* @param string $quotePath
39+
* @param string $orderPath
40+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
41+
*/
42+
public function __construct(
43+
ScopeConfigInterface $scopeConfig,
44+
Size $fileSize,
45+
private readonly Filesystem $filesystem,
46+
private readonly NotProtectedExtension $extensionValidator,
47+
private readonly ImageContentUploaderInterface $uploader,
48+
private readonly ValidatorChainFactory $validatorChainFactory,
49+
private readonly Factory $imageFactory,
50+
private readonly IoFile $ioFile,
51+
private readonly string $quotePath = self::QUOTE_PATH,
52+
private readonly string $orderPath = self::ORDER_PATH
53+
) {
54+
parent::__construct($scopeConfig, $filesystem, $fileSize);
55+
}
56+
57+
/**
58+
* Process image content for product option and return file information
59+
*
60+
* @param ImageContentInterface $imageContent
61+
* @param Option $option
62+
* @return array|null
63+
* @throws LocalizedException
64+
*/
65+
public function process(ImageContentInterface $imageContent, Option $option): ?array
66+
{
67+
if (!$imageContent->getBase64EncodedData()) {
68+
return null;
69+
}
70+
$this->validateBeforeSaveToTmp($imageContent, $option);
71+
72+
$tmpFilename = $this->uploader->saveToTmpDir($imageContent);
73+
$validatorChain = $this->validatorChainFactory->create();
74+
$validatorChain = $this->buildImageValidator($validatorChain, $option);
75+
76+
$tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP);
77+
if ($validatorChain->isValid($tmpDirectory->getAbsolutePath($tmpFilename))) {
78+
$size = $tmpDirectory->stat($tmpFilename)['size'];
79+
if (!$size) {
80+
throw new LocalizedException(__('The file is empty. Select another file and try again.'));
81+
}
82+
$imageInstance = $this->imageFactory->create($tmpDirectory->getAbsolutePath($tmpFilename));
83+
$width = $imageInstance->getOriginalWidth();
84+
$height = $imageInstance->getOriginalHeight();
85+
86+
$fileHash = hash('sha256', $tmpDirectory->readFile($tmpFilename));
87+
$mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
88+
$filePath = $this->uploader->moveFromTmpDir(
89+
$imageContent,
90+
$tmpFilename,
91+
$mediaDirectory,
92+
$this->quotePath
93+
);
94+
$fileFullPath = $mediaDirectory->getAbsolutePath($this->quotePath . $filePath);
95+
96+
return [
97+
'type' => $imageContent->getType(),
98+
'title' => $imageContent->getName(),
99+
'quote_path' => $this->quotePath . $filePath,
100+
'order_path' => $this->orderPath . $filePath,
101+
'fullpath' => $fileFullPath,
102+
'size' => $size,
103+
'width' => $width,
104+
'height' => $height,
105+
'secret_key' => substr($fileHash, 0, 20),
106+
];
107+
} elseif ($validatorChain->getMessages()) {
108+
$errors = $this->getValidatorErrors(
109+
array_keys($validatorChain->getMessages()),
110+
[
111+
'title' => $imageContent->getName(),
112+
'name' => $imageContent->getName(),
113+
'tmp_name' => $tmpDirectory->getAbsolutePath($tmpFilename),
114+
'type' => $imageContent->getType(),
115+
'size' => 0,
116+
],
117+
$option
118+
);
119+
120+
if (count($errors) > 0) {
121+
throw new LocalizedException(__(implode("\n", $errors)));
122+
}
123+
}
124+
125+
return null;
126+
}
127+
128+
/**
129+
* Get extension based on filename
130+
*
131+
* @param string $filename
132+
* @return string|null
133+
*/
134+
private function getFileExtension(string $filename): ?string
135+
{
136+
$pathInfo = $this->ioFile->getPathInfo($filename);
137+
138+
if (!isset($pathInfo['extension'])) {
139+
return null;
140+
}
141+
return $pathInfo['extension'];
142+
}
143+
144+
/**
145+
* Validate image content before saving to temporary directory
146+
*
147+
* @param ImageContentInterface $imageContent
148+
* @param Option $option
149+
* @return void
150+
* @throws LocalizedException
151+
*/
152+
private function validateBeforeSaveToTmp(ImageContentInterface $imageContent, Option $option): void
153+
{
154+
$extension = $this->getFileExtension($imageContent->getName() ?: '');
155+
if ($extension !== null && (!$extension || !$this->extensionValidator->isValid($extension))) {
156+
throw new LocalizedException(__(
157+
"The file '%1' for '%2' has an invalid extension.",
158+
$imageContent->getName(),
159+
$option->getTitle()
160+
));
161+
}
162+
}
163+
}

app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ private function getCountFromCategoryTableBulk(
408408
)
409409
->joinInner(
410410
['ce2' => $this->getTable('catalog_category_entity')],
411-
'ce2.path LIKE CONCAT(ce.path, \'/%\') OR ce2.entity_id = ce.entity_id',
411+
'ce2.path LIKE CONCAT(ce.path, \'/%\')',
412412
[]
413413
)
414414
->where('ce.entity_id IN (?)', $categoryIds);

0 commit comments

Comments
 (0)