diff --git a/Api/ProductGalleryManagementInterface.php b/Api/ProductGalleryManagementInterface.php index 636d3da..24ae886 100644 --- a/Api/ProductGalleryManagementInterface.php +++ b/Api/ProductGalleryManagementInterface.php @@ -15,9 +15,10 @@ interface ProductGalleryManagementInterface * @param string|null $roles * @param string|null $label * @param bool|int|null $disabled + * @param string $cldspinset * @return string */ - public function addItem($url, $sku, $publicId = null, $roles = null, $label = null, $disabled = 0); + public function addItem($url = null, $sku = null, $publicId = null, $roles = null, $label = null, $disabled = 0, $cldspinset = null); /** * Add multiple gallery items to one or more products from Cloudinary URLs. diff --git a/Api/ResourcesManagementInterface.php b/Api/ResourcesManagementInterface.php index 00d0157..05ba5ce 100644 --- a/Api/ResourcesManagementInterface.php +++ b/Api/ResourcesManagementInterface.php @@ -18,4 +18,11 @@ public function getImage(); * @return string */ public function getVideo(); + + /** + * GET for getSpinestFirstImage api + * + * @return string + */ + public function getResourcesByTag(); } diff --git a/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 60c722f..9ece3fe 100644 --- a/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -14,8 +14,10 @@ namespace Cloudinary\Cloudinary\Block\Adminhtml\Product\Helper\Form\Gallery; use Cloudinary\Cloudinary\Helper\MediaLibraryHelper; +use Cloudinary\Cloudinary\Model\ProductSpinsetMapFactory; use Magento\Backend\Block\Template\Context; use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\Json\DecoderInterface; use Magento\Framework\Json\EncoderInterface; /** @@ -29,27 +31,43 @@ class Content extends \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Galle protected $_template = 'Cloudinary_Cloudinary::catalog/product/helper/gallery.phtml'; /** - * MediaLibraryHelper - * @var array|null + * @var DecoderInterface */ - protected $mediaLibraryHelper; + protected $_jsonDecoder; /** - * @param Context $context - * @param EncoderInterface $jsonEncoder - * @param Config $mediaConfig - * @param MediaLibraryHelper $mediaLibraryHelper - * @param array $data + * @var MediaLibraryHelper + */ + protected $_mediaLibraryHelper; + + /** + * @var ProductSpinsetMapFactory + */ + protected $_productSpinsetMapFactory; + + /** + * @method __construct + * @param Context $context + * @param EncoderInterface $jsonEncoder + * @param DecoderInterface $jsonDecoder + * @param Config $mediaConfig + * @param MediaLibraryHelper $mediaLibraryHelper + * @param ProductSpinsetMapFactory $productSpinsetMapFactory + * @param array $data */ public function __construct( Context $context, EncoderInterface $jsonEncoder, + DecoderInterface $jsonDecoder, Config $mediaConfig, MediaLibraryHelper $mediaLibraryHelper, + ProductSpinsetMapFactory $productSpinsetMapFactory, array $data = [] ) { parent::__construct($context, $jsonEncoder, $mediaConfig, $data); - $this->mediaLibraryHelper = $mediaLibraryHelper; + $this->_jsonDecoder = $jsonDecoder; + $this->_mediaLibraryHelper = $mediaLibraryHelper; + $this->_productSpinsetMapFactory = $productSpinsetMapFactory; } /** @@ -61,7 +79,7 @@ public function __construct( */ public function getCloudinaryMediaLibraryWidgetOptions($multiple = true, $refresh = false) { - if (!($cloudinaryMLoptions = $this->mediaLibraryHelper->getCloudinaryMLOptions($multiple, $refresh))) { + if (!($cloudinaryMLoptions = $this->_mediaLibraryHelper->getCloudinaryMLOptions($multiple, $refresh))) { return null; } @@ -83,7 +101,7 @@ public function getCloudinaryMediaLibraryWidgetOptions($multiple = true, $refres 'useDerived' => false, 'addTmpExtension' => true, 'cloudinaryMLoptions' => $cloudinaryMLoptions, - 'cloudinaryMLshowOptions' => $this->mediaLibraryHelper->getCloudinaryMLshowOptions(null), + 'cloudinaryMLshowOptions' => $this->_mediaLibraryHelper->getCloudinaryMLshowOptions(null), ] ); } @@ -106,4 +124,24 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true) } return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false); } + + /** + * Returns image json + * + * @return string + */ + public function getImagesJson() + { + $images = $this->_jsonDecoder->decode(parent::getImagesJson()); + if ($images) { + foreach ($images as &$image) { + if ($image['media_type'] === 'image') { + $cldspinset = $this->_productSpinsetMapFactory->create()->getCollection()->addFieldToFilter("image_name", $image['file'])->setPageSize(1)->getFirstItem(); + $image['cldspinset'] = $cldspinset ? $cldspinset->getCldspinset() : ""; + } + } + return $this->_jsonEncoder->encode($images); + } + return '[]'; + } } diff --git a/Command/ProductGalleryApiQueueProcess.php b/Command/ProductGalleryApiQueueProcess.php new file mode 100644 index 0000000..65d2961 --- /dev/null +++ b/Command/ProductGalleryApiQueueProcess.php @@ -0,0 +1,62 @@ +appState = $appState; + $this->job = $job; + } + + /** + * Configure the command + * + * @return void + */ + protected function configure() + { + $this->setName('cloudinary:product-gallery-api-queue:process'); + $this->setDescription('Process queued items for product gallery API'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_CRONTAB); + return $this->job + ->setOutput($output) + ->execute(); + } +} diff --git a/Controller/Adminhtml/Ajax/RetrieveImage.php b/Controller/Adminhtml/Ajax/RetrieveImage.php index eaf0ed7..c9a3e2e 100644 --- a/Controller/Adminhtml/Ajax/RetrieveImage.php +++ b/Controller/Adminhtml/Ajax/RetrieveImage.php @@ -3,13 +3,13 @@ namespace Cloudinary\Cloudinary\Controller\Adminhtml\Ajax; use Cloudinary\Cloudinary\Core\ConfigurationInterface; +use Cloudinary\Cloudinary\Model\Framework\File\Uploader; use Cloudinary\Cloudinary\Model\MediaLibraryMapFactory; use Magento\Backend\App\Action\Context; use Magento\Catalog\Model\Product\Media\Config as ProductMediaConfig; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Controller\Result\RawFactory as ResultRawFactory; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\File\Uploader; use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\Curl; use Magento\Framework\Image\AdapterFactory as ImageAdapterFactory; diff --git a/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php b/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php index a790ce7..db33c81 100644 --- a/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php +++ b/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php @@ -9,7 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\File\Uploader; +use Cloudinary\Cloudinary\Model\Framework\File\Uploader; use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\Curl; use Magento\Framework\Image\AdapterFactory; diff --git a/Cron/ProductGalleryApiQueue.php b/Cron/ProductGalleryApiQueue.php new file mode 100644 index 0000000..5dbc20b --- /dev/null +++ b/Cron/ProductGalleryApiQueue.php @@ -0,0 +1,196 @@ +configuration = $configuration; + $this->cldProductGalleryManagement = $cldProductGalleryManagement; + $this->jsonHelper = $jsonHelper; + $this->productVideoFactory = $productVideoFactory; + $this->productGalleryApiQueueFactory = $productGalleryApiQueueFactory; + $this->notifierPool = $notifierPool; + } + + public function execute() + { + if ($this->configuration->isEnabled() && $this->configuration->isEnabledProductgalleryApiQueue()) { + try { + $queuedItems = $this->productGalleryApiQueueFactory->create()->getCollection() + ->addFieldToFilter("success", 0) + ->addFieldToFilter("tryouts", ['lt' => $this->configuration->getProductgalleryApiQueueMaxTryouts()]) + ->setOrder('created_at', 'asc'); + if (($_limit = $this->configuration->getProductgalleryApiQueueLimit())) { + $queuedItems->setPageSize($_limit); + } + + foreach ($queuedItems as $item) { + try { + $fullItemData = $this->jsonHelper->jsonDecode($item->getFullItemData()); + $this->processOutput("ProductGalleryApiQueue::execute() - Processing item ID: {$item->getId()} ...", "debug", ['full_item_data' => $fullItemData]); + $item->setTryouts($item->getTryouts() + 1); + $this->cldProductGalleryManagement->addGalleryItem( + $fullItemData["url"], + $fullItemData["sku"], + $fullItemData["publicId"], + $fullItemData["roles"], + $fullItemData["label"], + $fullItemData["disabled"], + $fullItemData["cldspinset"] + ); + $item->setSuccess(1); + $item->setSuccessAt(date('Y-m-d H:i:s')); + $item->setMessage('success'); + $item->setHasErrors(0); + } catch (\Exception $e) { + $item->setSuccess(0); + $item->setMessage("[ERROR]\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); + $item->setHasErrors(1); + + $this->processOutput("ProductGalleryApiQueue::execute() - Exception during product-gallery API queued item processing: " . $e->getMessage(), 'error', ['trace' => $e->getTraceAsString(), 'queued_item' => $item->getData()]); + if (!($this->output instanceof OutputInterface) && $item->getTryouts() >= $this->configuration->getProductgalleryApiQueueMaxTryouts() && count($this->adminNotificationErrors) < 7) { + $this->adminNotificationErrors[] = [ + "message" => $e->getMessage(), + "tryouts" => $item->getTryouts(), + "item_data" => $fullItemData + ]; + } + } + + $item->save(); + $this->processOutput("ProductGalleryApiQueue::execute() - Processing item ID: {$item->getId()} - Done.", "debug"); + } + } catch (\Exception $e) { + $this->processOutput("ProductGalleryApiQueue::execute() - Exception during product-gallery API queue processing: " . $e->getMessage(), 'error', ['trace' => $e->getTraceAsString()]); + if (!($this->output instanceof OutputInterface) && $item->getTryouts() >= $this->configuration->getProductgalleryApiQueueMaxTryouts() && count($this->adminNotificationErrors) < 7) { + $this->adminNotificationErrors[] = [ + "message" => $e->getMessage(), + "details" => $e->getTraceAsString() + ]; + } + } + if ($this->adminNotificationErrors) { + $adminNotificationErrors = $this->jsonHelper->jsonEncode(array_slice($this->adminNotificationErrors, 0, 5)); + if (count($this->adminNotificationErrors) > 5) { + $adminNotificationErrors .= " ... [this message is too long, check the log for the rest] "; + } + $this->addAdminNotification("[Cloudinary] An error occurred during the background processing of the product-gallery API queue! *More detailes can be found on the Cloudinary log file (var/log/cloudinary_cloudinary.log)", $adminNotificationErrors, 'critical'); + } + } + + return $this; + } + + /** + * @method setOutput + * @param OutputInterface $output + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + return $this; + } + + /** + * @method getOutput + * @return OutputInterface + */ + public function getOutput() + { + return $this->output; + } + + /** + * Process output messages (log to system.log / output to terminal) + * @method _processOutput + * @return $this + */ + protected function processOutput($message, $type = "info", $data = []) + { + if ($this->output instanceof OutputInterface) { + //Output to terminal + $outputType = ($type === "error") ? $type : "info"; + $this->output->writeln('<' . $outputType . '>' . json_encode($message) . '' . $outputType . '>'); + if ($data) { + $this->output->writeln('' . json_encode($data) . ''); + } + } else { + //Log to var/log/cloudinary_cloudinary.log + $this->configuration->log($message, $data); + } + + return $this; + } + + private function addAdminNotification(string $title, $description = "", $type = 'critical') + { + $method = 'add' . ucfirst($type); + $this->notifierPool->{$method}($title, $description); + return $this; + } +} diff --git a/Model/Api/ProductGalleryManagement.php b/Model/Api/ProductGalleryManagement.php index e8ab78d..138ec50 100644 --- a/Model/Api/ProductGalleryManagement.php +++ b/Model/Api/ProductGalleryManagement.php @@ -4,8 +4,10 @@ use Cloudinary\Cloudinary\Core\CloudinaryImageManager; use Cloudinary\Cloudinary\Core\ConfigurationInterface; +use Cloudinary\Cloudinary\Model\Framework\File\Uploader; use Cloudinary\Cloudinary\Model\MediaLibraryMap; use Cloudinary\Cloudinary\Model\MediaLibraryMapFactory; +use Cloudinary\Cloudinary\Model\ProductGalleryApiQueueFactory; use Cloudinary\Cloudinary\Model\ProductImageFinder; use Cloudinary\Cloudinary\Model\TransformationFactory; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -14,9 +16,9 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\File\Uploader; use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\Curl; use Magento\Framework\Image\AdapterFactory as ImageAdapterFactory; @@ -143,33 +145,45 @@ class ProductGalleryManagement implements \Cloudinary\Cloudinary\Api\ProductGall */ private $mediaLibraryMapFactory; + /** + * @var ProductGalleryApiQueueFactory + */ + private $productGalleryApiQueueFactory; + /** * @var AppEmulation */ - protected $_appEmulation; + private $appEmulation; + + /** + * @var ResourceConnection + */ + private $resourceConnection; /** * @method __construct - * @param ConfigurationInterface $configuration - * @param Http $request - * @param JsonHelper $jsonHelper - * @param ProductRepositoryInterface $productRepository - * @param ProductMediaConfig $mediaConfig - * @param Filesystem $fileSystem - * @param ImageAdapterFactory $imageAdapterFactory - * @param Curl $curl - * @param FileUtility $fileUtility - * @param FileProcessor $fileProcessor - * @param AllowedProtocols $protocolValidator - * @param NotProtectedExtension $extensionValidator - * @param StoreManagerInterface $storeManager - * @param ProductImageFinder $productImageFinder - * @param CloudinaryImageManager $cloudinaryImageManager - * @param ResourcesManagement $cloudinaryResourcesManagement - * @param TransformationFactory $transformationFactory - * @param Processor $mediaGalleryProcessor - * @param MediaLibraryMapFactory $mediaLibraryMapFactory - * @param AppEmulation $appEmulation + * @param ConfigurationInterface $configuration + * @param Http $request + * @param JsonHelper $jsonHelper + * @param ProductRepositoryInterface $productRepository + * @param ProductMediaConfig $mediaConfig + * @param Filesystem $fileSystem + * @param ImageAdapterFactory $imageAdapterFactory + * @param Curl $curl + * @param FileUtility $fileUtility + * @param FileProcessor $fileProcessor + * @param AllowedProtocols $protocolValidator + * @param NotProtectedExtension $extensionValidator + * @param StoreManagerInterface $storeManager + * @param ProductImageFinder $productImageFinder + * @param CloudinaryImageManager $cloudinaryImageManager + * @param ResourcesManagement $cloudinaryResourcesManagement + * @param TransformationFactory $transformationFactory + * @param Processor $mediaGalleryProcessor + * @param MediaLibraryMapFactory $mediaLibraryMapFactory + * @param ProductGalleryApiQueueFactory $productGalleryApiQueueFactory + * @param AppEmulation $appEmulation + * @param ResourceConnection $resourceConnection */ public function __construct( ConfigurationInterface $configuration, @@ -191,7 +205,9 @@ public function __construct( TransformationFactory $transformationFactory, Processor $mediaGalleryProcessor, MediaLibraryMapFactory $mediaLibraryMapFactory, - AppEmulation $appEmulation + ProductGalleryApiQueueFactory $productGalleryApiQueueFactory, + AppEmulation $appEmulation, + ResourceConnection $resourceConnection ) { $this->configuration = $configuration; $this->request = $request; @@ -212,7 +228,9 @@ public function __construct( $this->transformationFactory = $transformationFactory; $this->mediaGalleryProcessor = $mediaGalleryProcessor; $this->mediaLibraryMapFactory = $mediaLibraryMapFactory; + $this->productGalleryApiQueueFactory = $productGalleryApiQueueFactory; $this->appEmulation = $appEmulation; + $this->resourceConnection = $resourceConnection; } /** @@ -233,18 +251,18 @@ public function addProductMedia($sku, $urls) __("Cloudinary module is disabled. Please enable it first in order to use this API.") ); } - $this->emulateAdminhtmlArea(); $urls = (array)$urls; foreach ($urls as $i => $url) { try { $url = (array)$url; - $this->addGalleryItem( - $url["url"], + $this->processOrQueue( + (isset($url["url"])) ? $url["url"] : null, $sku, (isset($url["publicId"])) ? $url["publicId"] : null, (isset($url["roles"])) ? $url["roles"] : null, (isset($url["label"])) ? $url["label"] : null, - (isset($url["disabled"])) ? $url["disabled"] : null + (isset($url["disabled"])) ? $url["disabled"] : null, + (isset($url["cldspinset"])) ? $url["cldspinset"] : null ); $result["passed"]++; } catch (\Exception $e) { @@ -253,19 +271,22 @@ public function addProductMedia($sku, $urls) $result["failed"]["urls"][] = $url; } } - $this->stopEnvironmentEmulation(); } catch (\Exception $e) { $result["error"] = 1; $result["message"] = $e->getMessage(); } + if ($result["passed"] && !$result["failed"]["count"]) { + $result["message"] = $this->configuration->isEnabledProductgalleryApiQueue() ? "All items have been added to queue." : "success"; + } + return $this->jsonHelper->jsonEncode($result); } /** * {@inheritdoc} */ - public function addItem($url, $sku, $publicId = null, $roles = null, $label = null, $disabled = 0) + public function addItem($url = null, $sku = null, $publicId = null, $roles = null, $label = null, $disabled = 0, $cldspinset = null) { return $this->addItems([[ "url" => $url, @@ -273,7 +294,8 @@ public function addItem($url, $sku, $publicId = null, $roles = null, $label = nu "publicId" => $publicId, "roles" => $roles, "label" => $label, - "disabled" => $disabled + "disabled" => $disabled, + "cldspinset" => $cldspinset ]]); } @@ -293,20 +315,19 @@ public function addItems($items) __("Cloudinary module is disabled. Please enable it first in order to use this API.") ); } - $this->emulateAdminhtmlArea(); $items = (array)$items; foreach ($items as $i => $item) { try { $item = $result["items"][$i] = (array)$item; $result["items"][$i]["error"] = 0; - $result["items"][$i]["message"] = "success"; - $this->addGalleryItem( - $item["url"], - $item["sku"], - (isset($item["publicId"])) ? $item["publicId"] : null, + $result["items"][$i]["message"] = $this->configuration->isEnabledProductgalleryApiQueue() ? "The item was added to the queue." : "success"; + $this->processOrQueue( + (isset($item["url"])) ? $item["url"] : null, + (isset($item["sku"])) ? $item["sku"] : null(isset($item["publicId"])) ? $item["publicId"] : null, (isset($item["roles"])) ? $item["roles"] : null, (isset($item["label"])) ? $item["label"] : null, - (isset($item["disabled"])) ? $item["disabled"] : null + (isset($item["disabled"])) ? $item["disabled"] : null, + (isset($item["cldspinset"])) ? $item["cldspinset"] : null ); } catch (\Exception $e) { $result["errors"]++; @@ -317,14 +338,13 @@ public function addItems($items) } } } - $this->stopEnvironmentEmulation(); } catch (\Exception $e) { $result["errors"]++; $result["message"] = "\n{$e->getMessage()}"; } if (!$result["errors"]) { - $result["message"] = "success"; + $result["message"] = $this->configuration->isEnabledProductgalleryApiQueue() ? "All items have been added to queue." : "success"; } else { $result["message"] = "error" . $result["message"]; } @@ -333,99 +353,173 @@ public function addItems($items) } /** - * @method addGalleryItem + * @method processOrQueue * @param string $url * @param string $sku * @param string|null $publicId * @param string|null $roles * @param string|null $label * @param bool|int|null $disabled + * @param string $cldspinset */ - private function addGalleryItem($url, $sku, $publicId = null, $roles = null, $label = null, $disabled = 0) + public function processOrQueue($url, $sku, $publicId = null, $roles = null, $label = null, $disabled = 0, $cldspinset = null) { - $this->cldUniqid = $this->mapped = null; - $this->parsedRemoteFileUrl = $this->configuration->parseCloudinaryUrl($url, $publicId); - - if (!$this->parsedRemoteFileUrl["version"] && !$publicId) { + if (!$url && !$cldspinset) { throw new LocalizedException( - __("The `publicId` field is mandatory for Cloudinary URLs that doesn't contain a version number.") + __("The `url` field is mandatory when not passing `cldspinset`.") ); } - - $roles = ($roles) ? array_map('trim', (is_string($roles) ? explode(',', $roles) : (array) $roles)) : null; - $product = $this->productRepository->get($sku); - - $result = $this->retrieveImage($this->parsedRemoteFileUrl['thumbnail_url'] ?: $this->parsedRemoteFileUrl['transformationless_url']); - $result["file"] = $this->mediaGalleryProcessor->addImage( - $product, - $result["tmp_name"], - $roles, - true, - false - ); - - $mediaGalleryData = $product->getMediaGallery(); - $galItem = array_pop($mediaGalleryData["images"]); - - if ($this->parsedRemoteFileUrl["type"] === "video") { - $videoData = (array) $this->jsonHelper->jsonDecode($this->cloudinaryResourcesManagement->setId($this->parsedRemoteFileUrl["publicId"])->getVideo()); - $videoData["title"] = $label; - $videoData["description"] = ""; - if (!$videoData["error"]) { - $videoData["context"] = new DataObject((isset($videoData["data"]["context"])) ? (array)$videoData["data"]["context"] : []); - $videoData["title"] = $videoData["title"] ? $videoData["title"] : ($videoData["context"]->getData('caption') ?: $videoData["context"]->getData('alt')); - $videoData["description"] = $videoData["context"]->getData('description') ?: $videoData["context"]->getData('alt'); - } - $videoData["title"] = $videoData["title"] ?: $this->parsedRemoteFileUrl["publicId"]; - $videoData["description"] = preg_replace('/( |<([^>]+)>)/i', '', $videoData["description"] ?: $videoData["title"]); - - $galItem = array_merge($galItem, [ - "media_type" => "external-video", - "video_provider" => "cloudinary", - "disabled" => $disabled ? 1 : 0, - "label" => $videoData["title"], - "video_url" => $this->parsedRemoteFileUrl["orig_url"], - "video_title" => $videoData["title"], - "video_description" => $videoData["description"], + if (!$sku) { + throw new LocalizedException( + __("The `sku` field is mandatory.") + ); + } + if ($this->configuration->isEnabledProductgalleryApiQueue()) { + $fullItemData = $this->jsonHelper->jsonEncode([ + "url" => $url, + "sku" => $sku, + "publicId" => $publicId, + "roles" => $roles, + "label" => $label, + "disabled" => $disabled, + "cldspinset" => $cldspinset ]); + return $this->productGalleryApiQueueFactory->create() + ->setSku($sku) + ->setFullItemData($fullItemData) + ->save(); + } else { + return $this->addGalleryItem($url, $sku, $publicId, $roles, $label, $disabled, $cldspinset); } + } + /** + * @method addGalleryItem + * @param string $url + * @param string $sku + * @param string|null $publicId + * @param string|null $roles + * @param string|null $label + * @param bool|int|null $disabled + * @param string $cldspinset + * @return $this + */ + public function addGalleryItem($url, $sku, $publicId = null, $roles = null, $label = null, $disabled = 0, $cldspinset = null) + { + try { + $this->emulateAdminhtmlArea(); + + $this->cldUniqid = $this->mapped = null; - if ($this->parsedRemoteFileUrl["type"] === "image") { - if (!$label) { - $imageData = (array) $this->jsonHelper->jsonDecode($this->cloudinaryResourcesManagement->setId($this->parsedRemoteFileUrl["publicId"])->getImage()); - if (!$imageData["error"]) { - $imageData["context"] = new DataObject((isset($imageData["data"]["context"])) ? (array)$imageData["data"]["context"] : []); - $label = $imageData["context"]->getData('caption') ?: $imageData["context"]->getData('alt'); + if ($cldspinset) { + $imageData = (array) $this->jsonHelper->jsonDecode($this->cloudinaryResourcesManagement->setId($cldspinset)->setMaxResults(1)->getResourcesByTag()); + if (!$imageData || $imageData["error"] || !$imageData["data"] || !$imageData["data"][0] || $imageData["data"][0]["resource_type"] !== "image") { + throw new LocalizedException( + __("No spin set exists for the given tag. Ensure you have uploaded it to Cloudinary correctly, or try again with a different tag name.") + ); + } else { + $imageData["data"] = (array) $imageData["data"][0]; + $url = $url ?: $imageData["data"]["secure_url"]; } - $label = $label ?: ""; } - $galItem = array_merge($galItem, [ - "disabled" => $disabled ? 1 : 0, - "label" => $label, - ]); - } + if (!$url) { + throw new LocalizedException( + __("The `url` field is mandatory.") + ); + } - $mediaGalleryData["images"][] = $galItem; - $product->setData('media_gallery', $mediaGalleryData); + $this->parsedRemoteFileUrl = $this->configuration->parseCloudinaryUrl($url, $publicId); - $product->save(); - $mediaGalleryData = $product->getMediaGallery(); - $galItem = array_pop($mediaGalleryData["images"]); + if (!$this->parsedRemoteFileUrl["version"] && !$publicId) { + throw new LocalizedException( + __("The `publicId` field is mandatory for Cloudinary URLs that doesn't contain a version number.") + ); + } - /*foreach ($this->productImageFinder->findNewImages($product) as $image) { - $this->cloudinaryImageManager->uploadAndSynchronise($image); - }*/ + $roles = ($roles) ? array_map('trim', (is_string($roles) ? explode(',', $roles) : (array) $roles)) : null; + $product = $this->productRepository->get($sku); - if ($this->parsedRemoteFileUrl["type"] === "image" && $this->parsedRemoteFileUrl['transformations_string']) { - $this->transformationFactory->create() - ->setImageName($galItem["file"]) - ->setFreeTransformation($this->parsedRemoteFileUrl['transformations_string']) - ->save(); - } + $result = $this->retrieveImage($this->parsedRemoteFileUrl['thumbnail_url'] ?: $this->parsedRemoteFileUrl['transformationless_url']); + $result["file"] = $this->mediaGalleryProcessor->addImage( + $product, + $result["tmp_name"], + $roles, + true, + false + ); + + $mediaGalleryData = $product->getMediaGallery(); + $galItem = array_pop($mediaGalleryData["images"]); + + if ($this->parsedRemoteFileUrl["type"] === "video") { + $videoData = (array) $this->jsonHelper->jsonDecode($this->cloudinaryResourcesManagement->setId($this->parsedRemoteFileUrl["publicId"])->getVideo()); + $videoData["title"] = $label; + $videoData["description"] = ""; + if (!$videoData["error"]) { + $videoData["context"] = new DataObject((isset($videoData["data"]["context"])) ? (array)$videoData["data"]["context"] : []); + $videoData["title"] = $videoData["title"] ? $videoData["title"] : ($videoData["context"]->getData('caption') ?: $videoData["context"]->getData('alt')); + $videoData["description"] = $videoData["context"]->getData('description') ?: $videoData["context"]->getData('alt'); + } + $videoData["title"] = $videoData["title"] ?: $this->parsedRemoteFileUrl["publicId"]; + $videoData["description"] = preg_replace('/( |<([^>]+)>)/i', '', $videoData["description"] ?: $videoData["title"]); + + $galItem = array_merge($galItem, [ + "media_type" => "external-video", + "video_provider" => "cloudinary", + "disabled" => $disabled ? 1 : 0, + "label" => $videoData["title"], + "video_url" => $this->parsedRemoteFileUrl["orig_url"], + "video_title" => $videoData["title"], + "video_description" => $videoData["description"], + ]); + } - if ($this->configuration->isEnabledLocalMapping()) { - $this->saveCloudinaryMapping(); + if ($this->parsedRemoteFileUrl["type"] === "image") { + if (!$label) { + $imageData = $imageData ?: (array) $this->jsonHelper->jsonDecode($this->cloudinaryResourcesManagement->setId($this->parsedRemoteFileUrl["publicId"])->getImage()); + if (!$imageData["error"]) { + $imageData["context"] = new DataObject((isset($imageData["data"]["context"])) ? (array)$imageData["data"]["context"] : []); + $label = $imageData["context"]->getData('caption') ?: $imageData["context"]->getData('alt'); + } + $label = $label ?: ""; + } + $galItem = array_merge($galItem, [ + "disabled" => $disabled ? 1 : 0, + "label" => $label, + "cldspinset" => $cldspinset, + ]); + } + + $mediaGalleryData["images"][] = $galItem; + $product->setData('media_gallery', $mediaGalleryData); + + $product->save(); + $mediaGalleryData = $product->getMediaGallery(); + $galItem = array_pop($mediaGalleryData["images"]); + + if ($this->parsedRemoteFileUrl["type"] === "image" && $this->parsedRemoteFileUrl['transformations_string']) { + $this->transformationFactory->create() + ->setImageName($galItem["file"]) + ->setFreeTransformation($this->parsedRemoteFileUrl['transformations_string']) + ->save(); + } + + if ($this->parsedRemoteFileUrl["type"] === "image" && $cldspinset) { + $this->resourceConnection->getConnection() + ->insertOnDuplicate($this->resourceConnection->getTableName('cloudinary_product_spinset_map'), [ + 'image_name' => $galItem['file'], + 'cldspinset' => $cldspinset + ], ['image_name', 'cldspinset']); + } + + if ($this->configuration->isEnabledLocalMapping()) { + $this->saveCloudinaryMapping(); + } + } catch (\Exception $e) { + $this->stopEnvironmentEmulation(); + throw $e; } + + return $this; } /** diff --git a/Model/Api/ResourcesManagement.php b/Model/Api/ResourcesManagement.php index b6801ae..88233eb 100644 --- a/Model/Api/ResourcesManagement.php +++ b/Model/Api/ResourcesManagement.php @@ -13,6 +13,7 @@ class ResourcesManagement implements \Cloudinary\Cloudinary\Api\ResourcesManagem { private $initialized; private $id; + private $maxResults; protected $_resourceType = "image"; protected $_resourceData = []; @@ -68,8 +69,11 @@ private function initialize() { if (!$this->initialized) { $this->initialized = true; - if ($this->_request->getParam("id")) { - $this->setId($this->_request->getParam("id")); + if (($id = $this->_request->getParam("id"))) { + $this->setId($id); + } + if (($maxResults = $this->_request->getParam("max_results"))) { + $this->setMaxResults($maxResults); } if ($this->_configuration->isEnabled()) { Cloudinary::config($this->_configurationBuilder->build()); @@ -90,6 +94,17 @@ public function getId() return $this->id; } + public function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + return $this; + } + + public function getMaxResults() + { + return $this->maxResults; + } + /** * Get details of a single resource * @@ -99,6 +114,7 @@ public function getId() protected function _getResourceData() { try { + $this->initialize(); $this->_resourceData = $this->_api->resource( $this->getId(), [ @@ -126,7 +142,6 @@ protected function _getResourceData() */ public function getImage() { - $this->initialize(); $this->_resourceType = "image"; return $this->_getResourceData(); } @@ -136,8 +151,37 @@ public function getImage() */ public function getVideo() { - $this->initialize(); $this->_resourceType = "video"; return $this->_getResourceData(); } + + /** + * {@inheritdoc} + */ + public function getResourcesByTag() + { + try { + $this->initialize(); + $resources = $this->_api->resources_by_tag( + $this->getId(), + [ + "resource_type" => $this->_resourceType, + "max_results" => (int) $this->maxResults || null + ] + )['resources']; + return $this->_jsonEncoder->encode( + [ + "error" => 0, + "data" => $resources + ] + ); + } catch (\Exception $e) { + return $this->_jsonEncoder->encode( + [ + "error" => 1, + "message" => $e->getMessage() + ] + ); + } + } } diff --git a/Model/BatchDownloader.php b/Model/BatchDownloader.php index 8f205b5..dfd260e 100644 --- a/Model/BatchDownloader.php +++ b/Model/BatchDownloader.php @@ -12,7 +12,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\File\Uploader; +use Cloudinary\Cloudinary\Model\Framework\File\Uploader; use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\Curl; use Magento\Framework\Validator\AllowedProtocols; diff --git a/Model/Configuration.php b/Model/Configuration.php index e2fa854..0ca75b1 100644 --- a/Model/Configuration.php +++ b/Model/Configuration.php @@ -15,6 +15,7 @@ use Cloudinary\Cloudinary\Core\Image\Transformation\Quality; use Cloudinary\Cloudinary\Core\Security\CloudinaryEnvironmentVariable; use Cloudinary\Cloudinary\Core\UploadConfig; +use Cloudinary\Cloudinary\Model\Logger as CloudinaryLogger; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Config\Storage\WriterInterface; use Magento\Framework\App\Filesystem\DirectoryList; @@ -56,6 +57,9 @@ class Configuration implements ConfigurationInterface const CONFIG_PATH_USE_SIGNED_URLS = 'cloudinary/advanced/use_signed_urls'; const CONFIG_PATH_ENABLE_LOCAL_MAPPING = 'cloudinary/advanced/enable_local_mapping'; const CONFIG_PATH_SCHEDULED_VIDEO_DATA_IMPORT_LIMIT = 'cloudinary/advanced/cloudinary_scheduled_video_data_import_limit'; + const CONFIG_PATH_PG_API_QUEUE_ENABLED = 'cloudinary/advanced/product_gallery_api_queue_enabled'; + const CONFIG_PATH_PG_API_QUEUE_LIMIT = 'cloudinary/advanced/product_gallery_api_queue_limit'; + const CONFIG_PATH_PG_API_QUEUE_MAX_TRYOUTS = 'cloudinary/advanced/product_gallery_api_queue_max_tryouts'; //= Product Gallery const CONFIG_PATH_PG_ALL = 'cloudinary/product_gallery'; @@ -144,6 +148,11 @@ class Configuration implements ConfigurationInterface */ private $productMetadata; + /** + * @var CloudinaryLogger + */ + private $cloudinaryLogger; + /** * @method __construct * @param ScopeConfigInterface $configReader @@ -154,6 +163,7 @@ class Configuration implements ConfigurationInterface * @param StoreManagerInterface $storeManager * @param ModuleListInterface $moduleList * @param ProductMetadataInterface $productMetadata + * @param CloudinaryLogger $cloudinaryLogger */ public function __construct( ScopeConfigInterface $configReader, @@ -163,7 +173,8 @@ public function __construct( LoggerInterface $logger, StoreManagerInterface $storeManager, ModuleListInterface $moduleList, - ProductMetadataInterface $productMetadata + ProductMetadataInterface $productMetadata, + CloudinaryLogger $cloudinaryLogger ) { $this->configReader = $configReader; $this->configWriter = $configWriter; @@ -173,6 +184,7 @@ public function __construct( $this->storeManager = $storeManager; $this->moduleList = $moduleList; $this->productMetadata = $productMetadata; + $this->cloudinaryLogger = $cloudinaryLogger; } /** @@ -461,6 +473,41 @@ public function getScheduledVideoDataImportLimit() return (int) $this->configReader->getValue(self::CONFIG_PATH_SCHEDULED_VIDEO_DATA_IMPORT_LIMIT); } + /** + * @return bool + */ + public function isEnabledProductgalleryApiQueue() + { + return (bool) $this->configReader->getValue(self::CONFIG_PATH_PG_API_QUEUE_ENABLED); + } + + /** + * @return bool + */ + public function getProductgalleryApiQueueLimit() + { + $return = (int) $this->configReader->getValue(self::CONFIG_PATH_PG_API_QUEUE_LIMIT); + if ($return < 0) { + return 0; + } + return $return; + } + + /** + * @return bool + */ + public function getProductgalleryApiQueueMaxTryouts() + { + $return = (int) $this->configReader->getValue(self::CONFIG_PATH_PG_API_QUEUE_MAX_TRYOUTS); + if ($return > 20) { + return 20; + } + if ($return < 1) { + return 5; + } + return $return; + } + /** * @method getMediaBaseUrl * @return string @@ -560,7 +607,7 @@ public function parseCloudinaryUrl($url, $publicId = null) if ($parsed["type"] === "video") { $parsed["thumbnail_url"] = preg_replace('/\.[^.]+$/', '', $url); $parsed["thumbnail_url"] = preg_replace('/\/v[0-9]{1,10}\//', '/', $parsed["thumbnail_url"]); - $parsed["thumbnail_url"] = preg_replace('/\/(' . $parsed["publicId"] . ')$/', '/so_auto/$1.jpg', $parsed["thumbnail_url"]); + $parsed["thumbnail_url"] = preg_replace('/\/(' . \preg_quote($parsed["publicId"], '/') . ')$/', '/so_auto/$1.jpg', $parsed["thumbnail_url"]); } return $parsed; @@ -604,4 +651,17 @@ public function addUniquePrefixToBasename($filename, $uniqid = null) $uniqid = $uniqid ? $uniqid : $this->generateCLDuniqid(); return dirname($filename) . '/' . $uniqid . basename($filename); } + + /** + * Log to var/log/cloudinary_cloudinary.log + * @method log + * @param mixed $message + * @param array $data + * @return $this + */ + public function log($message, $data = [], $prefix = '[Cloudinary Log] ') + { + $this->cloudinaryLogger->info($prefix . json_encode($message), $data); + return $this; + } } diff --git a/Model/Framework/File/Uploader.php b/Model/Framework/File/Uploader.php new file mode 100644 index 0000000..6c88366 --- /dev/null +++ b/Model/Framework/File/Uploader.php @@ -0,0 +1,45 @@ + 180) { + throw new \InvalidArgumentException('Filename is too long; must be 90 characters or less'); + } + + if (preg_match('/^_+$/', $fileInfo['filename'])) { + $fileName = 'file.' . $fileInfo['extension']; + } + + return $fileName; + } +} diff --git a/Model/Logger.php b/Model/Logger.php new file mode 100644 index 0000000..26ad638 --- /dev/null +++ b/Model/Logger.php @@ -0,0 +1,6 @@ +helper = $helper; $this->transformationFactory = $transformationFactory; + $this->resourceConnection = $resourceConnection; } /** @@ -42,6 +54,15 @@ public function execute(Observer $observer) $product->getCloudinaryFreeTransformChanges() ); + foreach ($mediaGalleryImages as $gallItemId => $gallItem) { + if (isset($gallItem['cldspinset']) && $gallItem['media_type'] === 'image') { + $this->resourceConnection->getConnection() + ->insertOnDuplicate($this->resourceConnection->getTableName('cloudinary_product_spinset_map'), [ + 'image_name' => $gallItem['file'], + 'cldspinset' => $gallItem['cldspinset'] + ], ['image_name', 'cldspinset']); + } + } foreach ($changedTransforms as $id => $transform) { $this->storeFreeTransformation($this->helper->getImageNameForId($id, $mediaGalleryImages), $transform); } diff --git a/Model/ProductGalleryApiQueue.php b/Model/ProductGalleryApiQueue.php new file mode 100644 index 0000000..ec9f4d8 --- /dev/null +++ b/Model/ProductGalleryApiQueue.php @@ -0,0 +1,11 @@ +_init(\Cloudinary\Cloudinary\Model\ResourceModel\ProductGalleryApiQueue::class); + } +} diff --git a/Model/ProductSpinsetMap.php b/Model/ProductSpinsetMap.php new file mode 100644 index 0000000..439dd94 --- /dev/null +++ b/Model/ProductSpinsetMap.php @@ -0,0 +1,11 @@ +_init(\Cloudinary\Cloudinary\Model\ResourceModel\ProductSpinsetMap::class); + } +} diff --git a/Model/ResourceModel/ProductGalleryApiQueue.php b/Model/ResourceModel/ProductGalleryApiQueue.php new file mode 100644 index 0000000..70d3230 --- /dev/null +++ b/Model/ResourceModel/ProductGalleryApiQueue.php @@ -0,0 +1,16 @@ +_init('cloudinary_product_gallery_api_queue', 'id'); + } +} diff --git a/Model/ResourceModel/ProductGalleryApiQueue/Collection.php b/Model/ResourceModel/ProductGalleryApiQueue/Collection.php new file mode 100644 index 0000000..ab916fc --- /dev/null +++ b/Model/ResourceModel/ProductGalleryApiQueue/Collection.php @@ -0,0 +1,17 @@ +_init( + \Cloudinary\Cloudinary\Model\ProductGalleryApiQueue::class, + \Cloudinary\Cloudinary\Model\ResourceModel\ProductGalleryApiQueue::class + ); + } +} diff --git a/Model/ResourceModel/ProductSpinsetMap.php b/Model/ResourceModel/ProductSpinsetMap.php new file mode 100644 index 0000000..d89b82d --- /dev/null +++ b/Model/ResourceModel/ProductSpinsetMap.php @@ -0,0 +1,16 @@ +_init('cloudinary_product_spinset_map', 'id'); + } +} diff --git a/Model/ResourceModel/ProductSpinsetMap/Collection.php b/Model/ResourceModel/ProductSpinsetMap/Collection.php new file mode 100644 index 0000000..81e0a29 --- /dev/null +++ b/Model/ResourceModel/ProductSpinsetMap/Collection.php @@ -0,0 +1,17 @@ +_init( + \Cloudinary\Cloudinary\Model\ProductSpinsetMap::class, + \Cloudinary\Cloudinary\Model\ResourceModel\ProductSpinsetMap::class + ); + } +} diff --git a/Plugin/Catalog/Block/Product/View/Gallery.php b/Plugin/Catalog/Block/Product/View/Gallery.php index 0187229..626d4f9 100644 --- a/Plugin/Catalog/Block/Product/View/Gallery.php +++ b/Plugin/Catalog/Block/Product/View/Gallery.php @@ -4,6 +4,8 @@ use Cloudinary\Cloudinary\Core\ConfigurationInterface; use Cloudinary\Cloudinary\Helper\ProductGalleryHelper; +use Cloudinary\Cloudinary\Model\ProductSpinsetMapFactory; +use Magento\Framework\DataObject; use Magento\Framework\Json\EncoderInterface; class Gallery @@ -23,6 +25,11 @@ class Gallery */ private $configuration; + /** + * @var ProductSpinsetMapFactory + */ + protected $productSpinsetMapFactory; + /** * @var \Magento\Catalog\Block\Product\View\Gallery */ @@ -46,11 +53,13 @@ class Gallery public function __construct( ProductGalleryHelper $productGalleryHelper, EncoderInterface $jsonEncoder, - ConfigurationInterface $configuration + ConfigurationInterface $configuration, + ProductSpinsetMapFactory $productSpinsetMapFactory ) { $this->productGalleryHelper = $productGalleryHelper; $this->jsonEncoder = $jsonEncoder; $this->configuration = $configuration; + $this->productSpinsetMapFactory = $productSpinsetMapFactory; } /** @@ -83,6 +92,43 @@ public function getCldPGid() return 'cldPGid_' . $this->getHtmlId(); } + /** + * Retrieve product images in JSON format + * + * @return string + */ + protected function getGalleryImagesJson() + { + $imagesItems = []; + /** @var DataObject $image */ + foreach ($this->productGalleryBlock->getGalleryImages() as $image) { + $imageItem = new DataObject( + [ + 'file' => $image->getData('file'), + 'thumb' => $image->getData('small_image_url'), + 'img' => $image->getData('medium_image_url'), + 'full' => $image->getData('large_image_url'), + 'caption' => ($image->getLabel() ?: $this->productGalleryBlock->getProduct()->getName()), + 'position' => $image->getData('position'), + 'isMain' => $this->productGalleryBlock->isMainImage($image), + 'type' => str_replace('external-', '', $image->getMediaType()), + 'videoUrl' => $image->getVideoUrl(), + ] + ); + foreach ($this->productGalleryBlock->getGalleryImagesConfig()->getItems() as $imageConfig) { + $imageItem->setData( + $imageConfig->getData('json_object_key'), + $image->getData($imageConfig->getData('data_object_key')) + ); + } + $imagesItems[] = $imageItem->toArray(); + } + if (empty($imagesItems)) { + return $this->productGalleryBlock->getGalleryImagesJson(); + } + return $this->jsonEncoder->encode($imagesItems); + } + /** * @method getCloudinaryPGOptions * @param bool $refresh Refresh options @@ -94,7 +140,7 @@ protected function getCloudinaryPGOptions($refresh = false, $ignoreDisabled = fa if (is_null($this->cloudinaryPGoptions) || $refresh) { $this->cloudinaryPGoptions = $this->productGalleryHelper->getCloudinaryPGOptions($refresh, $ignoreDisabled); $this->cloudinaryPGoptions['container'] = '#' . $this->getCldPGid(); - $galleryAssets = (array) json_decode($this->productGalleryBlock->getGalleryImagesJson(), true); + $galleryAssets = (array) json_decode($this->getGalleryImagesJson(), true); if (count($galleryAssets)>1) { usort($galleryAssets, function ($a, $b) { return $a['position'] - $b['position']; @@ -107,13 +153,23 @@ protected function getCloudinaryPGOptions($refresh = false, $ignoreDisabled = fa foreach ($galleryAssets as $key => $value) { $publicId = $url = $transformation = null; if ($value['type'] === 'image') { + //Check if image is a spinset: + $cldspinset = $this->productSpinsetMapFactory->create()->getCollection()->addFieldToFilter("image_name", $value['file'])->setPageSize(1)->getFirstItem(); + if ($cldspinset && ($cldspinset = $cldspinset->getCldspinset())) { + $this->cloudinaryPGoptions['mediaAssets'][] = (object)[ + "tag" => $cldspinset, + "mediaType" => 'spin' + ]; + continue; + } + //==================================// $url = $value['full'] ?: $value['img']; } elseif ($value['type'] === 'video') { $url = $value['videoUrl']; } - if (\strpos($url, '.cloudinary.com/') !== false && strpos($url, '/' . $this->productGalleryHelper->getCloudName() . '/') !== false) { + if (\strpos($url, '.cloudinary.com/') !== false && (strpos($url, '/' . $this->productGalleryHelper->getCloudName() . '/') !== false || strpos($url, '://' . $this->productGalleryHelper->getCloudName()) !== false)) { $parsed = $this->configuration->parseCloudinaryUrl($url); - $publicId = $parsed['publicId'] . '.' . $parsed['extension']; + $publicId = ($value['type'] === 'image') ? $parsed['publicId'] . '.' . $parsed['extension'] : $parsed['publicId']; $transformation = \str_replace('/', ',', $parsed['transformations_string']); } if ($publicId) { diff --git a/Plugin/FileUploader.php b/Plugin/FileUploader.php index c73ef86..e36ef85 100644 --- a/Plugin/FileUploader.php +++ b/Plugin/FileUploader.php @@ -93,7 +93,7 @@ protected function absoluteFilePath(array $result) */ protected function mediaRelativePath($filepath) { - $mediaPath = $this->directoryList->getPath('media') . DIRECTORY_SEPARATOR; - return (strpos($filepath, $mediaPath) === 0) ? str_replace($mediaPath, '', $filepath) : $filepath; + $pubPath = $this->directoryList->getPath(DirectoryList::PUB) . DIRECTORY_SEPARATOR; + return (strpos($filepath, $pubPath) === 0) ? str_replace($pubPath, '', $filepath) : $filepath; } } diff --git a/Setup/InstallSchema.php b/Setup/InstallSchema.php index 4763ec1..03482ef 100644 --- a/Setup/InstallSchema.php +++ b/Setup/InstallSchema.php @@ -2,10 +2,10 @@ namespace Cloudinary\Cloudinary\Setup; +use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\InstallSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; -use Magento\Framework\DB\Ddl\Table; class InstallSchema implements InstallSchemaInterface { diff --git a/Setup/UpgradeSchema.php b/Setup/UpgradeSchema.php index 7697443..06f2b73 100644 --- a/Setup/UpgradeSchema.php +++ b/Setup/UpgradeSchema.php @@ -25,6 +25,14 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $this->createMediaLibraryMapTable($setup); } + if (version_compare($context->getVersion(), '1.12.0', '<')) { + $this->createProductGalleryApiQueueTable($setup); + } + + if (version_compare($context->getVersion(), '1.13.0', '<')) { + $this->createProductSpinsetMapTable($setup); + } + $setup->endSetup(); } @@ -95,4 +103,127 @@ private function createMediaLibraryMapTable(SchemaSetupInterface $setup) $setup->getConnection()->createTable($table); } + + /** + * + * @param SchemaSetupInterface $setup + */ + private function createProductGalleryApiQueueTable(SchemaSetupInterface $setup) + { + $table = $setup->getConnection()->newTable( + $setup->getTable('cloudinary_product_gallery_api_queue') + )->addColumn( + 'id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'ID' + )->addColumn( + 'sku', + Table::TYPE_TEXT, + 255, + ['nullable' => false], + 'Product SKU' + )->addColumn( + 'full_item_data', + \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 3000, + ['nullable' => true], + 'Prepared Schema' + ) + ->addColumn( + 'created_at', + \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT], + 'Created At' + ) + ->addColumn( + 'updated_at', + \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE], + 'Created At' + ) + ->addColumn( + 'success', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + 1, + ['unsigned' => true, 'nullable' => false, 'default' => '0'], + 'Success' + ) + ->addColumn( + 'success_at', + \Magento\Framework\DB\Ddl\Table::TYPE_DATETIME, + null, + ['nullable' => true], + 'Success At' + ) + ->addColumn( + 'message', + \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 3000, + ['nullable' => true], + 'Message' + ) + ->addColumn( + 'has_errors', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + 1, + ['unsigned' => true, 'nullable' => true, 'default' => '0'], + 'Has Errors' + ) + ->addColumn( + 'tryouts', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + 11, + ['unsigned' => true, 'nullable' => true, 'default' => '0'], + 'Tryouts' + ); + $setup->getConnection()->createTable($table); + } + + /** + * @param SchemaSetupInterface $setup + */ + private function createProductSpinsetMapTable(SchemaSetupInterface $setup) + { + $table = $setup->getConnection()->newTable( + $setup->getTable('cloudinary_product_spinset_map') + )->addColumn( + 'id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'ID' + )->addColumn( + 'store_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false, 'default' => '0'], + 'Store ID' + )->addColumn( + 'image_name', + Table::TYPE_TEXT, + 255, + ['nullable' => false], + 'Relative image path' + )->addColumn( + 'cldspinset', + Table::TYPE_TEXT, + 255, + [], + 'Cloudinary Spinset Tag' + )->addIndex( + $setup->getIdxName( + 'cloudinary_product_spinset_map', + ['store_id', 'image_name'], + \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE + ), + ['store_id', 'image_name'], + ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE] + ); + + $setup->getConnection()->createTable($table); + } } diff --git a/composer.json b/composer.json index 76eb03b..0ef75df 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "cloudinary/cloudinary-magento2", "description": "Cloudinary Magento 2 Integration.", "type": "magento2-module", - "version": "1.11.3", + "version": "1.13.0", "license": "MIT", "require": { "cloudinary/cloudinary_php": "*" diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 32486b8..2938275 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -10,10 +10,10 @@ cloudinary Cloudinary_Cloudinary::config_cloudinary - Enable module + Enable Extension 1 - Enable cloudinary + Enable Cloudinary Magento\Config\Model\Config\Source\Yesno @@ -22,23 +22,23 @@ - Cloudinary Setup + Cloudinary Account 1 Cloudinary Account Credentials - Format should be: cloudinary://API_Key:API_Secret@Cloud_Name]]> + Cloudinary's Management Console.Format should be: cloudinary://API_Key:API_Secret@Cloud_Name]]> Cloudinary\Cloudinary\Model\Config\Backend\Credentials - Automatic login - ]]> + Automatic Login + ]]> validate-email - Cloudinary Account Credentials + Cloudinary Setup 1 Image Delivery Domain Sharding @@ -46,8 +46,8 @@ Magento\Config\Model\Config\Source\Yesno - Use auto upload mapping to upload images - Automatically upload images from your site to your Cloudinary account (if they don’t already exist). + Enable auto-upload + Automatically upload your existing Magento images to your Cloudinary account when an image is requested by a user. Magento\Config\Model\Config\Source\Yesno @@ -66,7 +66,7 @@ Image Cropping Gravity - Define the part of the image to focus on when cropping images. + documentation.]]> Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Gravity @@ -75,8 +75,8 @@ Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Dpr - Global custom transformation - Add global custom transformations in addition to those selected above. See the Cloudinary documentation for the full range of transformations. You may need to clear or rebuild the Magento block and page caches to see the changes in the front end. + Global Custom Transformation + Custom transformations will be added to the default image transformations settings chosen above.For information about the full range of transforms available see the Cloudinary documentation.You may need to clear or rebuild the Magento block and full page caches to see the changes in the front end.]]> Cloudinary\Cloudinary\Model\Config\Backend\Free Cloudinary\Cloudinary\Block\Adminhtml\Form\Field\Free @@ -84,15 +84,18 @@ Product Gallery 0 - - Enable Cloudinary's product gallery - Enable Cloudinary's product gallery (override Magento's default) + + Enable Cloudinary's Product Gallery + Product Gallery documentation for information on the configuration options.]]> Magento\Config\Model\Config\Source\Yesno cloudinary/product_gallery/enabled - Theme colors + Theme Colors 1 + + 1 + Primary cloudinary/product_gallery/themeProps_primary @@ -119,15 +122,18 @@ - Main viewer parameters + Main Viewer Parameters 1 + + 1 + - Fade transition + Fade Transition cloudinary/product_gallery/transition Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\Transition - Aspect ratio + Aspect Ratio cloudinary/product_gallery/aspectRatio Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\AspectRatio @@ -137,12 +143,12 @@ Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\Navigation - Show zoom + Show Zoom cloudinary/product_gallery/zoom Magento\Config\Model\Config\Source\Yesno - Zoom type + Zoom Type cloudinary/product_gallery/zoomProps_type Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ZoomType @@ -150,7 +156,7 @@ - Zoom trigger + Zoom Trigger cloudinary/product_gallery/zoomProps_viewerPosition Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ZoomViewerPosition @@ -159,7 +165,7 @@ - Zoom viewer position + Zoom Viewer Position cloudinary/product_gallery/zoomProps_trigger Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ZoomTrigger @@ -168,8 +174,11 @@ - Carousel parameters + Carousel Parameters 1 + + 1 + Carousel Location cloudinary/product_gallery/carouselLocation @@ -186,7 +195,7 @@ Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\CarouselStyle - Thumbnail width + Thumbnail Width cloudinary/product_gallery/thumbnailProps_width validate-number @@ -194,7 +203,7 @@ - Thumbnail height + Thumbnail Height cloudinary/product_gallery/thumbnailProps_height validate-number @@ -202,7 +211,7 @@ - Navigation button shape + Navigation Button Shape cloudinary/product_gallery/thumbnailProps_navigationShape Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ThumbnailsNavigationShape @@ -210,7 +219,7 @@ - Thumbnail selected style + Thumbnail Selected Style cloudinary/product_gallery/thumbnailProps_selectedStyle Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ThumbnailsSelectedStyle @@ -218,7 +227,7 @@ - Thumbnail selected border position + Thumbnail Selected Border Position cloudinary/product_gallery/thumbnailProps_selectedBorderPosition Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ThumbnailsSelectedBorderPosition @@ -226,7 +235,7 @@ - Thumbnail selected border width + Thumbnail Selected Border Width cloudinary/product_gallery/thumbnailProps_selectedBorderWidth validate-number @@ -234,7 +243,7 @@ - Thumbnail media icon shape + Thumbnail Media Icon Shape cloudinary/product_gallery/thumbnailProps_mediaSymbolShape Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\ThumbnailsMediaSymbolShape @@ -242,7 +251,7 @@ - Indicators shape + Indicators Shape cloudinary/product_gallery/indicatorProps_shape Cloudinary\Cloudinary\Model\Config\Source\Dropdown\ProductGallery\IndicatorsShape @@ -251,12 +260,15 @@ - Custom free parameters (advanced) + Custom Free Parameters (advanced) - e.g., {"zoom": true, "thumbnailProps": {"borderColor": "#EBF0F4"}}]]> + See Product Gallery reference for full list of parameters.e.g., {"zoom": true, "thumbnailProps": {"borderColor": "#EBF0F4"}}]]> cloudinary/product_gallery/custom_free_params Cloudinary\Cloudinary\Model\Config\Backend\ProductGalleryCustomFreeParams + + 1 + @@ -264,13 +276,13 @@ 0 Enable Lazyload - Lazy Load Plugin for jQuery)]]> + Lazy Load Plugin for jQuery).]]> cloudinary/lazyload/enabled Magento\Config\Model\Config\Source\Yesno - Apply on CMS blocks - Automatically apply lazyload on CMS blocks + Apply on CMS Blocks + Automatically apply lazyload on CMS blocks. cloudinary/lazyload/auto_replace_cms_blocks Magento\Config\Model\Config\Source\Yesno @@ -278,8 +290,8 @@ - Apply on CMS blocks - Ignored List - When applying on CMS blocks, skip those (use this if there are conflicts on some of the blocks) + CMS Blocks to Exclude + List of CMS blocks to exclude from Lazyloading. Use this if there are conflicts on some of the blocks. cloudinary/lazyload/ignored_cms_blocks Cloudinary\Cloudinary\Model\Config\Source\Dropdown\CmsBlocks @@ -289,7 +301,7 @@ Lazyload Threshold - Amount of pixels below the viewport, in which all images gets loaded before the user sees them + The threshold for how many pixels from the top of the page before lazy loading is applied to your images. cloudinary/lazyload/threshold 1 @@ -297,7 +309,7 @@ Lazyload Effect - The effect you want to use to show the loaded images + The effect to use to show the loaded images. cloudinary/lazyload/effect Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Lazyload\Effect @@ -306,7 +318,7 @@ Lazyload Placeholder - Choose the transformation effect that will be used on the images as a loading placeholder + The transformation effect that will be used on the images as a loading placeholder. cloudinary/lazyload/placeholder Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Lazyload\Placeholder @@ -319,24 +331,43 @@ 0 Remove version number from URLs - Remove version number (e.g., ".../v1/...") from URLs + Remove version number (e.g., ".../v1/...") from URLs. Magento\Config\Model\Config\Source\Yesno - Use root path - Remove "/image/upload/" from URLs + Use Root Path + Remove "/image/upload/" from URLs. Magento\Config\Model\Config\Source\Yesno - Use signed URLs + Use Signed URLs Use dynamic Cloudinary delivery URLs with a signature that needs to be validated before making it available to view. Magento\Config\Model\Config\Source\Yesno - Use existing Cloudinary folder - Map Cloudinary assets to their original location to prevent duplication in your Cloudinary Media Library. If set to 'No', assets will be synchronized to a new Cloudinary location based on the local Magento path. + Use Existing Cloudinary Folder + Map existing Cloudinary assets to their original location to prevent duplication in your Cloudinary Media Library. If set to 'No', assets will be synchronized to a new Cloudinary location based on the local Magento path. + Magento\Config\Model\Config\Source\Yesno + + + Process Product Gallery API Requests In The Background (add to queue) + When using the rest/V1/cloudinary/products API endpoint, add the requests to queue and process later using a cronjob. Magento\Config\Model\Config\Source\Yesno + + Product Gallery API Queue - Limit + Maximum queue items to process on each run (0 = no limit). + + 1 + + + + Product Gallery API Queue - Maximum Tryouts + Number of times to try processing the queued item before stopping and adding an error notification on admin (Minimum: 1, Maximum: 20). + + 1 + + diff --git a/etc/config.xml b/etc/config.xml index b03019d..67d7e0d 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -6,7 +6,7 @@ 1 auto - 1.0 + 2.0 1 @@ -23,6 +23,9 @@ 0 1 10 + 1 + 20 + 5 0 @@ -51,5 +54,15 @@ {} + + + + 0 + + + 0 + + + diff --git a/etc/crontab.xml b/etc/crontab.xml index a13acb1..4f50aa5 100644 --- a/etc/crontab.xml +++ b/etc/crontab.xml @@ -3,6 +3,9 @@ * * * * * + + + * * * * * diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml new file mode 100644 index 0000000..77408fc --- /dev/null +++ b/etc/csp_whitelist.xml @@ -0,0 +1,105 @@ + + + + + + cloudinary.com + www.cloudinary.com + api.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + cdnjs.cloudflare.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + + + + + cloudinary.com + www.cloudinary.com + p.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + cdnjs.cloudflare.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + + + + + cloudinary.com + www.cloudinary.com + p.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + cdnjs.cloudflare.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + + + + + cloudinary.com + www.cloudinary.com + p.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + + + + + cloudinary.com + www.cloudinary.com + p.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + data: + + + + + cloudinary.com + www.cloudinary.com + p.cloudinary.com + media-library.cloudinary.com + product-gallery.cloudinary.com + analytics-api.cloudinary.com + res.cloudinary.com + res-1.cloudinary.com + res-2.cloudinary.com + res-3.cloudinary.com + res-4.cloudinary.com + res-5.cloudinary.com + + + + diff --git a/etc/di.xml b/etc/di.xml index 8784694..98fc0e2 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -8,6 +8,7 @@ Cloudinary\Cloudinary\Command\UploadImages Cloudinary\Cloudinary\Command\StopMigration Cloudinary\Cloudinary\Command\ResetAll + Cloudinary\Cloudinary\Command\ProductGalleryApiQueueProcess @@ -94,4 +95,17 @@ + + + Magento\Framework\Filesystem\Driver\File + + + + + cloudinaryLogger + + Cloudinary\Cloudinary\Model\Logger\CloudinaryHandler + + + diff --git a/etc/module.xml b/etc/module.xml index 86c6b13..82b4af4 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + diff --git a/etc/webapi.xml b/etc/webapi.xml index 4046d84..d3b782b 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -19,6 +19,15 @@ + + + + + + + + + diff --git a/marketplace.composer.json b/marketplace.composer.json index 2ba9f63..518fed7 100644 --- a/marketplace.composer.json +++ b/marketplace.composer.json @@ -2,7 +2,7 @@ "name": "cloudinary/cloudinary", "description": "Cloudinary Magento 2 Integration.", "type": "magento2-module", - "version": "1.11.3", + "version": "1.13.0", "license": "MIT", "require": { "cloudinary/cloudinary_php": "*" diff --git a/view/adminhtml/requirejs-config.js b/view/adminhtml/requirejs-config.js index 930fbd6..4a8ca23 100644 --- a/view/adminhtml/requirejs-config.js +++ b/view/adminhtml/requirejs-config.js @@ -4,7 +4,10 @@ var config = { cloudinaryFreeTransform: 'Cloudinary_Cloudinary/js/cloudinary-free', newVideoDialog: 'Cloudinary_Cloudinary/js/new-video-dialog', 'Magento_ProductVideo/js/get-video-information': 'Cloudinary_Cloudinary/js/get-video-information', - cloudinaryMediaLibraryModal: 'Cloudinary_Cloudinary/js/cloudinary-media-library-modal' + cloudinaryMediaLibraryModal: 'Cloudinary_Cloudinary/js/cloudinary-media-library-modal', + cloudinarySpinsetModal: 'Cloudinary_Cloudinary/js/cloudinary-spinset-modal', + cldspinsetDialog: 'Cloudinary_Cloudinary/js/cloudinary-spinset-dialog', + productGallery: 'Cloudinary_Cloudinary/js/product-gallery', } }, paths: { diff --git a/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/view/adminhtml/templates/catalog/product/helper/gallery.phtml index 77b0c3a..16f13cf 100644 --- a/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -23,14 +23,35 @@ $cloudinaryMLwidgetOprions = $block->getCloudinaryMediaLibraryWidgetOptions(); = $block->escapeHtml(__('Add Video')) ?> - - = $block->escapeHtml(__('Add from Cloudinary')) ?> - + + + = $block->escapeHtml(__('Add from Cloudinary')) ?> + + + + = $block->escapeHtml(__('Add Image/Video')) ?> + + + = $block->escapeHtml(__('Add Spinset')) ?> + + + + + @@ -119,6 +140,10 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to name="= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][label]" value="<%- data.label %>" data-form-part="= /* @escapeNotVerified */ $formName ?>"/> + getToggleCode() ? $element->getToggleCode() : 'to value="<%- data.video_description %>" data-form-part="= /* @escapeNotVerified */ $formName ?>"/> - + getToggleCode() ? $element->getToggleCode() : 'to + + + = /* @escapeNotVerified */ __('Cloudinary Spinset Tag') ?> + + + + + + + = $block->escapeHtml(__('Role')) ?> @@ -309,6 +347,42 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to = $block->getChildHtml('new-video') ?> + + + + + + + + = /* @escapeNotVerified */ __('Cloudinary Spinset Tag') ?> + + + + + + + + + + + + = /* @escapeNotVerified */ __('Preview') ?> + + + + + + + + + + + + +