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