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