[!!!][TASK] Drop evaluation of @inject annotations
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Controller / FormManagerController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Controller;
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 Symfony\Component\Yaml\Yaml;
19 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Backend\View\BackendTemplateView;
22 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23 use TYPO3\CMS\Core\Imaging\Icon;
24 use TYPO3\CMS\Core\Imaging\IconFactory;
25 use TYPO3\CMS\Core\Localization\LanguageService;
26 use TYPO3\CMS\Core\Messaging\AbstractMessage;
27 use TYPO3\CMS\Core\Page\PageRenderer;
28 use TYPO3\CMS\Core\Utility\ArrayUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
31 use TYPO3\CMS\Form\Exception as FormException;
32 use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
33 use TYPO3\CMS\Form\Service\DatabaseService;
34 use TYPO3\CMS\Form\Service\TranslationService;
35
36 /**
37 * The form manager controller
38 *
39 * Scope: backend
40 * @internal
41 */
42 class FormManagerController extends AbstractBackendController
43 {
44
45 /**
46 * @var DatabaseService
47 */
48 protected $databaseService;
49
50 /**
51 * @param \TYPO3\CMS\Form\Service\DatabaseService $databaseService
52 * @internal
53 */
54 public function injectDatabaseService(\TYPO3\CMS\Form\Service\DatabaseService $databaseService)
55 {
56 $this->databaseService = $databaseService;
57 }
58
59 /**
60 * Default View Container
61 *
62 * @var BackendTemplateView
63 */
64 protected $defaultViewObjectName = BackendTemplateView::class;
65
66 /**
67 * Displays the Form Manager
68 *
69 * @internal
70 */
71 public function indexAction()
72 {
73 $this->registerDocheaderButtons();
74 $this->view->getModuleTemplate()->setModuleName($this->request->getPluginName() . '_' . $this->request->getControllerName());
75 $this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
76
77 $this->view->assign('forms', $this->getAvailableFormDefinitions());
78 $this->view->assign('stylesheets', $this->resolveResourcePaths($this->formSettings['formManager']['stylesheets']));
79 $this->view->assign('dynamicRequireJsModules', $this->formSettings['formManager']['dynamicRequireJsModules']);
80 $this->view->assign('formManagerAppInitialData', $this->getFormManagerAppInitialData());
81 if (!empty($this->formSettings['formManager']['javaScriptTranslationFile'])) {
82 $this->getPageRenderer()->addInlineLanguageLabelFile($this->formSettings['formManager']['javaScriptTranslationFile']);
83 }
84 }
85
86 /**
87 * Initialize the create action.
88 * This action uses the Fluid JsonView::class as view.
89 *
90 * @internal
91 */
92 public function initializeCreateAction()
93 {
94 $this->defaultViewObjectName = JsonView::class;
95 }
96
97 /**
98 * Creates a new Form and redirects to the Form Editor
99 *
100 * @param string $formName
101 * @param string $templatePath
102 * @param string $prototypeName
103 * @param string $savePath
104 * @throws FormException
105 * @internal
106 */
107 public function createAction(string $formName, string $templatePath, string $prototypeName, string $savePath)
108 {
109 if (!$this->isValidTemplatePath($prototypeName, $templatePath)) {
110 throw new FormException(sprintf('The template path "%s" is not allowed', $templatePath), 1329233410);
111 }
112 if (empty($formName)) {
113 throw new FormException('No form name', 1472312204);
114 }
115
116 $templatePath = GeneralUtility::getFileAbsFileName($templatePath);
117 $form = Yaml::parse(file_get_contents($templatePath));
118 $form['label'] = $formName;
119 $form['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
120 $form['prototypeName'] = $prototypeName;
121
122 $formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($form['identifier'], $savePath);
123
124 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] ?? [] as $className) {
125 $hookObj = GeneralUtility::makeInstance($className);
126 if (method_exists($hookObj, 'beforeFormCreate')) {
127 $form = $hookObj->beforeFormCreate(
128 $formPersistenceIdentifier,
129 $form
130 );
131 }
132 }
133
134 $response = [
135 'status' => 'success',
136 'url' => $this->controllerContext->getUriBuilder()->uriFor('index', ['formPersistenceIdentifier' => $formPersistenceIdentifier], 'FormEditor')
137 ];
138
139 try {
140 $this->formPersistenceManager->save($formPersistenceIdentifier, $form);
141 } catch (PersistenceManagerException $e) {
142 $response = [
143 'status' => 'error',
144 'message' => $e->getMessage(),
145 'code' => $e->getCode(),
146 ];
147 }
148
149 $this->view->assign('response', $response);
150 // createAction uses the Extbase JsonView::class.
151 // That's why we have to set the view variables in this way.
152 $this->view->setVariablesToRender([
153 'response',
154 ]);
155 }
156
157 /**
158 * Initialize the duplicate action.
159 * This action uses the Fluid JsonView::class as view.
160 *
161 * @internal
162 */
163 public function initializeDuplicateAction()
164 {
165 $this->defaultViewObjectName = JsonView::class;
166 }
167
168 /**
169 * Duplicates a given formDefinition and redirects to the Form Editor
170 *
171 * @param string $formName
172 * @param string $formPersistenceIdentifier persistence identifier of the form to duplicate
173 * @param string $savePath
174 * @internal
175 */
176 public function duplicateAction(string $formName, string $formPersistenceIdentifier, string $savePath)
177 {
178 $formToDuplicate = $this->formPersistenceManager->load($formPersistenceIdentifier);
179 $formToDuplicate['label'] = $formName;
180 $formToDuplicate['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
181
182 $formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($formToDuplicate['identifier'], $savePath);
183
184 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDuplicate'] ?? [] as $className) {
185 $hookObj = GeneralUtility::makeInstance($className);
186 if (method_exists($hookObj, 'beforeFormDuplicate')) {
187 $formToDuplicate = $hookObj->beforeFormDuplicate(
188 $formPersistenceIdentifier,
189 $formToDuplicate
190 );
191 }
192 }
193
194 $response = [
195 'status' => 'success',
196 'url' => $this->controllerContext->getUriBuilder()->uriFor('index', ['formPersistenceIdentifier' => $formPersistenceIdentifier], 'FormEditor')
197 ];
198
199 try {
200 $this->formPersistenceManager->save($formPersistenceIdentifier, $formToDuplicate);
201 } catch (PersistenceManagerException $e) {
202 $response = [
203 'status' => 'error',
204 'message' => $e->getMessage(),
205 'code' => $e->getCode(),
206 ];
207 }
208
209 $this->view->assign('response', $response);
210 // createAction uses the Extbase JsonView::class.
211 // That's why we have to set the view variables in this way.
212 $this->view->setVariablesToRender([
213 'response',
214 ]);
215 }
216
217 /**
218 * Initialize the references action.
219 * This action uses the Fluid JsonView::class as view.
220 *
221 * @internal
222 */
223 public function initializeReferencesAction()
224 {
225 $this->defaultViewObjectName = JsonView::class;
226 }
227
228 /**
229 * Show references to this persistence identifier
230 *
231 * @param string $formPersistenceIdentifier persistence identifier of the form to duplicate
232 * @internal
233 */
234 public function referencesAction(string $formPersistenceIdentifier)
235 {
236 $this->view->assign('references', $this->getProcessedReferencesRows($formPersistenceIdentifier));
237 $this->view->assign('formPersistenceIdentifier', $formPersistenceIdentifier);
238 // referencesAction uses the extbase JsonView::class.
239 // That's why we have to set the view variables in this way.
240 $this->view->setVariablesToRender([
241 'references',
242 'formPersistenceIdentifier'
243 ]);
244 }
245
246 /**
247 * Delete a formDefinition identified by the $formPersistenceIdentifier.
248 *
249 * @param string $formPersistenceIdentifier persistence identifier to delete
250 * @internal
251 */
252 public function deleteAction(string $formPersistenceIdentifier)
253 {
254 if (empty($this->databaseService->getReferencesByPersistenceIdentifier($formPersistenceIdentifier))) {
255 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] ?? [] as $className) {
256 $hookObj = GeneralUtility::makeInstance($className);
257 if (method_exists($hookObj, 'beforeFormDelete')) {
258 $hookObj->beforeFormDelete(
259 $formPersistenceIdentifier
260 );
261 }
262 }
263
264 $this->formPersistenceManager->delete($formPersistenceIdentifier);
265 } else {
266 $controllerConfiguration = TranslationService::getInstance()->translateValuesRecursive(
267 $this->formSettings['formManager']['controller'],
268 $this->formSettings['formManager']['translationFile']
269 );
270
271 $this->addFlashMessage(
272 sprintf($controllerConfiguration['deleteAction']['errorMessage'], $formPersistenceIdentifier),
273 $controllerConfiguration['deleteAction']['errorTitle'],
274 AbstractMessage::ERROR,
275 true
276 );
277 }
278 $this->redirect('index');
279 }
280
281 /**
282 * Return a list of all accessible file mountpoints.
283 *
284 * Only registered mountpoints from
285 * TYPO3.CMS.Form.persistenceManager.allowedFileMounts
286 * are listet. This is list will be reduced by the configured
287 * mountpoints for the current backend user.
288 *
289 * @return array
290 */
291 protected function getAccessibleFormStorageFolders(): array
292 {
293 $preparedAccessibleFormStorageFolders = [];
294 foreach ($this->formPersistenceManager->getAccessibleFormStorageFolders() as $identifier => $folder) {
295 // TODO: deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
296 if ($folder->getCombinedIdentifier() === '1:/user_upload/') {
297 continue;
298 }
299
300 $preparedAccessibleFormStorageFolders[] = [
301 'label' => $folder->getName(),
302 'value' => $identifier
303 ];
304 }
305
306 if ($this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
307 foreach ($this->formPersistenceManager->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
308 $preparedAccessibleFormStorageFolders[] = [
309 'label' => $relativePath,
310 'value' => $relativePath
311 ];
312 }
313 }
314
315 return $preparedAccessibleFormStorageFolders;
316 }
317
318 /**
319 * Returns the json encoded data which is used by the form editor
320 * JavaScript app.
321 *
322 * @return string
323 */
324 protected function getFormManagerAppInitialData(): string
325 {
326 $formManagerAppInitialData = [
327 'selectablePrototypesConfiguration' => $this->formSettings['formManager']['selectablePrototypesConfiguration'],
328 'accessibleFormStorageFolders' => $this->getAccessibleFormStorageFolders(),
329 'endpoints' => [
330 'create' => $this->controllerContext->getUriBuilder()->uriFor('create'),
331 'duplicate' => $this->controllerContext->getUriBuilder()->uriFor('duplicate'),
332 'delete' => $this->controllerContext->getUriBuilder()->uriFor('delete'),
333 'references' => $this->controllerContext->getUriBuilder()->uriFor('references')
334 ],
335 ];
336
337 $formManagerAppInitialData = ArrayUtility::reIndexNumericArrayKeysRecursive($formManagerAppInitialData);
338 $formManagerAppInitialData = TranslationService::getInstance()->translateValuesRecursive(
339 $formManagerAppInitialData,
340 $this->formSettings['formManager']['translationFile'] ?? null
341 );
342 return json_encode($formManagerAppInitialData);
343 }
344
345 /**
346 * List all formDefinitions which can be loaded through t form persistence
347 * manager. Enrich this data by a reference counter.
348 * @return array
349 */
350 protected function getAvailableFormDefinitions(): array
351 {
352 $allReferencesForFileUid = $this->databaseService->getAllReferencesForFileUid();
353 $allReferencesForPersistenceIdentifier = $this->databaseService->getAllReferencesForPersistenceIdentifier();
354
355 $availableFormDefinitions = [];
356 foreach ($this->formPersistenceManager->listForms() as $formDefinition) {
357 $referenceCount = 0;
358 if (
359 isset($formDefinition['fileUid'])
360 && array_key_exists($formDefinition['fileUid'], $allReferencesForFileUid)
361 ) {
362 $referenceCount = $allReferencesForFileUid[$formDefinition['fileUid']];
363 } elseif (array_key_exists($formDefinition['persistenceIdentifier'], $allReferencesForPersistenceIdentifier)) {
364 $referenceCount = $allReferencesForPersistenceIdentifier[$formDefinition['persistenceIdentifier']];
365 }
366
367 $formDefinition['referenceCount'] = $referenceCount;
368 $availableFormDefinitions[] = $formDefinition;
369 }
370
371 return $availableFormDefinitions;
372 }
373
374 /**
375 * Returns an array with informations about the references for a
376 * formDefinition identified by $persistenceIdentifier.
377 *
378 * @param string $persistenceIdentifier
379 * @return array
380 * @throws \InvalidArgumentException
381 */
382 protected function getProcessedReferencesRows(string $persistenceIdentifier): array
383 {
384 if (empty($persistenceIdentifier)) {
385 throw new \InvalidArgumentException('$persistenceIdentifier must not be empty.', 1477071939);
386 }
387
388 $references = [];
389 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
390
391 $referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($persistenceIdentifier);
392 foreach ($referenceRows as &$referenceRow) {
393 $record = $this->getRecord($referenceRow['tablename'], $referenceRow['recuid']);
394 if (!$record) {
395 continue;
396 }
397 $pageRecord = $this->getRecord('pages', $record['pid']);
398 $urlParameters = [
399 'edit' => [
400 $referenceRow['tablename'] => [
401 $referenceRow['recuid'] => 'edit'
402 ]
403 ],
404 'returnUrl' => $this->getModuleUrl('web_FormFormbuilder')
405 ];
406
407 $references[] = [
408 'recordPageTitle' => is_array($pageRecord) ? $this->getRecordTitle('pages', $pageRecord) : '',
409 'recordTitle' => $this->getRecordTitle($referenceRow['tablename'], $record, true),
410 'recordIcon' => $iconFactory->getIconForRecord($referenceRow['tablename'], $record, Icon::SIZE_SMALL)->render(),
411 'recordUid' => $referenceRow['recuid'],
412 'recordEditUrl' => $this->getModuleUrl('record_edit', $urlParameters),
413 ];
414 }
415 return $references;
416 }
417
418 /**
419 * Check if a given $templatePath for a given $prototypeName is valid
420 * and accessible.
421 *
422 * Valid template paths has to be configured within
423 * TYPO3.CMS.Form.formManager.selectablePrototypesConfiguration.[('identifier': $prototypeName)].newFormTemplates.[('templatePath': $templatePath)]
424 *
425 * @param string $prototypeName
426 * @param string $templatePath
427 * @return bool
428 */
429 protected function isValidTemplatePath(string $prototypeName, string $templatePath): bool
430 {
431 $isValid = false;
432 foreach ($this->formSettings['formManager']['selectablePrototypesConfiguration'] as $prototypesConfiguration) {
433 if ($prototypesConfiguration['identifier'] !== $prototypeName) {
434 continue;
435 }
436 foreach ($prototypesConfiguration['newFormTemplates'] as $templatesConfiguration) {
437 if ($templatesConfiguration['templatePath'] !== $templatePath) {
438 continue;
439 }
440 $isValid = true;
441 break;
442 }
443 }
444
445 $templatePath = GeneralUtility::getFileAbsFileName($templatePath);
446 if (!is_file($templatePath)) {
447 $isValid = false;
448 }
449
450 return $isValid;
451 }
452
453 /**
454 * Register document header buttons
455 *
456 * @throws \InvalidArgumentException
457 */
458 protected function registerDocheaderButtons()
459 {
460 /** @var ButtonBar $buttonBar */
461 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
462 $currentRequest = $this->request;
463 $moduleName = $currentRequest->getPluginName();
464 $getVars = $this->request->getArguments();
465
466 // Create new
467 $addFormButton = $buttonBar->makeLinkButton()
468 ->setDataAttributes(['identifier' => 'newForm'])
469 ->setHref('#')
470 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.create_new_form'))
471 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-add', Icon::SIZE_SMALL));
472 $buttonBar->addButton($addFormButton, ButtonBar::BUTTON_POSITION_LEFT);
473
474 // Reload
475 $reloadButton = $buttonBar->makeLinkButton()
476 ->setHref(GeneralUtility::getIndpEnv('REQUEST_URI'))
477 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
478 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL));
479 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
480
481 // Shortcut
482 $mayMakeShortcut = $this->getBackendUser()->mayMakeShortcut();
483 if ($mayMakeShortcut) {
484 $extensionName = $currentRequest->getControllerExtensionName();
485 if (count($getVars) === 0) {
486 $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
487 $getVars = ['id', 'route', $modulePrefix];
488 }
489
490 $shortcutButton = $buttonBar->makeShortcutButton()
491 ->setModuleName($moduleName)
492 ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:module.shortcut_name'))
493 ->setGetVariables($getVars);
494 $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
495 }
496 }
497
498 /**
499 * Returns a form identifier which is the lower cased form name.
500 *
501 * @param string $formName
502 * @return string
503 */
504 protected function convertFormNameToIdentifier(string $formName): string
505 {
506 $formIdentifier = preg_replace('/[^a-zA-Z0-9-_]/', '', $formName);
507 $formIdentifier = lcfirst($formIdentifier);
508 return $formIdentifier;
509 }
510
511 /**
512 * Wrapper used for unit testing.
513 *
514 * @param string $table
515 * @param int $uid
516 * @return array|null
517 */
518 protected function getRecord(string $table, int $uid)
519 {
520 return BackendUtility::getRecord($table, $uid);
521 }
522
523 /**
524 * Wrapper used for unit testing.
525 *
526 * @param string $table
527 * @param array $row
528 * @param bool $prep
529 * @return string
530 */
531 protected function getRecordTitle(string $table, array $row, bool $prep = false): string
532 {
533 return BackendUtility::getRecordTitle($table, $row, $prep);
534 }
535
536 /**
537 * Wrapper used for unit testing.
538 *
539 * @param string $moduleName
540 * @param array $urlParameters
541 * @return string
542 */
543 protected function getModuleUrl(string $moduleName, array $urlParameters = []): string
544 {
545 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
546 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
547 return (string)$uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
548 }
549
550 /**
551 * Returns the current BE user.
552 *
553 * @return BackendUserAuthentication
554 */
555 protected function getBackendUser(): BackendUserAuthentication
556 {
557 return $GLOBALS['BE_USER'];
558 }
559
560 /**
561 * Returns the Language Service
562 *
563 * @return LanguageService
564 */
565 protected function getLanguageService(): LanguageService
566 {
567 return $GLOBALS['LANG'];
568 }
569
570 /**
571 * Returns the page renderer
572 *
573 * @return PageRenderer
574 */
575 protected function getPageRenderer(): PageRenderer
576 {
577 return GeneralUtility::makeInstance(PageRenderer::class);
578 }
579 }