From 72a4825c1b1202327d4d2647c8011bed461f6531 Mon Sep 17 00:00:00 2001
From: Stefan Froemken <froemken@gmail.com>
Date: Thu, 9 Jan 2020 09:15:44 +0100
Subject: [PATCH] [FEATURE] Improve FileDumpController
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Add possibility to use records of sys_file_reference
- Add possibility to resize images
- Add possibility to apply cropVariants

Resolves: #90068
Releases: master
Change-Id: Ib80021dc25b42e7021cf5429b2df8029aac1fd8c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62834
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Christian Eßl <indy.essl@gmail.com>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Susanne Moog <look@susi.dev>
---
 .../Classes/Controller/FileDumpController.php | 186 +++++++++++++-----
 typo3/sysext/core/Configuration/Services.yaml |   3 +
 ...0068-ImplementBetterFileDumpController.rst |  80 ++++++++
 .../extbase/Classes/Service/ImageService.php  |   5 +-
 4 files changed, 227 insertions(+), 47 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-90068-ImplementBetterFileDumpController.rst

diff --git a/typo3/sysext/core/Classes/Controller/FileDumpController.php b/typo3/sysext/core/Classes/Controller/FileDumpController.php
index 1ec78dcbe70f..2c272f378d1e 100644
--- a/typo3/sysext/core/Classes/Controller/FileDumpController.php
+++ b/typo3/sysext/core/Classes/Controller/FileDumpController.php
@@ -1,6 +1,5 @@
 <?php
 declare(strict_types = 1);
-
 namespace TYPO3\CMS\Core\Controller;
 
 /*
@@ -19,7 +18,12 @@ namespace TYPO3\CMS\Core\Controller;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
+use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\FileReference;
 use TYPO3\CMS\Core\Resource\Hook\FileDumpEIDHookInterface;
+use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -29,78 +33,168 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  */
 class FileDumpController
 {
+    /**
+     * @var ResourceFactory
+     */
+    protected $resourceFactory;
+
+    public function __construct(ResourceFactory $resourceFactory)
+    {
+        $this->resourceFactory = $resourceFactory;
+    }
+
     /**
      * Main method to dump a file
      *
      * @param ServerRequestInterface $request
      * @return ResponseInterface
-     *
      * @throws \InvalidArgumentException
      * @throws \RuntimeException
-     * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
+     * @throws FileDoesNotExistException
      * @throws \UnexpectedValueException
      */
     public function dumpAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $parameters = $this->buildParametersFromRequest($request);
+
+        if (!$this->isTokenValid($parameters, $request)) {
+            return (new Response)->withStatus(403);
+        }
+        $file = $this->createFileObjectByParameters($parameters);
+        if ($file === null) {
+            return (new Response)->withStatus(404);
+        }
+
+        // Hook: allow some other process to do some security/access checks. Hook should return 403 response if access is rejected, void otherwise
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['FileDumpEID.php']['checkFileAccess'] ?? [] as $className) {
+            $hookObject = GeneralUtility::makeInstance($className);
+            if (!$hookObject instanceof FileDumpEIDHookInterface) {
+                throw new \UnexpectedValueException($className . ' must implement interface ' . FileDumpEIDHookInterface::class, 1394442417);
+            }
+            $response = $hookObject->checkFileAccess($file);
+            if ($response instanceof ResponseInterface) {
+                return $response;
+            }
+        }
+
+        // Apply cropping, if possible
+        if (!$file instanceof ProcessedFile) {
+            $cropVariant = $parameters['cv'] ?: 'default';
+            $cropString = $file instanceof FileReference ? $file->getProperty('crop') : '';
+            $cropArea = CropVariantCollection::create((string)$cropString)->getCropArea($cropVariant);
+            $processingInstructions = [
+                'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($file),
+            ];
+
+            // Apply width/height, if given
+            if (!empty($parameters['s'])) {
+                $size = GeneralUtility::trimExplode(':', $parameters['s']);
+                $processingInstructions = array_merge(
+                    $processingInstructions,
+                    [
+                        'width' => $size[0] ?? null,
+                        'height' => $size[1] ?? null,
+                        'minWidth' => $size[2] ? (int)$size[2] : null,
+                        'minHeight' => $size[3] ? (int)$size[3] : null,
+                        'maxWidth' => $size[4] ? (int)$size[4] : null,
+                        'maxHeight' => $size[5] ? (int)$size[5] : null
+                    ]
+                );
+            }
+            if (is_callable([$file, 'getOriginalFile'])) {
+                // Get the original file from the file reference
+                $file = $file->getOriginalFile();
+            }
+            $file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);
+        }
+
+        return $file->getStorage()->streamFile($file);
+    }
+
+    protected function buildParametersFromRequest(ServerRequestInterface $request): array
     {
         $parameters = ['eID' => 'dumpFile'];
-        $t = $this->getGetOrPost($request, 't');
+        $queryParams = $request->getQueryParams();
+        // Identifier of what to process. f, r or p
+        // Only needed while hash_equals
+        $t = (string)($queryParams['t'] ?? '');
         if ($t) {
             $parameters['t'] = $t;
         }
-        $f = $this->getGetOrPost($request, 'f');
+        // sys_file
+        $f = (string)($queryParams['f'] ?? '');
         if ($f) {
-            $parameters['f'] = $f;
+            $parameters['f'] = (int)$f;
+        }
+        // sys_file_reference
+        $r = (string)($queryParams['r'] ?? '');
+        if ($r) {
+            $parameters['r'] = (int)$r;
         }
-        $p = $this->getGetOrPost($request, 'p');
+        // Processed file
+        $p = (string)($queryParams['p'] ?? '');
         if ($p) {
-            $parameters['p'] = $p;
+            $parameters['p'] = (int)$p;
         }
+        // File's width and height in this order: w:h:minW:minH:maxW:maxH
+        $s = (string)($queryParams['s'] ?? '');
+        if ($s) {
+            $parameters['s'] = $s;
+        }
+        // File's crop variant
+        $v = (string)($queryParams['cv'] ?? '');
+        if ($v) {
+            $parameters['cv'] = (string)$v;
+        }
+
+        return $parameters;
+    }
+
+    protected function isTokenValid(array $parameters, ServerRequestInterface $request): bool
+    {
+        return hash_equals(
+            GeneralUtility::hmac(implode('|', $parameters), 'resourceStorageDumpFile'),
+            $request->getQueryParams()['token'] ?? ''
+        );
+    }
 
-        if (hash_equals(GeneralUtility::hmac(implode('|', $parameters), 'resourceStorageDumpFile'), $this->getGetOrPost($request, 'token'))) {
-            if (isset($parameters['f'])) {
-                try {
-                    $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($parameters['f']);
-                    if ($file->isDeleted() || $file->isMissing()) {
-                        $file = null;
-                    }
-                } catch (\Exception $e) {
+    /**
+     * @param array $parameters
+     * @return File|FileReference|ProcessedFile|null
+     */
+    protected function createFileObjectByParameters(array $parameters)
+    {
+        $file = null;
+        if (isset($parameters['f'])) {
+            try {
+                $file = $this->resourceFactory->getFileObject($parameters['f']);
+                if ($file->isDeleted() || $file->isMissing()) {
                     $file = null;
                 }
-            } else {
-                $file = GeneralUtility::makeInstance(ProcessedFileRepository::class)->findByUid($parameters['p']);
-                if (!$file || $file->isDeleted()) {
+            } catch (\Exception $e) {
+                $file = null;
+            }
+        } elseif (isset($parameters['r'])) {
+            try {
+                $file = $this->resourceFactory->getFileReferenceObject($parameters['r']);
+                if ($file->isMissing()) {
                     $file = null;
                 }
+            } catch (\Exception $e) {
+                $file = null;
             }
-
-            if ($file === null) {
-                return (new Response)->withStatus(404);
-            }
-
-            // Hook: allow some other process to do some security/access checks. Hook should return 403 response if access is rejected, void otherwise
-            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['FileDumpEID.php']['checkFileAccess'] ?? [] as $className) {
-                $hookObject = GeneralUtility::makeInstance($className);
-                if (!$hookObject instanceof FileDumpEIDHookInterface) {
-                    throw new \UnexpectedValueException($className . ' must implement interface ' . FileDumpEIDHookInterface::class, 1394442417);
-                }
-                $response = $hookObject->checkFileAccess($file);
-                if ($response instanceof ResponseInterface) {
-                    return $response;
+        } elseif (isset($parameters['p'])) {
+            try {
+                $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
+                /** @var ProcessedFile|null $file */
+                $file = $processedFileRepository->findByUid($parameters['p']);
+                if (!$file || $file->isDeleted()) {
+                    $file = null;
                 }
+            } catch (\Exception $e) {
+                $file = null;
             }
-
-            return $file->getStorage()->streamFile($file);
         }
-        return (new Response)->withStatus(403);
-    }
-
-    /**
-     * @param ServerRequestInterface $request
-     * @param string $parameter
-     * @return string
-     */
-    protected function getGetOrPost(ServerRequestInterface $request, string $parameter): string
-    {
-        return (string)($request->getParsedBody()[$parameter] ?? $request->getQueryParams()[$parameter] ?? '');
+        return $file;
     }
 }
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index 5ac89f353670..850feacf6dda 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -85,6 +85,9 @@ services:
   TYPO3\CMS\Core\Mail\Mailer:
     public: true
 
+  TYPO3\CMS\Core\Controller\FileDumpController:
+    public: true
+
   TYPO3\CMS\Core\Core\ClassLoadingInformation:
     public: false
     tags:
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90068-ImplementBetterFileDumpController.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90068-ImplementBetterFileDumpController.rst
new file mode 100644
index 000000000000..0fb440d6dafc
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90068-ImplementBetterFileDumpController.rst
@@ -0,0 +1,80 @@
+.. include:: ../../Includes.txt
+
+=====================================================================
+Feature: #90068 - Implement better FileDumpController
+=====================================================================
+
+See :issue:`90068`
+
+Description
+===========
+
+FileDumpController can now process UIDs of sys_file_reference records and
+can adopt image sizes to records of sys_file.
+
+Following URI-Parameters are now possible:
+
++ `t` (*Type*): Can be one of `f` (sys_file), `r` (sys_file_reference) or `p` (sys_file_processedfile)
++ `f` (*File*): Use it for an UID of table sys_file
++ `r` (*Reference*): Use it for an UID of table sys_file_reference
++ `p` (*Processed*): Use it for an UID of table sys_file_processedfile
++ `s` (*Size*): Use it for an UID of table sys_file_processedfile
++ `cv` (*CropVariant*): In case of sys_file_reference, you can assign it a cropping variant
+
+You have to choose one of these parameters: `f`, `r` and `p`. It is not possible
+to use them multiple times in one request.
+
+The Parameter `s` has following syntax: width:height:minW:minH:maxW:maxH. You
+can leave this Parameter empty to load file in original size. Parameter `width`
+and `height` can consist of trailing `c` or `m` identicator like known from TS.
+
+See following example how to create an URI using the FileDumpController for
+a sys_file record with a fixed image size:
+
+.. code-block:: php
+
+   $queryParameterArray = ['eID' => 'dumpFile', 't' => 'f'];
+   $queryParameterArray['f'] = $resourceObject->getUid();
+   $queryParameterArray['s'] = '320c:280c';
+   $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
+   $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
+   $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
+
+
+In this example crop variant `default` and an image size of 320:280 will be
+applied to a sys_file_reference record:
+
+.. code-block:: php
+
+   $queryParameterArray = ['eID' => 'dumpFile', 't' => 'r'];
+   $queryParameterArray['f'] = $resourceObject->getUid();
+   $queryParameterArray['s'] = '320c:280c:320:280:320:280';
+   $queryParameterArray['cv'] = 'default';
+   $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
+   $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
+   $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
+
+
+This example shows the usage how to create an URI to load an image of
+sys_file_processfiles:
+
+.. code-block:: php
+
+   $queryParameterArray = ['eID' => 'dumpFile', 't' => 'p'];
+   $queryParameterArray['p'] = $resourceObject->getUid();
+   $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
+   $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
+   $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
+
+
+There are some restriction while using the new URI-Parameters:
++ You can't assign any size parameter to processed files, as they are already resized.
++ You can't apply CropVariants to sys_file and sys_file_processedfile records.
+
+
+Impact
+======
+
+No impact, as this class was extended only. It's full backwards compatible
+
+.. index:: FAL, ext:core
diff --git a/typo3/sysext/extbase/Classes/Service/ImageService.php b/typo3/sysext/extbase/Classes/Service/ImageService.php
index 8f3611c9f645..32b463a8be2e 100644
--- a/typo3/sysext/extbase/Classes/Service/ImageService.php
+++ b/typo3/sysext/extbase/Classes/Service/ImageService.php
@@ -176,7 +176,10 @@ class ImageService implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function setCompatibilityValues(ProcessedFile $processedImage): void
     {
-        if ($this->environmentService->isEnvironmentInFrontendMode()) {
+        if (
+            $this->environmentService->isEnvironmentInFrontendMode()
+            && is_object($GLOBALS['TSFE'])
+        ) {
             $GLOBALS['TSFE']->lastImageInfo = $this->getCompatibilityImageResourceValues($processedImage);
             $GLOBALS['TSFE']->imagesOnPage[] = $processedImage->getPublicUrl();
         }
-- 
GitLab