924b9b1aba9d3c3027510af4582f3c009f2b0f0c
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / File / EditFileController.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\Form\FormResultCompiler;
21 use TYPO3\CMS\Backend\Form\NodeFactory;
22 use TYPO3\CMS\Backend\Routing\UriBuilder;
23 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
24 use TYPO3\CMS\Backend\Template\DocumentTemplate;
25 use TYPO3\CMS\Backend\Template\ModuleTemplate;
26 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
28 use TYPO3\CMS\Core\Http\HtmlResponse;
29 use TYPO3\CMS\Core\Http\RedirectResponse;
30 use TYPO3\CMS\Core\Imaging\Icon;
31 use TYPO3\CMS\Core\Localization\LanguageService;
32 use TYPO3\CMS\Core\Messaging\FlashMessage;
33 use TYPO3\CMS\Core\Messaging\FlashMessageService;
34 use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
35 use TYPO3\CMS\Core\Resource\ResourceFactory;
36 use TYPO3\CMS\Core\Utility\GeneralUtility;
37 use TYPO3\CMS\Core\Utility\HttpUtility;
38 use TYPO3\CMS\Fluid\View\StandaloneView;
39
40 /**
41 * Script Class for rendering the file editing screen
42 */
43 class EditFileController
44 {
45 use PublicPropertyDeprecationTrait;
46
47 /**
48 * @var array
49 */
50 protected $deprecatedPublicProperties = [
51 'origTarget' => 'Using $origTarget of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
52 'target' => 'Using $target of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
53 'returnUrl' => 'Using $returnUrl of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
54 'content' => 'Using $content of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
55 'title' => 'Using $title of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
56 'doc' => 'Using $doc of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
57 ];
58 /**
59 * Module content accumulated.
60 *
61 * @var string
62 */
63 protected $content;
64
65 /**
66 * @var string
67 */
68 protected $title;
69
70 /**
71 * Document template object
72 *
73 * @var DocumentTemplate
74 * @deprecated since v9, will be removed in v10, unused
75 */
76 protected $doc;
77
78 /**
79 * Original input target
80 *
81 * @var string
82 */
83 protected $origTarget;
84
85 /**
86 * The original target, but validated.
87 *
88 * @var string
89 */
90 protected $target;
91
92 /**
93 * Return URL of list module.
94 *
95 * @var string
96 */
97 protected $returnUrl;
98
99 /**
100 * the file that is being edited on
101 *
102 * @var \TYPO3\CMS\Core\Resource\AbstractFile
103 */
104 protected $fileObject;
105
106 /**
107 * ModuleTemplate object
108 *
109 * @var ModuleTemplate
110 */
111 protected $moduleTemplate;
112
113 /**
114 * Constructor
115 */
116 public function __construct()
117 {
118 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
119 $GLOBALS['SOBE'] = $this;
120 // @deprecated since v9, will be moved out of __construct() in v10
121 $this->init($GLOBALS['TYPO3_REQUEST']);
122 }
123
124 /**
125 * Processes the request, currently everything is handled and put together via "main()"
126 *
127 * @param ServerRequestInterface $request the current request
128 * @return ResponseInterface the response with the content
129 */
130 public function mainAction(ServerRequestInterface $request): ResponseInterface
131 {
132 if ($response = $this->main()) {
133 return $response;
134 }
135 return new HtmlResponse($this->moduleTemplate->renderContent());
136 }
137
138 /**
139 * Initialize script class
140 *
141 * @param ServerRequestInterface $request
142 *
143 * @throws InsufficientFileAccessPermissionsException
144 */
145 protected function init(ServerRequestInterface $request): void
146 {
147 $parsedBody = $request->getParsedBody();
148 $queryParams = $request->getQueryParams();
149
150 // Setting target, which must be a file reference to a file within the mounts.
151 $this->target = $this->origTarget = $parsedBody['target'] ?? $queryParams['target'] ?? '';
152 $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
153 // create the file object
154 if ($this->target) {
155 $this->fileObject = ResourceFactory::getInstance()
156 ->retrieveFileOrFolderObject($this->target);
157 }
158 // Cleaning and checking target directory
159 if (!$this->fileObject) {
160 $title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
161 $message = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
162 throw new \RuntimeException($title . ': ' . $message, 1294586841);
163 }
164 if ($this->fileObject->getStorage()->getUid() === 0) {
165 throw new InsufficientFileAccessPermissionsException(
166 'You are not allowed to access files outside your storages',
167 1375889832
168 );
169 }
170
171 // Setting the title and the icon
172 $icon = $this->moduleTemplate->getIconFactory()->getIcon('apps-filetree-root', Icon::SIZE_SMALL)->render();
173 $this->title = $icon
174 . htmlspecialchars(
175 $this->fileObject->getStorage()->getName()
176 ) . ': ' . htmlspecialchars(
177 $this->fileObject->getIdentifier()
178 );
179
180 // Setting template object
181 $this->moduleTemplate->addJavaScriptCode(
182 'FileEditBackToList',
183 'function backToList() {
184 top.goToModule("file_FilelistList");
185 }'
186 );
187 }
188
189 /**
190 * Main function, rendering the actual content of the editing page
191 *
192 * @return ResponseInterface|null Possible redirect response
193 */
194 public function main(): ?ResponseInterface
195 {
196 // @deprecated Variable can be removed in v10
197 $deprecatedCaller = false;
198 // Foreign class call? Method will be protected in v10, giving core freedom to move stuff around
199 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
200 if (end($backtrace)['class'] !== __CLASS__) {
201 // @deprecated since TYPO3 v9, this method will be set to protected in v10
202 trigger_error('Method main() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
203 $deprecatedCaller = true;
204 }
205
206 $dataColumnDefinition = [
207 'label' => htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'))
208 . ' ' . htmlspecialchars($this->target),
209 'config' => [
210 'type' => 'text',
211 'cols' => 48,
212 'wrap' => 'OFF',
213 ],
214 'defaultExtras' => 'fixed-font: enable-tab'
215 ];
216
217 $this->getButtonsInternal();
218 // Hook: before compiling the output
219 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/file_edit.php']['preOutputProcessingHook'] ?? [] as $hookFunction) {
220 $hookParameters = [
221 'content' => &$this->content,
222 'target' => &$this->target,
223 'dataColumnDefinition' => &$dataColumnDefinition,
224 ];
225 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
226 }
227
228 $assigns = [];
229 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
230 $assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
231 $assigns['fileName'] = $this->fileObject->getName();
232
233 $extList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'];
234 try {
235 if (!$extList || !GeneralUtility::inList($extList, $this->fileObject->getExtension())) {
236 // @todo throw a minor exception here, not the global one
237 throw new \Exception('Files with that extension are not editable. Allowed extensions are: ' . $extList, 1476050135);
238 }
239
240 // Making the formfields
241 $hValue = (string)$uriBuilder->buildUriFromRoute('file_edit', [
242 'target' => $this->origTarget,
243 'returnUrl' => $this->returnUrl
244 ]);
245
246 $formData = [
247 'databaseRow' => [
248 'uid' => 0,
249 'data' => $this->fileObject->getContents(),
250 'target' => $this->fileObject->getUid(),
251 'redirect' => $hValue,
252 ],
253 'tableName' => 'editfile',
254 'processedTca' => [
255 'columns' => [
256 'data' => $dataColumnDefinition,
257 'target' => [
258 'config' => [
259 'type' => 'input',
260 'renderType' => 'hidden',
261 ],
262 ],
263 'redirect' => [
264 'config' => [
265 'type' => 'input',
266 'renderType' => 'hidden',
267 ],
268 ],
269 ],
270 'types' => [
271 1 => [
272 'showitem' => 'data,target,redirect',
273 ],
274 ],
275 ],
276 'recordTypeValue' => 1,
277 'inlineStructure' => [],
278 'renderType' => 'fullRecordContainer',
279 ];
280
281 $resultArray = GeneralUtility::makeInstance(NodeFactory::class)->create($formData)->render();
282 $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
283 $formResultCompiler->mergeResult($resultArray);
284
285 $form = $formResultCompiler->addCssFiles()
286 . $resultArray['html']
287 . $formResultCompiler->printNeededJSFunctions();
288
289 $assigns['form'] = $form;
290 } catch (\Exception $e) {
291 // @todo catch dedicated exceptions, not the global one, if possible
292 $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', FlashMessage::ERROR, true);
293
294 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
295 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
296 $defaultFlashMessageQueue->enqueue($flashMessage);
297
298 if ($deprecatedCaller === true) {
299 HttpUtility::redirect($this->returnUrl, HttpUtility::HTTP_STATUS_500);
300 } else {
301 return new RedirectResponse($this->returnUrl, HttpUtility::HTTP_STATUS_500);
302 }
303 }
304
305 // Rendering of the output via fluid
306 $view = GeneralUtility::makeInstance(StandaloneView::class);
307 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
308 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
309 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
310 'EXT:backend/Resources/Private/Templates/File/EditFile.html'
311 ));
312 $view->assignMultiple($assigns);
313 $pageContent = $view->render();
314
315 // Hook: after compiling the output
316 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/file_edit.php']['postOutputProcessingHook'] ?? [] as $hookFunction) {
317 $hookParameters = [
318 'pageContent' => &$pageContent,
319 'target' => &$this->target
320 ];
321 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
322 }
323
324 $this->content .= $pageContent;
325 $this->moduleTemplate->setContent($this->content);
326 return null;
327 }
328
329 /**
330 * Builds the buttons for the docheader and returns them as an
331 *
332 * @deprecated since TYPO3 v9, will be set protected in TYPO3 v10
333 */
334 public function getButtons()
335 {
336 trigger_error('Method getButtons() will be replaced by protected method getButtonsInternal() in v10. Do not call from other extension', E_USER_DEPRECATED);
337 $this->getButtonsInternal();
338 }
339
340 /**
341 * Builds the buttons for the docheader
342 */
343 protected function getButtonsInternal(): void
344 {
345 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
346
347 $lang = $this->getLanguageService();
348 // CSH button
349 $helpButton = $buttonBar->makeHelpButton()
350 ->setFieldName('file_edit')
351 ->setModuleName('xMOD_csh_corebe');
352 $buttonBar->addButton($helpButton);
353
354 // Save button
355 $saveButton = $buttonBar->makeInputButton()
356 ->setName('_save')
357 ->setValue('1')
358 ->setForm('EditFileController')
359 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_edit.php.submit'))
360 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL));
361
362 // Save and Close button
363 $saveAndCloseButton = $buttonBar->makeInputButton()
364 ->setName('_saveandclosedok')
365 ->setValue('1')
366 ->setForm('EditFileController')
367 ->setOnClick(
368 'document.editform.elements.namedItem("data[editfile][0][redirect]").value='
369 . GeneralUtility::quoteJSvalue($this->returnUrl)
370 . ';'
371 )
372 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_edit.php.saveAndClose'))
373 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
374 'actions-document-save-close',
375 Icon::SIZE_SMALL
376 ));
377
378 $splitButton = $buttonBar->makeSplitButton()
379 ->addItem($saveButton)
380 ->addItem($saveAndCloseButton);
381 $buttonBar->addButton($splitButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
382
383 // Cancel button
384 $closeButton = $buttonBar->makeLinkButton()
385 ->setHref('#')
386 ->setOnClick('backToList(); return false;')
387 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'))
388 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
389 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
390
391 // Make shortcut:
392 $shortButton = $buttonBar->makeShortcutButton()
393 ->setModuleName('file_edit')
394 ->setGetVariables(['target']);
395 $buttonBar->addButton($shortButton);
396 }
397
398 /**
399 * Returns LanguageService
400 *
401 * @return LanguageService
402 */
403 protected function getLanguageService(): LanguageService
404 {
405 return $GLOBALS['LANG'];
406 }
407
408 /**
409 * Returns the current BE user.
410 *
411 * @return BackendUserAuthentication
412 */
413 protected function getBackendUser(): BackendUserAuthentication
414 {
415 return $GLOBALS['BE_USER'];
416 }
417 }