[BUGFIX] Fix invalid type hints in EXT:form's file upload converter
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Mvc / Property / TypeConverter / UploadedFileReferenceConverter.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Log\LogManager;
19 use TYPO3\CMS\Core\Resource\File as File;
20 use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder;
23 use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference;
24 use TYPO3\CMS\Extbase\Error\Error;
25 use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
26 use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
27 use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
28 use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
29 use TYPO3\CMS\Form\Service\TranslationService;
30
31 /**
32 * Class UploadedFileReferenceConverter
33 *
34 * Scope: frontend
35 * @internal
36 */
37 class UploadedFileReferenceConverter extends AbstractTypeConverter
38 {
39
40 /**
41 * Folder where the file upload should go to (including storage).
42 */
43 const CONFIGURATION_UPLOAD_FOLDER = 1;
44
45 /**
46 * How to handle a upload when the name of the uploaded file conflicts.
47 */
48 const CONFIGURATION_UPLOAD_CONFLICT_MODE = 2;
49
50 /**
51 * Validator for file types
52 */
53 const CONFIGURATION_FILE_VALIDATORS = 4;
54
55 /**
56 * @var string
57 */
58 protected $defaultUploadFolder = '1:/user_upload/';
59
60 /**
61 * One of 'cancel', 'replace', 'rename'
62 *
63 * @var string
64 */
65 protected $defaultConflictMode = 'rename';
66
67 /**
68 * @var array
69 */
70 protected $sourceTypes = ['array'];
71
72 /**
73 * @var string
74 */
75 protected $targetType = ExtbaseFileReference::class;
76
77 /**
78 * Take precedence over the available FileReferenceConverter
79 *
80 * @var int
81 */
82 protected $priority = 12;
83
84 /**
85 * @var \TYPO3\CMS\Core\Resource\FileInterface[]
86 */
87 protected $convertedResources = [];
88
89 /**
90 * @var \TYPO3\CMS\Core\Resource\ResourceFactory
91 */
92 protected $resourceFactory;
93
94 /**
95 * @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
96 */
97 protected $hashService;
98
99 /**
100 * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
101 */
102 protected $persistenceManager;
103
104 /**
105 * @param \TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory
106 * @internal
107 */
108 public function injectResourceFactory(\TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory)
109 {
110 $this->resourceFactory = $resourceFactory;
111 }
112
113 /**
114 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
115 * @internal
116 */
117 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
118 {
119 $this->hashService = $hashService;
120 }
121
122 /**
123 * @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
124 * @internal
125 */
126 public function injectPersistenceManager(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager)
127 {
128 $this->persistenceManager = $persistenceManager;
129 }
130
131 /**
132 * Actually convert from $source to $targetType, taking into account the fully
133 * built $convertedChildProperties and $configuration.
134 *
135 * @param array $source
136 * @param string $targetType
137 * @param array $convertedChildProperties
138 * @param PropertyMappingConfigurationInterface $configuration
139 * @return AbstractFileFolder|Error|null
140 * @internal
141 */
142 public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
143 {
144 if (!isset($source['error']) || $source['error'] === \UPLOAD_ERR_NO_FILE) {
145 if (isset($source['submittedFile']['resourcePointer'])) {
146 try {
147 // File references use numeric resource pointers, direct
148 // file relations are using "file:" prefix (e.g. "file:5")
149 $resourcePointer = $this->hashService->validateAndStripHmac($source['submittedFile']['resourcePointer']);
150 if (strpos($resourcePointer, 'file:') === 0) {
151 $fileUid = (int)substr($resourcePointer, 5);
152 return $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid));
153 }
154 return $this->createFileReferenceFromFalFileReferenceObject(
155 $this->resourceFactory->getFileReferenceObject($resourcePointer),
156 (int)$resourcePointer
157 );
158 } catch (\InvalidArgumentException $e) {
159 // Nothing to do. No file is uploaded and resource pointer is invalid. Discard!
160 }
161 }
162 return null;
163 }
164
165 if ($source['error'] !== \UPLOAD_ERR_OK) {
166 return $this->objectManager->get(Error::class, $this->getUploadErrorMessage($source['error']), 1471715915);
167 }
168
169 if (isset($this->convertedResources[$source['tmp_name']])) {
170 return $this->convertedResources[$source['tmp_name']];
171 }
172
173 try {
174 $resource = $this->importUploadedResource($source, $configuration);
175 } catch (\Exception $e) {
176 return $this->objectManager->get(Error::class, $e->getMessage(), $e->getCode());
177 }
178
179 $this->convertedResources[$source['tmp_name']] = $resource;
180 return $resource;
181 }
182
183 /**
184 * Import a resource and respect configuration given for properties
185 *
186 * @param array $uploadInfo
187 * @param PropertyMappingConfigurationInterface $configuration
188 * @return ExtbaseFileReference
189 * @throws TypeConverterException
190 */
191 protected function importUploadedResource(
192 array $uploadInfo,
193 PropertyMappingConfigurationInterface $configuration
194 ): ExtbaseFileReference {
195 if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) {
196 throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1471710357);
197 }
198
199 $uploadFolderId = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $this->defaultUploadFolder;
200 $conflictMode = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $this->defaultConflictMode;
201
202 $uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId);
203 $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode);
204
205 $validators = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_FILE_VALIDATORS);
206 if (is_array($validators)) {
207 foreach ($validators as $validator) {
208 if ($validator instanceof AbstractValidator) {
209 $validationResult = $validator->validate($uploadedFile);
210 if ($validationResult->hasErrors()) {
211 $uploadedFile->getStorage()->deleteFile($uploadedFile);
212 throw new TypeConverterException($validationResult->getErrors()[0]->getMessage(), 1471708999);
213 }
214 }
215 }
216 }
217
218 $resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) && strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === false
219 ? $this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer'])
220 : null;
221
222 $fileReferenceModel = $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer);
223
224 return $fileReferenceModel;
225 }
226
227 /**
228 * @param File $file
229 * @param int $resourcePointer
230 * @return ExtbaseFileReference
231 */
232 protected function createFileReferenceFromFalFileObject(
233 File $file,
234 int $resourcePointer = null
235 ): ExtbaseFileReference {
236 $fileReference = $this->resourceFactory->createFileReferenceObject(
237 [
238 'uid_local' => $file->getUid(),
239 'uid_foreign' => uniqid('NEW_'),
240 'uid' => uniqid('NEW_'),
241 'crop' => null,
242 ]
243 );
244 return $this->createFileReferenceFromFalFileReferenceObject($fileReference, $resourcePointer);
245 }
246
247 /**
248 * In case no $resourcePointer is given a new file reference domain object
249 * will be returned. Otherwise the file reference is reconstituted from
250 * storage and will be updated(!) with the provided $falFileReference.
251 *
252 * @param CoreFileReference $falFileReference
253 * @param int $resourcePointer
254 * @return ExtbaseFileReference
255 */
256 protected function createFileReferenceFromFalFileReferenceObject(
257 CoreFileReference $falFileReference,
258 int $resourcePointer = null
259 ): ExtbaseFileReference {
260 if ($resourcePointer === null) {
261 $fileReference = $this->objectManager->get(ExtbaseFileReference::class);
262 } else {
263 $fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, ExtbaseFileReference::class, false);
264 }
265
266 $fileReference->setOriginalResource($falFileReference);
267 return $fileReference;
268 }
269
270 /**
271 * Returns a human-readable message for the given PHP file upload error
272 * constant.
273 *
274 * @param int $errorCode
275 * @return string
276 */
277 protected function getUploadErrorMessage(int $errorCode): string
278 {
279 $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
280 switch ($errorCode) {
281 case \UPLOAD_ERR_INI_SIZE:
282 $logger->error('The uploaded file exceeds the upload_max_filesize directive in php.ini.', []);
283 return TranslationService::getInstance()->translate('upload.error.150530345', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
284 case \UPLOAD_ERR_FORM_SIZE:
285 $logger->error('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', []);
286 return TranslationService::getInstance()->translate('upload.error.150530345', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
287 case \UPLOAD_ERR_PARTIAL:
288 $logger->error('The uploaded file was only partially uploaded.', []);
289 return TranslationService::getInstance()->translate('upload.error.150530346', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
290 case \UPLOAD_ERR_NO_FILE:
291 $logger->error('No file was uploaded.', []);
292 return TranslationService::getInstance()->translate('upload.error.150530347', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
293 case \UPLOAD_ERR_NO_TMP_DIR:
294 $logger->error('Missing a temporary folder.', []);
295 return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
296 case \UPLOAD_ERR_CANT_WRITE:
297 $logger->error('Failed to write file to disk.', []);
298 return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
299 case \UPLOAD_ERR_EXTENSION:
300 $logger->error('File upload stopped by extension.', []);
301 return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
302 default:
303 $logger->error('Unknown upload error.', []);
304 return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
305 }
306 }
307 }