diff --git a/vendor/magento/module-catalog/Model/Product/Gallery/CreateHandler.php b/vendor/magento/module-catalog/Model/Product/Gallery/CreateHandler.php index 7a1bd21d781..125ac325d61 100644 --- a/vendor/magento/module-catalog/Model/Product/Gallery/CreateHandler.php +++ b/vendor/magento/module-catalog/Model/Product/Gallery/CreateHandler.php @@ -8,10 +8,18 @@ declare(strict_types=1); namespace Magento\Catalog\Model\Product\Gallery; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Json\Helper\Data; +use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\MediaStorage\Model\File\Uploader as FileUploader; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -91,6 +99,11 @@ class CreateHandler implements ExtensionInterface */ private $storeManager; + /** + * @var DeleteValidator + */ + private $deleteValidator; + /** * @var string[] */ @@ -101,25 +114,27 @@ class CreateHandler implements ExtensionInterface ]; /** - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository - * @param \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel - * @param \Magento\Framework\Json\Helper\Data $jsonHelper - * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb - * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager - * @throws \Magento\Framework\Exception\FileSystemException + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $attributeRepository + * @param Gallery $resourceModel + * @param Data $jsonHelper + * @param Config $mediaConfig + * @param Filesystem $filesystem + * @param Database $fileStorageDb + * @param StoreManagerInterface|null $storeManager + * @param DeleteValidator|null $deleteValidator + * @throws FileSystemException */ public function __construct( - \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, - \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel, - \Magento\Framework\Json\Helper\Data $jsonHelper, - \Magento\Catalog\Model\Product\Media\Config $mediaConfig, - \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, - \Magento\Store\Model\StoreManagerInterface $storeManager = null + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $attributeRepository, + Gallery $resourceModel, + Data $jsonHelper, + Config $mediaConfig, + Filesystem $filesystem, + Database $fileStorageDb, + StoreManagerInterface $storeManager = null, + ?DeleteValidator $deleteValidator = null ) { $this->metadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $this->attributeRepository = $attributeRepository; @@ -129,6 +144,7 @@ class CreateHandler implements ExtensionInterface $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fileStorageDb = $fileStorageDb; $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + $this->deleteValidator = $deleteValidator ?: ObjectManager::getInstance()->get(DeleteValidator::class); } /** @@ -167,7 +183,7 @@ class CreateHandler implements ExtensionInterface if ($product->getIsDuplicate() != true) { foreach ($value['images'] as &$image) { - if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + if (!empty($image['removed']) && $this->deleteValidator->validate($product, $image['file'])) { $image['removed'] = ''; } @@ -186,7 +202,7 @@ class CreateHandler implements ExtensionInterface // For duplicating we need copy original images. $duplicate = []; foreach ($value['images'] as &$image) { - if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + if (!empty($image['removed']) && $this->deleteValidator->validate($product, $image['file'])) { $image['removed'] = ''; } @@ -606,41 +622,6 @@ class CreateHandler implements ExtensionInterface return $this->imagesGallery; } - /** - * Check possibility to remove image - * - * @param ProductInterface $product - * @param string $imageFile - * @return bool - */ - private function canRemoveImage(ProductInterface $product, string $imageFile) :bool - { - $canRemoveImage = true; - $gallery = $this->getImagesForAllStores($product); - $storeId = $product->getStoreId(); - $storeIds = []; - $storeIds[] = 0; - $websiteIds = array_map('intval', $product->getWebsiteIds() ?? []); - foreach ($this->storeManager->getStores() as $store) { - if (in_array((int) $store->getWebsiteId(), $websiteIds, true)) { - $storeIds[] = (int) $store->getId(); - } - } - - if (!empty($gallery)) { - foreach ($gallery as $image) { - if (in_array((int) $image['store_id'], $storeIds) - && $image['filepath'] === $imageFile - && (int) $image['store_id'] !== $storeId - ) { - $canRemoveImage = false; - } - } - } - - return $canRemoveImage; - } - /** * Get media attribute value for store view * diff --git a/vendor/magento/module-catalog/Model/Product/Gallery/DeleteValidator.php b/vendor/magento/module-catalog/Model/Product/Gallery/DeleteValidator.php new file mode 100644 index 00000000000..845a1dc66dc --- /dev/null +++ b/vendor/magento/module-catalog/Model/Product/Gallery/DeleteValidator.php @@ -0,0 +1,84 @@ +resourceModel = $resourceModel; + } + + /** + * Validates media image for removal + * + * @param ProductInterface $product + * @param string $imageFile + * @return Phrase[] + */ + public function validate(ProductInterface $product, string $imageFile): array + { + $errors = []; + if (count($product->getStoreIds()) > 1) { + if (in_array($imageFile, $this->getImagesWithRolesInOtherStores($product))) { + $errors[] = __('The image cannot be removed as it has been assigned to the other image role'); + } + } + + return $errors; + } + + /** + * Returns all images that are assigned to a role in store views other than the current store view + * + * @param ProductInterface $product + * @return array + */ + private function getImagesWithRolesInOtherStores(ProductInterface $product): array + { + if ($this->product !== $product || !$this->imagesWithRolesInOtherStoresCache) { + $this->product = $product; + $storeIds = array_diff( + array_merge($product->getStoreIds(), [Store::DEFAULT_STORE_ID]), + [$product->getStoreId()] + ); + $this->imagesWithRolesInOtherStoresCache = array_column( + $this->resourceModel->getProductImages($product, $storeIds), + 'filepath' + ); + } + return $this->imagesWithRolesInOtherStoresCache; + } +} diff --git a/vendor/magento/module-catalog/Model/Product/Gallery/GalleryManagement.php b/vendor/magento/module-catalog/Model/Product/Gallery/GalleryManagement.php index 6a078a91511..07ddd084601 100644 --- a/vendor/magento/module-catalog/Model/Product/Gallery/GalleryManagement.php +++ b/vendor/magento/module-catalog/Model/Product/Gallery/GalleryManagement.php @@ -7,10 +7,21 @@ namespace Magento\Catalog\Model\Product\Gallery; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Api\ImageContentValidatorInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File\Mime; +use Magento\Framework\Filesystem\Io\File; /** * Class GalleryManagement @@ -32,17 +43,74 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal protected $contentValidator; /** - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository - * @param ImageContentValidatorInterface $contentValidator + * @var ProductInterfaceFactory + */ + private $productInterfaceFactory; + + /** + * @var DeleteValidator + */ + private $deleteValidator; + + /** + * @var ImageContentInterfaceFactory + */ + protected $imageContentInterface; + + /** + * Filesystem facade * + * @var Filesystem + */ + protected $filesystem; + + /** + * @var Mime + */ + protected $mime; + + /** + * @var File + */ + protected $file; + + /** + * @param ProductRepositoryInterface $productRepository + * @param ImageContentValidatorInterface $contentValidator + * @param ProductInterfaceFactory|null $productInterfaceFactory + * @param DeleteValidator|null $deleteValidator + * @param ImageContentInterfaceFactory|null $imageContentInterface + * @param Filesystem|null $filesystem + * @param Mime|null $mime + * @param File|null $file * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, - ImageContentValidatorInterface $contentValidator + ProductRepositoryInterface $productRepository, + ImageContentValidatorInterface $contentValidator, + ?ProductInterfaceFactory $productInterfaceFactory = null, + ?DeleteValidator $deleteValidator = null, + ?ImageContentInterfaceFactory $imageContentInterface = null, + ?Filesystem $filesystem = null, + ?Mime $mime = null, + ?File $file = null ) { $this->productRepository = $productRepository; $this->contentValidator = $contentValidator; + $this->productInterfaceFactory = $productInterfaceFactory + ?? ObjectManager::getInstance()->get(ProductInterfaceFactory::class); + $this->deleteValidator = $deleteValidator + ?? ObjectManager::getInstance()->get(DeleteValidator::class); + $this->imageContentInterface = $imageContentInterface + ?? ObjectManager::getInstance()->get(ImageContentInterfaceFactory::class); + $this->filesystem = $filesystem + ?? ObjectManager::getInstance()->get(Filesystem::class); + $this->mime = $mime + ?? ObjectManager::getInstance()->get(Mime::class); + $this->file = $file + ?? ObjectManager::getInstance()->get( + File::class + ); } /** @@ -72,6 +140,8 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal } $existingMediaGalleryEntries[] = $entry; } + $product = $this->productInterfaceFactory->create(); + $product->setSku($sku); $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); @@ -119,6 +189,8 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal __('No image with the provided ID was found. Verify the ID and try again.') ); } + $product = $this->productInterfaceFactory->create(); + $product->setSku($sku); $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { @@ -145,6 +217,10 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal foreach ($existingMediaGalleryEntries as $key => $entry) { if ($entry->getId() == $entryId) { unset($existingMediaGalleryEntries[$key]); + $errors = $this->deleteValidator->validate($product, $entry->getFile()); + if (!empty($errors)) { + throw new StateException($errors[0]); + } $found = true; break; } @@ -154,6 +230,8 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal __('No image with the provided ID was found. Verify the ID and try again.') ); } + $product = $this->productInterfaceFactory->create(); + $product->setSku($sku); $product->setMediaGalleryEntries($existingMediaGalleryEntries); $this->productRepository->save($product); return true; @@ -165,6 +243,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal public function get($sku, $entryId) { try { + /** @var Product $product */ $product = $this->productRepository->get($sku); } catch (\Exception $exception) { throw new NoSuchEntityException(__("The product doesn't exist. Verify and try again.")); @@ -173,6 +252,7 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal $mediaGalleryEntries = $product->getMediaGalleryEntries(); foreach ($mediaGalleryEntries as $entry) { if ($entry->getId() == $entryId) { + $entry->setContent($this->getImageContent($product, $entry)); return $entry; } } @@ -185,9 +265,32 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal */ public function getList($sku) { - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product = $this->productRepository->get($sku); + $mediaGalleryEntries = $product->getMediaGalleryEntries(); + foreach ($mediaGalleryEntries as $entry) { + $entry->setContent($this->getImageContent($product, $entry)); + } + return $mediaGalleryEntries; + } - return $product->getMediaGalleryEntries(); + /** + * Get image content + * + * @param Product $product + * @param ProductAttributeMediaGalleryEntryInterface $entry + * @return ImageContentInterface + * @throws FileSystemException + */ + private function getImageContent($product, $entry): ImageContentInterface + { + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $path = $mediaDirectory->getAbsolutePath($product->getMediaConfig()->getMediaPath($entry->getFile())); + $fileName = $this->file->getPathInfo($path)['basename']; + $imageFileContent = $mediaDirectory->getDriver()->fileGetContents($path); + return $this->imageContentInterface->create() + ->setName($fileName) + ->setBase64EncodedData(base64_encode($imageFileContent)) + ->setType($this->mime->getMimeType($path)); } } diff --git a/vendor/magento/module-catalog/Model/ProductRepository/MediaGalleryProcessor.php b/vendor/magento/module-catalog/Model/ProductRepository/MediaGalleryProcessor.php index ecb7322ac10..b67cf4acd9f 100644 --- a/vendor/magento/module-catalog/Model/ProductRepository/MediaGalleryProcessor.php +++ b/vendor/magento/module-catalog/Model/ProductRepository/MediaGalleryProcessor.php @@ -8,14 +8,18 @@ declare(strict_types=1); namespace Magento\Catalog\Model\ProductRepository; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Gallery\DeleteValidator; use Magento\Catalog\Model\Product\Gallery\Processor; use Magento\Catalog\Model\Product\Media\Config; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; +use Magento\Store\Model\Store; /** * Process Media gallery data for ProductRepository before save product. @@ -37,25 +41,31 @@ class MediaGalleryProcessor private $contentFactory; /** - * Image processor. - * * @var ImageProcessorInterface */ private $imageProcessor; + /** + * @var DeleteValidator + */ + private $deleteValidator; + /** * @param Processor $processor * @param ImageContentInterfaceFactory $contentFactory * @param ImageProcessorInterface $imageProcessor + * @param DeleteValidator|null $deleteValidator */ public function __construct( Processor $processor, ImageContentInterfaceFactory $contentFactory, - ImageProcessorInterface $imageProcessor + ImageProcessorInterface $imageProcessor, + ?DeleteValidator $deleteValidator = null ) { $this->processor = $processor; $this->contentFactory = $contentFactory; $this->imageProcessor = $imageProcessor; + $this->deleteValidator = $deleteValidator ?? ObjectManager::getInstance()->get(DeleteValidator::class); } /** @@ -103,7 +113,7 @@ class MediaGalleryProcessor // phpcs:ignore Magento2.Performance.ForeachArrayMerge $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); } - } else { + } elseif ($this->canRemoveImage($product, $existingEntry)) { //set the removed flag $existingEntry['removed'] = true; } @@ -254,4 +264,17 @@ class MediaGalleryProcessor } } } + + /** + * Check whether the image can be removed + * + * @param ProductInterface $product + * @param array $image + * @return bool + */ + private function canRemoveImage(ProductInterface $product, array $image): bool + { + return !isset($image['file']) + || !$this->deleteValidator->validate($product, $image['file']); + } }