1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Frontend\Controller;
17
18use Psr\Http\Message\ResponseInterface;
19use Psr\Http\Message\ServerRequestInterface;
20use TYPO3\CMS\Core\Exception;
21use TYPO3\CMS\Core\Http\Response;
22use TYPO3\CMS\Core\Resource\FileInterface;
23use TYPO3\CMS\Core\Resource\ProcessedFile;
24use TYPO3\CMS\Core\Resource\ResourceFactory;
25use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
26use TYPO3\CMS\Core\Utility\GeneralUtility;
27use TYPO3\CMS\Core\Utility\MathUtility;
28
29/**
30 * eID-Script "tx_cms_showpic"
31 *
32 * Shows a picture from FAL in enlarged format in a separate window.
33 * Picture file and settings is supplied by GET-parameters:
34 *
35 *  - file = fileUid or Combined Identifier
36 *  - encoded in a parameter Array (with weird format - see ContentObjectRenderer about ll. 1500)
37 *  - width, height = usual width an height, m/c supported
38 *  - frame
39 *  - bodyTag
40 *  - title
41 *
42 * @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
43 */
44class ShowImageController
45{
46    protected const ALLOWED_PARAMETER_NAMES = ['width', 'height', 'crop', 'bodyTag', 'title'];
47
48    /**
49     * @var \Psr\Http\Message\ServerRequestInterface
50     */
51    protected $request;
52
53    /**
54     * @var \TYPO3\CMS\Core\Resource\File
55     */
56    protected $file;
57
58    /**
59     * @var int
60     */
61    protected $width;
62
63    /**
64     * @var int
65     */
66    protected $height;
67
68    /**
69     * @var string
70     */
71    protected $crop;
72
73    /**
74     * @var int
75     */
76    protected $frame;
77
78    /**
79     * @var string
80     */
81    protected $bodyTag = '<body>';
82
83    /**
84     * @var string
85     */
86    protected $title = 'Image';
87
88    /**
89     * @var string
90     */
91    protected $content = <<<EOF
92<!DOCTYPE html>
93<html>
94<head>
95	<title>###TITLE###</title>
96	<meta name="robots" content="noindex,follow" />
97</head>
98###BODY###
99	###IMAGE###
100</body>
101</html>
102EOF;
103
104    /**
105     * @var string
106     */
107    protected $imageTag = '<img src="###publicUrl###" alt="###alt###" title="###title###" width="###width###" height="###height###" />';
108
109    /**
110     * Init function, setting the input vars in the global space.
111     *
112     * @throws \InvalidArgumentException
113     * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
114     */
115    public function initialize()
116    {
117        $fileUid = $this->request->getQueryParams()['file'] ?? null;
118        $parametersArray = $this->request->getQueryParams()['parameters'] ?? null;
119
120        // If no file-param or parameters are given, we must exit
121        if (!$fileUid || !isset($parametersArray) || !is_array($parametersArray)) {
122            throw new \InvalidArgumentException('No valid fileUid given', 1476048455);
123        }
124
125        // rebuild the parameter array and check if the HMAC is correct
126        $parametersEncoded = implode('', $parametersArray);
127
128        /* For backwards compatibility the HMAC is transported within the md5 param */
129        $hmacParameter = $this->request->getQueryParams()['md5'] ?? null;
130        $hmac = GeneralUtility::hmac(implode('|', [$fileUid, $parametersEncoded]));
131        if (!is_string($hmacParameter) || !hash_equals($hmac, $hmacParameter)) {
132            throw new \InvalidArgumentException('hash does not match', 1476048456);
133        }
134
135        // decode the parameters Array - `bodyTag` contains HTML if set and would lead
136        // to a false-positive XSS-detection, that's why parameters are base64-encoded
137        $parameters = json_decode(base64_decode($parametersEncoded), true) ?? [];
138        foreach ($parameters as $parameterName => $parameterValue) {
139            if (in_array($parameterName, static::ALLOWED_PARAMETER_NAMES, true)) {
140                $this->{$parameterName} = $parameterValue;
141            }
142        }
143
144        if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
145            $this->file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$fileUid);
146        } else {
147            $this->file = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($fileUid);
148        }
149        if (!($this->file instanceof FileInterface && $this->isFileValid($this->file))) {
150            throw new Exception('File processing for local storage is denied', 1594043425);
151        }
152
153        $this->frame = $this->request->getQueryParams()['frame'] ?? null;
154    }
155
156    /**
157     * Main function which creates the image if needed and outputs the HTML code for the page displaying the image.
158     * Accumulates the content in $this->content
159     */
160    public function main()
161    {
162        $processedImage = $this->processImage();
163        $imageTagMarkers = [
164            '###publicUrl###' => htmlspecialchars($processedImage->getPublicUrl() ?? ''),
165            '###alt###' => htmlspecialchars($this->file->getProperty('alternative') ?: $this->title),
166            '###title###' => htmlspecialchars($this->file->getProperty('title') ?: $this->title),
167            '###width###' => $processedImage->getProperty('width'),
168            '###height###' => $processedImage->getProperty('height')
169        ];
170        $this->imageTag = str_replace(array_keys($imageTagMarkers), array_values($imageTagMarkers), $this->imageTag);
171        $markerArray = [
172            '###TITLE###' => $this->file->getProperty('title') ?: $this->title,
173            '###IMAGE###' => $this->imageTag,
174            '###BODY###' => $this->bodyTag
175        ];
176
177        $this->content = str_replace(array_keys($markerArray), array_values($markerArray), $this->content);
178    }
179
180    /**
181     * Does the actual image processing
182     *
183     * @return \TYPO3\CMS\Core\Resource\ProcessedFile
184     */
185    protected function processImage()
186    {
187        $max = strpos($this->width . $this->height, 'm') !== false ? 'm' : '';
188        $this->height = MathUtility::forceIntegerInRange($this->height, 0);
189        $this->width = MathUtility::forceIntegerInRange($this->width, 0) . $max;
190
191        $processingConfiguration = [
192            'width' => $this->width,
193            'height' => $this->height,
194            'frame' => $this->frame,
195            'crop' => $this->crop,
196        ];
197        return $this->file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
198    }
199
200    /**
201     * Fetches the content and builds a content file out of it
202     *
203     * @param ServerRequestInterface $request the current request object
204     * @return ResponseInterface the modified response
205     */
206    public function processRequest(ServerRequestInterface $request): ResponseInterface
207    {
208        $this->request = $request;
209
210        try {
211            $this->initialize();
212            $this->main();
213            $response = new Response();
214            $response->getBody()->write($this->content);
215            return $response;
216        } catch (\InvalidArgumentException $e) {
217            // add a 410 "gone" if invalid parameters given
218            return (new Response())->withStatus(410);
219        } catch (Exception $e) {
220            return (new Response())->withStatus(404);
221        }
222    }
223
224    protected function isFileValid(FileInterface $file): bool
225    {
226        return $file->getStorage()->getDriverType() !== 'Local'
227            || GeneralUtility::makeInstance(FileNameValidator::class)
228                ->isValid(basename($file->getIdentifier()));
229    }
230}
231