[FEATURE] Allow additional file processors 88/61088/13
authorTim Schreiner <schreiner.tim@gmail.com>
Tue, 18 Jun 2019 17:37:13 +0000 (19:37 +0200)
committerTymoteusz Motylewski <t.motylewski@gmail.com>
Wed, 25 Sep 2019 11:35:32 +0000 (13:35 +0200)
Allow registration of additional file processors.

Resolves: #88602
Releases: master
Change-Id: Iff5594ce07750e557f2dc316f3e8aacd90cf5c81
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61088
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
typo3/sysext/core/Classes/Resource/OnlineMedia/Processing/PreviewProcessing.php
typo3/sysext/core/Classes/Resource/Processing/LocalCropScaleMaskHelper.php
typo3/sysext/core/Classes/Resource/Processing/LocalImageProcessor.php
typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php
typo3/sysext/core/Classes/Resource/Processing/ProcessorRegistry.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Feature-88602-AllowAdditionalFileProcessors.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Resource/Processing/ProcessorRegistryTest.php [new file with mode: 0644]

index 4994325..8e0ecce 100644 (file)
@@ -22,7 +22,6 @@ use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
 use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
-use TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor;
 use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
 use TYPO3\CMS\Core\Type\File\ImageInfo;
 use TYPO3\CMS\Core\Utility\CommandUtility;
@@ -36,11 +35,6 @@ use TYPO3\CMS\Frontend\Imaging\GifBuilder;
 class PreviewProcessing
 {
     /**
-     * @var LocalImageProcessor
-     */
-    protected $processor;
-
-    /**
      * @param ProcessedFile $processedFile
      * @return bool
      */
@@ -230,17 +224,6 @@ class PreviewProcessing
     }
 
     /**
-     * @return LocalImageProcessor
-     */
-    protected function getProcessor()
-    {
-        if (!$this->processor) {
-            $this->processor = GeneralUtility::makeInstance(LocalImageProcessor::class);
-        }
-        return $this->processor;
-    }
-
-    /**
      * @return GraphicalFunctions
      */
     protected function getGraphicalFunctionsObject(): GraphicalFunctions
index addb151..63c35a1 100644 (file)
@@ -26,19 +26,6 @@ use TYPO3\CMS\Frontend\Imaging\GifBuilder;
 class LocalCropScaleMaskHelper
 {
     /**
-     * @var LocalImageProcessor
-     */
-    protected $processor;
-
-    /**
-     * @param LocalImageProcessor $processor
-     */
-    public function __construct(LocalImageProcessor $processor)
-    {
-        $this->processor = $processor;
-    }
-
-    /**
      * This method actually does the processing of files locally
      *
      * Takes the original file (for remote storages this will be fetched from the remote server),
index 240b260..c6b9aad 100644 (file)
@@ -125,10 +125,10 @@ class LocalImageProcessor implements ProcessorInterface
     {
         switch ($taskName) {
             case 'Preview':
-                $helper = GeneralUtility::makeInstance(LocalPreviewHelper::class, $this);
+                $helper = GeneralUtility::makeInstance(LocalPreviewHelper::class);
             break;
             case 'CropScaleMask':
-                $helper = GeneralUtility::makeInstance(LocalCropScaleMaskHelper::class, $this);
+                $helper = GeneralUtility::makeInstance(LocalCropScaleMaskHelper::class);
             break;
             default:
                 throw new \InvalidArgumentException('Cannot find helper for task name: "' . $taskName . '"', 1353401352);
index 628775b..6a71628 100644 (file)
@@ -38,19 +38,6 @@ class LocalPreviewHelper
     ];
 
     /**
-     * @var LocalImageProcessor
-     */
-    protected $processor;
-
-    /**
-     * @param LocalImageProcessor $processor
-     */
-    public function __construct(LocalImageProcessor $processor)
-    {
-        $this->processor = $processor;
-    }
-
-    /**
      * Enforce default configuration for preview processing
      *
      * @param array $configuration
diff --git a/typo3/sysext/core/Classes/Resource/Processing/ProcessorRegistry.php b/typo3/sysext/core/Classes/Resource/Processing/ProcessorRegistry.php
new file mode 100644 (file)
index 0000000..61ba70e
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Processing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Registry for images processors.
+ */
+class ProcessorRegistry implements SingletonInterface
+{
+    /**
+     * @var array
+     */
+    protected $registeredProcessors = [];
+
+    /**
+     * Auto register processors from configuration
+     */
+    public function __construct()
+    {
+        $this->registeredProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)
+            ->orderByDependencies($this->getRegisteredProcessors());
+    }
+
+    /**
+     * Finds a matching processor that can process the given task.
+     * Registered processors will be tested by their priority from high to low.
+     *
+     * @param TaskInterface $task
+     * @return ProcessorInterface
+     */
+    public function getProcessorByTask(TaskInterface $task): ProcessorInterface
+    {
+        $processor = null;
+
+        foreach ($this->registeredProcessors as $key => $processorConfiguration) {
+            if (!isset($processorConfiguration['className'])) {
+                throw new \RuntimeException(
+                    'Missing key "className" for processor configuration "' . $key . '".',
+                    1560875741
+                );
+            }
+
+            $processor = GeneralUtility::makeInstance($processorConfiguration['className']);
+
+            if (!$processor instanceof ProcessorInterface) {
+                throw new \RuntimeException(
+                    'Processor "' . get_class($processor) . '" needs to implement interface "' . ProcessorInterface::class . '".',
+                    1560876288
+                );
+            }
+
+            if ($processor->canProcessTask($task)) {
+                /*
+                 * Stop checking for further processors to speed up image processing.
+                 * If another processor should be used, it can be registered with higher priority.
+                 */
+                break;
+            }
+
+            $processor = null;
+        }
+
+        if ($processor === null) {
+            throw new \RuntimeException(
+                'No matching image processor found.',
+                1560876294
+            );
+        }
+
+        return $processor;
+    }
+
+    /**
+     * @return array
+     */
+    protected function getRegisteredProcessors(): array
+    {
+        return $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processors'] ?? [];
+    }
+}
index 334ebf3..67b128c 100644 (file)
@@ -110,8 +110,7 @@ class FileProcessingService
         if ($processedFile->isNew() || (!$processedFile->usesOriginalFile() && !$processedFile->exists()) ||
             $processedFile->isOutdated()) {
             $task = $processedFile->getTask();
-            /** @var Resource\Processing\LocalImageProcessor $processor */
-            $processor = GeneralUtility::makeInstance(Resource\Processing\LocalImageProcessor::class);
+            $processor = $this->getProcessorByTask($task);
             $processor->processTask($task);
 
             if ($task->isExecuted() && $task->isSuccessful() && $processedFile->isProcessed()) {
@@ -123,6 +122,17 @@ class FileProcessingService
     }
 
     /**
+     * @param Resource\Processing\TaskInterface $task
+     * @return Resource\Processing\ProcessorInterface
+     */
+    protected function getProcessorByTask(Resource\Processing\TaskInterface $task): Resource\Processing\ProcessorInterface
+    {
+        $processorRegistry = GeneralUtility::makeInstance(Resource\Processing\ProcessorRegistry::class);
+
+        return $processorRegistry->getProcessorByTask($task);
+    }
+
+    /**
      * Get the SignalSlot dispatcher
      *
      * @return Dispatcher
index f37dc86..903550a 100644 (file)
@@ -242,6 +242,11 @@ return [
                     'filterHiddenFilesAndFolders'
                 ]
             ],
+            'processors' => [
+                'LocalImageProcessor' => [
+                    'className' => \TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor::class,
+                ],
+            ],
             'processingTaskTypes' => [
                 'Image.Preview' => \TYPO3\CMS\Core\Resource\Processing\ImagePreviewTask::class,
                 'Image.CropScaleMask' => \TYPO3\CMS\Core\Resource\Processing\ImageCropScaleMaskTask::class
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-88602-AllowAdditionalFileProcessors.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-88602-AllowAdditionalFileProcessors.rst
new file mode 100644 (file)
index 0000000..0d300e2
--- /dev/null
@@ -0,0 +1,40 @@
+.. include:: ../../Includes.txt
+
+==============================================================
+Feature: #88602 - Allow registering additional file processors
+==============================================================
+
+See :issue:`88602`
+
+Description
+===========
+
+Registering additional file processors has been introduced.
+New processors need to implement the interface :php:`\TYPO3\CMS\Core\Resource\Processing\ProcessorInterface`.
+
+To register a new processor, add the following code to :file:`ext_localconf.php`
+
+.. code-block:: php
+
+   $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processors']['MyNewImageProcessor'] = [
+       'className' => \Vendor\ExtensionName\Resource\Processing\MyNewImageProcessor::class,
+       'before' => 'LocalImageProcessor',
+   ];
+
+To order the processors, use `before` and `after` statements. TYPO3 will process the file
+with the first processor that is able to process a given task.
+
+Impact
+======
+
+Developers are now able to provide their own file processing. By providing priorities, the processor ending up handling
+the file can be determined on a fine granular level including a fallback.
+
+Examples for custom implementations might be:
+
+* add a watermark to each image of type png
+* compress uploaded pdf files into zip archives
+* store images that should be cropped at a separate position in the target storage
+* ... insert your own special use case here.
+
+.. index:: Backend, ext:core, fal
diff --git a/typo3/sysext/core/Tests/Unit/Resource/Processing/ProcessorRegistryTest.php b/typo3/sysext/core/Tests/Unit/Resource/Processing/ProcessorRegistryTest.php
new file mode 100644 (file)
index 0000000..374e90e
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Resource\Processing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Resource\Processing\AbstractTask;
+use TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor;
+use TYPO3\CMS\Core\Resource\Processing\ProcessorRegistry;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case.
+ */
+class ProcessorRegistryTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function getProcessorWhenOnlyOneIsRegistered()
+    {
+        $subject = $this->getAccessibleMockForAbstractClass(
+            ProcessorRegistry::class,
+            [],
+            '',
+            false
+        );
+        $subject->_set('registeredProcessors', [
+            [
+                'className' => LocalImageProcessor::class,
+            ]
+        ]);
+        $taskMock = $this->getAccessibleMockForAbstractClass(
+            AbstractTask::class,
+            [],
+            '',
+            false,
+            false,
+            false,
+            ['getType', 'getName']
+        );
+        $taskMock->expects($this->once())
+            ->method('getType')
+            ->willReturn('Image');
+        $taskMock->expects($this->once())
+            ->method('getName')
+            ->willReturn('CropScaleMask');
+
+        $processor = $subject->getProcessorByTask($taskMock);
+
+        $this->assertInstanceOf(LocalImageProcessor::class, $processor);
+    }
+
+    /**
+     * @test
+     */
+    public function getProcessorWhenNoneIsRegistered()
+    {
+        $this->expectExceptionCode(1560876294);
+
+        $subject = $this->getAccessibleMockForAbstractClass(
+            ProcessorRegistry::class,
+            [],
+            '',
+            false,
+            false,
+            false
+        );
+        $taskMock = $this->getAccessibleMockForAbstractClass(
+            AbstractTask::class,
+            [],
+            '',
+            false,
+            false,
+            false
+        );
+
+        $subject->getProcessorByTask($taskMock);
+    }
+
+    /**
+     * @test
+     */
+    public function getProcessorWhenSameProcessorIsRegisteredTwice()
+    {
+        $subject = $this->getAccessibleMockForAbstractClass(
+            ProcessorRegistry::class,
+            [],
+            '',
+            false
+        );
+        $subject->_set('registeredProcessors', [
+            'LocalImageProcessor' => [
+                'className' => LocalImageProcessor::class,
+            ],
+            'AnotherLocalImageProcessor' => [
+                'className' => LocalImageProcessor::class,
+                'after' => 'LocalImageProcessor',
+            ],
+        ]);
+        $taskMock = $this->getAccessibleMockForAbstractClass(
+            AbstractTask::class,
+            [],
+            '',
+            false,
+            false,
+            false,
+            ['getType', 'getName']
+        );
+        $taskMock->expects($this->once())
+            ->method('getType')
+            ->willReturn('Image');
+        $taskMock->expects($this->once())
+            ->method('getName')
+            ->willReturn('CropScaleMask');
+
+        $processor = $subject->getProcessorByTask($taskMock);
+
+        $this->assertInstanceOf(LocalImageProcessor::class, $processor);
+    }
+}