diff --git a/CHANGELOG_en-GB.md b/CHANGELOG_en-GB.md index 4b8e3df..9d3a47a 100644 --- a/CHANGELOG_en-GB.md +++ b/CHANGELOG_en-GB.md @@ -1,3 +1,7 @@ +# 3.0.4 +* Add configuration within the plugin to activate its function explicit +* Add button to test the service with a sample image + # 3.0.3 * Adjust wordings and remove typos diff --git a/README.md b/README.md index 51a56dd..1bd3bdc 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,17 @@ You may want to delete folder `thumbnails` within folder `public` ## Adding more thumbnail sizes: - Save new size in the folder of the media management -- (up to and including plugin version 3.0.1) then run the command `bin/console media:generate-thumbnails` on the console to update the thumbnails for all images in the database +- (no more needed from version 3.0.2) run the command `bin/console media:generate-thumbnails` on the console to update the thumbnails for all images in the database - Clear shop cache ## Find Patterns You can find patterns in [GitHub Discussions in category Patterns](https://github.com/FriendsOfShopware/FroshPlatformThumbnailProcessor/discussions/categories/patterns) +## Uninstall + +After uninstalling plugin you have to run `bin/console media:generate-thumbnails -strict` to generate the thumbnails-files on disk. + ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/composer.json b/composer.json index e7e3b07..9a3be61 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "thumbnail" ], "description": "This plugins allows you to use variable thumbnails, without having them on storage.", - "version": "3.0.3", + "version": "3.0.4", "type": "shopware-platform-plugin", "license": "mit", "authors": [ diff --git a/phpstan.neon b/phpstan.neon index a9c7019..0a91d5a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,7 @@ parameters: message: "#^Call to an undefined method Shopware\\\\Core\\\\Content\\\\Media\\\\Message\\\\GenerateThumbnailsMessage\\:\\:setContext\\(\\)\\.$#" count: 1 path: src/DependencyInjection/FileSaver.php + reportUnmatched: false - message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" diff --git a/src/Controller/Api/TestController.php b/src/Controller/Api/TestController.php new file mode 100644 index 0000000..e825361 --- /dev/null +++ b/src/Controller/Api/TestController.php @@ -0,0 +1,141 @@ + ['api']])] +class TestController +{ + public const REQUEST_ATTRIBUTE_TEST_ACTIVE = 'FroshPlatformThumbnailProcessorTestActive'; + + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityRepository $mediaRepository, + private readonly EntityRepository $mediaFolderRepository, + private readonly FileSaver $fileSaver, + private readonly FileFetcher $fileFetcher + ) { + } + + #[Route(path: '/api/_action/thumbnail-processor-test/get-sample-image')] + public function check(Request $request, RequestDataBag $dataBag): JsonResponse + { + if (!$dataBag->has('salesChannelId')) { + return new JsonResponse(['success' => false]); + } + + $testFile = \realpath(__DIR__ . '/../../Resources/data/froshthumbnailprocessortestimage.jpg'); + + if (!\is_string($testFile) || !\is_file($testFile)) { + throw new \RuntimeException(\sprintf('Test file at "%s" is missing', $testFile)); + } + + $fileContent = \file_get_contents($testFile); + + if (!\is_string($fileContent)) { + throw new \RuntimeException(\sprintf('Test file at "%s" could not be read', $testFile)); + } + + $request->attributes->set(self::REQUEST_ATTRIBUTE_TEST_ACTIVE, '1'); + + $salesChannelId = $dataBag->get('salesChannelId'); + if (\is_string($salesChannelId)) { + $request->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, $salesChannelId); + } + + $media = $this->getSampleMedia($fileContent, $testFile); + + $thumbnail = new MediaThumbnailEntity(); + $thumbnail->setWidth(200); + $thumbnail->setHeight(200); + + return new JsonResponse(['url' => $this->urlGenerator->getAbsoluteThumbnailUrl($media, $thumbnail)]); + } + + private function getProductFolderId(Context $context): string + { + $criteria = (new Criteria()) + ->addFilter(new EqualsFilter('media_folder.defaultFolder.entity', 'product')) + ->addAssociation('defaultFolder') + ->setLimit(1); + + $ids = $this->mediaFolderRepository + ->searchIds($criteria, $context) + ->getIds(); + + if (\is_string($ids[0])) { + return $ids[0]; + } + + throw new \RuntimeException('Media folder for product could not have been found!'); + } + + private function getSampleMedia(string $fileContent, string $testFile): MediaEntity + { + $context = Context::createDefaultContext(); + $mediaId = \hash('xxh128', $fileContent); + + $existingMedia = $this->getMediaById($mediaId, $context); + if ($existingMedia) { + return $existingMedia; + } + + $mediaFolderId = $this->getProductFolderId($context); + + $this->mediaRepository->upsert( + [ + [ + 'id' => $mediaId, + 'mediaFolderId' => $mediaFolderId, + ], + ], + $context + ); + + $pathInfo = pathinfo($testFile); + if (empty($pathInfo['extension'])) { + $pathInfo['extension'] = 'jpg'; + } + + $uploadedFile = $this->fileFetcher->fetchBlob( + $fileContent, + $pathInfo['extension'], + 'image/' . $pathInfo['extension'] + ); + + $this->fileSaver->persistFileToMedia( + $uploadedFile, + $pathInfo['filename'], + $mediaId, + $context + ); + + $existingMedia = $this->getMediaById($mediaId, $context); + if ($existingMedia) { + return $existingMedia; + } + + throw new \RuntimeException('Media has not been saved!'); + } + + private function getMediaById(string $id, Context $context): ?MediaEntity + { + $criteria = new Criteria([$id]); + + return $this->mediaRepository->search($criteria, $context)->getEntities()->first(); + } +} diff --git a/src/Migration/Migration1686772873AddActiveConfig.php b/src/Migration/Migration1686772873AddActiveConfig.php new file mode 100644 index 0000000..86a7e4c --- /dev/null +++ b/src/Migration/Migration1686772873AddActiveConfig.php @@ -0,0 +1,57 @@ +getPluginVersion($connection); + + if (empty($currentPluginVersion)) { + return; + } + + // we added the active flag with version 3.0.3, so we don't need to set the default value afterward + if (\version_compare($currentPluginVersion, '3.0.3', '>')) { + return; + } + + $connection->update( + 'system_config', + [ + 'configuration_value' => '{"_value": true}', + ], + ['configuration_key' => 'FroshPlatformThumbnailProcessor.config.Active'] + ); + } + + public function updateDestructive(Connection $connection): void + { + } + + private function getPluginVersion(Connection $connection): ?string + { + $builder = $connection->createQueryBuilder()->select('`version`') + ->from('plugin') + ->where('`name` = :pluginName') + ->andWhere('`active` = 1') + ->setParameter('pluginName', 'FroshPlatformThumbnailProcessor'); + + $result = $builder->executeQuery()->fetchOne(); + + if (\is_string($result)) { + return $result; + } + + return null; + } +} diff --git a/src/Resources/app/administration/src/main.js b/src/Resources/app/administration/src/main.js index ea42925..bdbb690 100644 --- a/src/Resources/app/administration/src/main.js +++ b/src/Resources/app/administration/src/main.js @@ -1 +1,3 @@ -import './frosh-thumbnail-processor-info-texts'; +import './module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts'; +import './module/frosh-thumbnail-processor/test-button' +import './service/thumbnailProcessorTestService'; diff --git a/src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.html.twig b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.html.twig similarity index 100% rename from src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.html.twig rename to src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.html.twig diff --git a/src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.scss b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.scss similarity index 100% rename from src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.scss rename to src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/frosh-thumbnail-processor-info-texts.scss diff --git a/src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/index.js b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/index.js similarity index 100% rename from src/Resources/app/administration/src/frosh-thumbnail-processor-info-texts/index.js rename to src/Resources/app/administration/src/module/frosh-thumbnail-processor/frosh-thumbnail-processor-info-texts/index.js diff --git a/src/Resources/app/administration/src/module/frosh-thumbnail-processor/snippet/en-GB.json b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/snippet/en-GB.json new file mode 100644 index 0000000..663a68c --- /dev/null +++ b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/snippet/en-GB.json @@ -0,0 +1,12 @@ +{ + "thumbnail-processor": { + "test": { + "title": "Service Test", + "success": "Seems good! Please check the displayed sample image to contain lighten trees view.", + "error": { + "general": "There was an error loading the image. Please check the url and setting.", + "noResize": "The used service does not resize the image." + } + } + } +} diff --git a/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/index.js b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/index.js new file mode 100644 index 0000000..698f02f --- /dev/null +++ b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/index.js @@ -0,0 +1,122 @@ +const {Component, Mixin} = Shopware; +import template from './test-button.html.twig'; +import './style.css'; + +Component.register('thumbnailprocessor-test', { + template, + + props: ['btnLabel'], + inject: ['thumbnailProcessorTest'], + + mixins: [ + Mixin.getByName('notification') + ], + + data() { + return { + isLoading: false, + isSuccessful: false, + }; + }, + + computed: { + pluginSalesChannelId() { + let configData = this.$parent; + for (let i = 0; i < 20; i++) { + if (typeof configData.currentSalesChannelId != "undefined") { + return configData.currentSalesChannelId; + } + + configData = configData.$parent; + } + + throw "Can not get pluginConfigData"; + } + }, + + methods: { + finish() { + this.isSuccessful = false; + }, + + showError(message, sampleUrl) { + this.isSuccessful = false; + + if (sampleUrl) { + message += ' sample url: ' + sampleUrl; + } + + this.createNotificationError({ + title: this.$tc('thumbnail-processor.test.title'), + message: message + }); + }, + + saveAndCheck() { + this.isLoading = true; + this.systemConfigSaveAll(); + }, + + check() { + const me = this; + + me.thumbnailProcessorTest.getUrl(this.pluginSalesChannelId).then((res) => { + if (res.url) { + me.isSuccessful = true; + + const img = document.createElement('img'); + img.width = 200; + img.height = 200; + + img.onload = function() { + if (img.naturalWidth !== 200) { + me.showError(me.$tc('thumbnail-processor.test.error.noResize'), res.url); + } + }; + + img.onerror = function() { + me.showError(me.$tc('thumbnail-processor.test.error.general'), res.url); + }; + + img.src = res.url; + + const testElement = document.querySelector('[name="FroshPlatformThumbnailProcessor.config.test"]'); + const testImage = testElement.querySelector('.frosh-thumbnail-processor-testimage img'); + + if (testImage) { + testImage.replaceWith(img); + } else { + const testImageContainer = document.createElement('p'); + testImageContainer.classList.add('frosh-thumbnail-processor-testimage'); + testImageContainer.appendChild(img); + testElement.appendChild(testImageContainer); + } + } else { + me.showError(me.$tc('thumbnail-processor.test.error.general')); + } + + setTimeout(() => { + this.isLoading = false; + }, 2500); + }); + }, + + systemConfigSaveAll() { + const me = this; + let el = this.$parent; + + for (let i = 0; i < 30; i++) { + if (typeof el.$refs.systemConfig != "undefined") { + return el.$refs.systemConfig.saveAll() + .then(() => { + me.check(); + }) + } + + el = el.$parent; + } + + throw "Can not get systemConfig"; + } + } +}) diff --git a/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/style.css b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/style.css new file mode 100644 index 0000000..8ca5e13 --- /dev/null +++ b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/style.css @@ -0,0 +1,3 @@ +.frosh-thumbnail-processor-testimage { + margin: 20px 0; +} diff --git a/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/test-button.html.twig b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/test-button.html.twig new file mode 100644 index 0000000..fec1682 --- /dev/null +++ b/src/Resources/app/administration/src/module/frosh-thumbnail-processor/test-button/test-button.html.twig @@ -0,0 +1,8 @@ +
+ {{ btnLabel }} +
diff --git a/src/Resources/app/administration/src/service/thumbnailProcessorTestService.js b/src/Resources/app/administration/src/service/thumbnailProcessorTestService.js new file mode 100644 index 0000000..c5aaa8a --- /dev/null +++ b/src/Resources/app/administration/src/service/thumbnailProcessorTestService.js @@ -0,0 +1,25 @@ +const ApiService = Shopware.Classes.ApiService; +const { Application } = Shopware; + +class ApiClient extends ApiService { + constructor(httpClient, loginService, apiEndpoint = 'thumbnail-processor-test') { + super(httpClient, loginService, apiEndpoint); + } + + getUrl(salesChannelId) { + const headers = this.getBasicHeaders({}); + + return this.httpClient + .post(`_action/${this.getApiBasePath()}/get-sample-image`, {'salesChannelId': salesChannelId},{ + headers + }) + .then((response) => { + return ApiService.handleResponse(response); + }); + } +} + +Application.addServiceProvider('thumbnailProcessorTest', (container) => { + const initContainer = Application.getContainer('init'); + return new ApiClient(initContainer.httpClient, container.loginService); +}); diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml index 9a39abd..07879eb 100644 --- a/src/Resources/config/config.xml +++ b/src/Resources/config/config.xml @@ -4,16 +4,24 @@ Configuration + + + Active + + false + Should the pattern be used in shop for images? Keep it disabled unless you tested with the button below. + + ThumbnailPattern {mediaUrl}/{mediaPath}?width={width} {mediaUrl}/{mediaPath}?width={width} - available variables: - {mediaUrl}: https://cdn.test.de/ - {mediaPath}: media/image/5b/6d/16/tea.png + + {mediaUrl}: https://cdn.test.de/
+ {mediaPath}: media/image/5b/6d/16/tea.png
{width}: 800 -
+ ]]>
@@ -39,8 +47,9 @@ froshthumbnailprocessorinfotexts - - configRestriction + + test + Save and Test
diff --git a/src/Resources/config/routes.xml b/src/Resources/config/routes.xml new file mode 100644 index 0000000..02f1960 --- /dev/null +++ b/src/Resources/config/routes.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 836324b..fec05a7 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -36,5 +36,13 @@ + + + + + + + + diff --git a/src/Resources/data/froshthumbnailprocessortestimage.jpg b/src/Resources/data/froshthumbnailprocessortestimage.jpg new file mode 100644 index 0000000..db9277e Binary files /dev/null and b/src/Resources/data/froshthumbnailprocessortestimage.jpg differ diff --git a/src/Resources/store/images/1.jpg b/src/Resources/store/images/1.jpg index 90f6adf..fa650c0 100644 Binary files a/src/Resources/store/images/1.jpg and b/src/Resources/store/images/1.jpg differ diff --git a/src/Service/UrlGeneratorDecorator.php b/src/Service/UrlGeneratorDecorator.php index 4567caf..fa7423b 100644 --- a/src/Service/UrlGeneratorDecorator.php +++ b/src/Service/UrlGeneratorDecorator.php @@ -2,11 +2,13 @@ namespace Frosh\ThumbnailProcessor\Service; +use Frosh\ThumbnailProcessor\Controller\Api\TestController; use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailEntity; use Shopware\Core\Content\Media\MediaEntity; use Shopware\Core\Content\Media\MediaType\ImageType; use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface; use Shopware\Core\DevOps\Environment\EnvironmentHelper; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Contracts\Service\ResetInterface; @@ -33,6 +35,10 @@ public function __construct( public function getAbsoluteMediaUrl(MediaEntity $media): string { + if ($this->isActive() === false) { + return $this->decoratedService->getAbsoluteMediaUrl($media); + } + if (!($media->getMediaType() instanceof ImageType)) { return $this->decoratedService->getAbsoluteMediaUrl($media); } @@ -55,6 +61,10 @@ public function getRelativeMediaUrl(MediaEntity $media): string public function getAbsoluteThumbnailUrl(MediaEntity $media, MediaThumbnailEntity $thumbnail): string { + if ($this->isActive() === false) { + return $this->decoratedService->getAbsoluteMediaUrl($media); + } + if (!$this->canProcessFileExtension($media->getFileExtension())) { return $this->decoratedService->getAbsoluteMediaUrl($media); } @@ -173,4 +183,26 @@ private function getMaxWidth(): string return '3000'; } + + private function isActive(): bool + { + $activeConfig = $this->configReader->getConfig('Active'); + + if ($activeConfig === null) { + return true; + } + + return $this->testisActive() || !empty($activeConfig); + } + + private function testisActive(): bool + { + $mainRequest = $this->requestStack->getMainRequest(); + + if ($mainRequest instanceof Request) { + return $mainRequest->attributes->has(TestController::REQUEST_ATTRIBUTE_TEST_ACTIVE); + } + + return false; + } }