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