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