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