[BUGFIX] Streamline creating text files in filelist module
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / File / FileController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Controller\File;
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 Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Backend\Clipboard\Clipboard;
21 use TYPO3\CMS\Backend\Routing\UriBuilder;
22 use TYPO3\CMS\Backend\Utility\BackendUtility;
23 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24 use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
25 use TYPO3\CMS\Core\Http\HtmlResponse;
26 use TYPO3\CMS\Core\Http\JsonResponse;
27 use TYPO3\CMS\Core\Http\RedirectResponse;
28 use TYPO3\CMS\Core\Imaging\Icon;
29 use TYPO3\CMS\Core\Imaging\IconFactory;
30 use TYPO3\CMS\Core\Resource\DuplicationBehavior;
31 use TYPO3\CMS\Core\Resource\File;
32 use TYPO3\CMS\Core\Resource\Folder;
33 use TYPO3\CMS\Core\Resource\ProcessedFile;
34 use TYPO3\CMS\Core\Resource\ResourceFactory;
35 use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
36 use TYPO3\CMS\Core\Utility\GeneralUtility;
37 use TYPO3\CMS\Core\Utility\HttpUtility;
38
39 /**
40 * Gateway for TCE (TYPO3 Core Engine) file-handling through POST forms.
41 * This script serves as the file administration part of the TYPO3 Core Engine.
42 * Basically it includes two libraries which are used to manipulate files on the server.
43 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
44 */
45 class FileController
46 {
47 use PublicMethodDeprecationTrait;
48
49 /**
50 * @var array
51 */
52 private $deprecatedPublicMethods = [
53 'initClipboard' => 'Using FileController::initClipboard() is deprecated and will not be possible anymore in TYPO3 v10.0.',
54 'main' => 'Using FileController::main() is deprecated and will not be possible anymore in TYPO3 v10.0.',
55 ];
56
57 /**
58 * Array of file-operations.
59 *
60 * @var array
61 */
62 protected $file;
63
64 /**
65 * Clipboard operations array
66 *
67 * @var array
68 */
69 protected $CB;
70
71 /**
72 * Defines behaviour when uploading files with names that already exist; possible values are
73 * the values of the \TYPO3\CMS\Core\Resource\DuplicationBehavior enumeration
74 *
75 * @var \TYPO3\CMS\Core\Resource\DuplicationBehavior
76 */
77 protected $overwriteExistingFiles;
78
79 /**
80 * The page where the user should be redirected after everything is done
81 *
82 * @var string
83 */
84 protected $redirect;
85
86 /**
87 * Internal, dynamic:
88 * File processor object
89 *
90 * @var ExtendedFileUtility
91 */
92 protected $fileProcessor;
93
94 /**
95 * The result array from the file processor
96 *
97 * @var array
98 */
99 protected $fileData;
100
101 /**
102 * Constructor
103 */
104 public function __construct()
105 {
106 // @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
107 $this->init($GLOBALS['TYPO3_REQUEST']);
108 }
109
110 /**
111 * Injects the request object for the current request or subrequest
112 * As this controller goes only through the main() method, it just redirects to the given URL afterwards.
113 *
114 * @param ServerRequestInterface $request the current request
115 * @return ResponseInterface the response with the content
116 */
117 public function mainAction(ServerRequestInterface $request): ResponseInterface
118 {
119 $this->main();
120
121 BackendUtility::setUpdateSignal('updateFolderTree');
122
123 // go and edit the new created file
124 if ($request->getParsedBody()['edit'] ?? '') {
125 /** @var File $file */
126 $file = $this->fileData['newfile'][0];
127 $this->redirect = $this->getFileEditRedirect($file) ?? $this->redirect;
128 }
129 if ($this->redirect) {
130 return new RedirectResponse(
131 GeneralUtility::locationHeaderUrl($this->redirect),
132 303
133 );
134 }
135 // empty response
136 return new HtmlResponse('');
137 }
138
139 /**
140 * Handles the actual process from within the ajaxExec function
141 * therefore, it does exactly the same as the real typo3/tce_file.php
142 * but without calling the "finish" method, thus makes it simpler to deal with the
143 * actual return value
144 *
145 * @param ServerRequestInterface $request
146 * @return ResponseInterface
147 */
148 public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface
149 {
150 $this->main();
151 $errors = $this->fileProcessor->getErrorMessages();
152 if (!empty($errors)) {
153 return (new HtmlResponse('<t3err>' . implode(',', $errors) . '</t3err>'))->withStatus(500, '(AJAX)');
154 }
155 $flatResult = [];
156 foreach ($this->fileData as $action => $results) {
157 foreach ($results as $result) {
158 if (is_array($result)) {
159 foreach ($result as $subResult) {
160 $flatResult[$action][] = $this->flattenResultDataValue($subResult);
161 }
162 } else {
163 $flatResult[$action][] = $this->flattenResultDataValue($result);
164 }
165 }
166 }
167 return (new JsonResponse())->setPayload($flatResult);
168 }
169
170 /**
171 * Ajax entry point to check if a file exists in a folder
172 *
173 * @param ServerRequestInterface $request
174 * @return ResponseInterface
175 */
176 public function fileExistsInFolderAction(ServerRequestInterface $request): ResponseInterface
177 {
178 $fileName = $request->getParsedBody()['fileName'] ?? $request->getQueryParams()['fileName'] ?? null;
179 $fileTarget = $request->getParsedBody()['fileTarget'] ?? $request->getQueryParams()['fileTarget'] ?? null;
180
181 $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
182 $fileTargetObject = $fileFactory->retrieveFileOrFolderObject($fileTarget);
183 $processedFileName = $fileTargetObject->getStorage()->sanitizeFileName($fileName, $fileTargetObject);
184
185 $result = [];
186 if ($fileTargetObject->hasFile($processedFileName)) {
187 $result = $this->flattenResultDataValue($fileTargetObject->getStorage()->getFileInFolder($processedFileName, $fileTargetObject));
188 }
189 return (new JsonResponse())->setPayload($result);
190 }
191
192 /**
193 * Registering incoming data
194 *
195 * @param ServerRequestInterface $request
196 */
197 protected function init(ServerRequestInterface $request): void
198 {
199 // Set the GPvars from outside
200 $parsedBody = $request->getParsedBody();
201 $queryParams = $request->getQueryParams();
202 $this->file = $parsedBody['data'] ?? $queryParams['data'] ?? null;
203 if ($this->file === null) {
204 // This happens in clipboard mode only
205 $this->redirect = GeneralUtility::sanitizeLocalUrl($parsedBody['redirect'] ?? $queryParams['redirect'] ?? '');
206 } else {
207 $mode = key($this->file);
208 $elementKey = key($this->file[$mode]);
209 $this->redirect = GeneralUtility::sanitizeLocalUrl($this->file[$mode][$elementKey]['redirect']);
210 }
211 $this->CB = $parsedBody['CB'] ?? $queryParams['CB'] ?? null;
212
213 if (isset($this->file['rename'][0]['conflictMode'])) {
214 $conflictMode = $this->file['rename'][0]['conflictMode'];
215 unset($this->file['rename'][0]['conflictMode']);
216 $this->overwriteExistingFiles = DuplicationBehavior::cast($conflictMode);
217 } else {
218 $this->overwriteExistingFiles = DuplicationBehavior::cast($parsedBody['overwriteExistingFiles'] ?? $queryParams['overwriteExistingFiles'] ?? null);
219 }
220 $this->initClipboard();
221 $this->fileProcessor = GeneralUtility::makeInstance(ExtendedFileUtility::class);
222 }
223
224 /**
225 * Initialize the Clipboard. This will fetch the data about files to paste/delete if such an action has been sent.
226 */
227 protected function initClipboard(): void
228 {
229 if (is_array($this->CB)) {
230 $clipObj = GeneralUtility::makeInstance(Clipboard::class);
231 $clipObj->initializeClipboard();
232 if ($this->CB['paste']) {
233 $clipObj->setCurrentPad($this->CB['pad']);
234 $this->file = $clipObj->makePasteCmdArray_file($this->CB['paste'], $this->file);
235 }
236 if ($this->CB['delete']) {
237 $clipObj->setCurrentPad($this->CB['pad']);
238 $this->file = $clipObj->makeDeleteCmdArray_file($this->file);
239 }
240 }
241 }
242
243 /**
244 * Performing the file admin action:
245 * Initializes the objects, setting permissions, sending data to object.
246 */
247 protected function main(): void
248 {
249 // Initializing:
250 $this->fileProcessor->setActionPermissions();
251 $this->fileProcessor->setExistingFilesConflictMode($this->overwriteExistingFiles);
252 $this->fileProcessor->start($this->file);
253 $this->fileData = $this->fileProcessor->processData();
254 }
255
256 /**
257 * Gets URI to be used for editing given file (if file extension is defined in textfile_ext)
258 *
259 * @param File $file to be edited
260 * @return string|null URI to be redirected to
261 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
262 */
263 protected function getFileEditRedirect(File $file): ?string
264 {
265 $textFileExtensionList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] ?? '';
266 if (!GeneralUtility::inList($textFileExtensionList, $file->getExtension())) {
267 return null;
268 }
269 $properties = $file->getProperties();
270 $urlParameters = [
271 'target' => $properties['storage'] . ':' . $properties['identifier']
272 ];
273 if ($this->redirect) {
274 $urlParameters['returnUrl'] = $this->redirect;
275 }
276 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
277 return (string)$uriBuilder->buildUriFromRoute('file_edit', $urlParameters);
278 }
279
280 /**
281 * Redirecting the user after the processing has been done.
282 * Might also display error messages directly, if any.
283 *
284 * @deprecated since TYPO3 v9. Will be removed in TYPO3 v10.0.
285 */
286 public function finish()
287 {
288 trigger_error('FileController->finish() will be removed in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
289 BackendUtility::setUpdateSignal('updateFolderTree');
290 if ($this->redirect) {
291 HttpUtility::redirect($this->redirect);
292 }
293 }
294
295 /**
296 * Flatten result value from FileProcessor
297 *
298 * The value can be a File, Folder or boolean
299 *
300 * @param bool|File|Folder $result
301 *
302 * @return bool|string|array
303 */
304 protected function flattenResultDataValue($result)
305 {
306 if ($result instanceof File) {
307 $thumbUrl = '';
308 if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $result->getExtension())) {
309 $processedFile = $result->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, []);
310 if ($processedFile) {
311 $thumbUrl = $processedFile->getPublicUrl(true);
312 }
313 }
314 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
315 $result = array_merge(
316 $result->toArray(),
317 [
318 'date' => BackendUtility::date($result->getModificationTime()),
319 'icon' => $iconFactory->getIconForFileExtension($result->getExtension(), Icon::SIZE_SMALL)->render(),
320 'thumbUrl' => $thumbUrl
321 ]
322 );
323 } elseif ($result instanceof Folder) {
324 $result = $result->getIdentifier();
325 }
326
327 return $result;
328 }
329
330 /**
331 * Returns the current BE user.
332 *
333 * @return BackendUserAuthentication
334 */
335 protected function getBackendUser(): BackendUserAuthentication
336 {
337 return $GLOBALS['BE_USER'];
338 }
339 }