[TASK] Update Settings.cfg to recent TYPO3 version
[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 $preparedAccessibleFormStorageFolders[] = [
296 'label' => $folder->getName(),
297 'value' => $identifier
298 ];
299 }
300
301 if ($this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
302 foreach ($this->formPersistenceManager->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
303 $preparedAccessibleFormStorageFolders[] = [
304 'label' => $relativePath,
305 'value' => $relativePath
306 ];
307 }
308 }
309
310 return $preparedAccessibleFormStorageFolders;
311 }
312
313 /**
314 * Returns the json encoded data which is used by the form editor
315 * JavaScript app.
316 *
317 * @return string
318 */
319 protected function getFormManagerAppInitialData(): string
320 {
321 $formManagerAppInitialData = [
322 'selectablePrototypesConfiguration' => $this->formSettings['formManager']['selectablePrototypesConfiguration'],
323 'accessibleFormStorageFolders' => $this->getAccessibleFormStorageFolders(),
324 'endpoints' => [
325 'create' => $this->controllerContext->getUriBuilder()->uriFor('create'),
326 'duplicate' => $this->controllerContext->getUriBuilder()->uriFor('duplicate'),
327 'delete' => $this->controllerContext->getUriBuilder()->uriFor('delete'),
328 'references' => $this->controllerContext->getUriBuilder()->uriFor('references')
329 ],
330 ];
331
332 $formManagerAppInitialData = ArrayUtility::reIndexNumericArrayKeysRecursive($formManagerAppInitialData);
333 $formManagerAppInitialData = TranslationService::getInstance()->translateValuesRecursive(
334 $formManagerAppInitialData,
335 $this->formSettings['formManager']['translationFile'] ?? null
336 );
337 return json_encode($formManagerAppInitialData);
338 }
339
340 /**
341 * List all formDefinitions which can be loaded through t form persistence
342 * manager. Enrich this data by a reference counter.
343 * @return array
344 */
345 protected function getAvailableFormDefinitions(): array
346 {
347 $allReferencesForFileUid = $this->databaseService->getAllReferencesForFileUid();
348 $allReferencesForPersistenceIdentifier = $this->databaseService->getAllReferencesForPersistenceIdentifier();
349
350 $availableFormDefinitions = [];
351 foreach ($this->formPersistenceManager->listForms() as $formDefinition) {
352 $referenceCount = 0;
353 if (
354 isset($formDefinition['fileUid'])
355 && array_key_exists($formDefinition['fileUid'], $allReferencesForFileUid)
356 ) {
357 $referenceCount = $allReferencesForFileUid[$formDefinition['fileUid']];
358 } elseif (array_key_exists($formDefinition['persistenceIdentifier'], $allReferencesForPersistenceIdentifier)) {
359 $referenceCount = $allReferencesForPersistenceIdentifier[$formDefinition['persistenceIdentifier']];
360 }
361
362 $formDefinition['referenceCount'] = $referenceCount;
363 $availableFormDefinitions[] = $formDefinition;
364 }
365
366 return $availableFormDefinitions;
367 }
368
369 /**
370 * Returns an array with informations about the references for a
371 * formDefinition identified by $persistenceIdentifier.
372 *
373 * @param string $persistenceIdentifier
374 * @return array
375 * @throws \InvalidArgumentException
376 */
377 protected function getProcessedReferencesRows(string $persistenceIdentifier): array
378 {
379 if (empty($persistenceIdentifier)) {
380 throw new \InvalidArgumentException('$persistenceIdentifier must not be empty.', 1477071939);
381 }
382
383 $references = [];
384 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
385
386 $referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($persistenceIdentifier);
387 foreach ($referenceRows as &$referenceRow) {
388 $record = $this->getRecord($referenceRow['tablename'], $referenceRow['recuid']);
389 if (!$record) {
390 continue;
391 }
392 $pageRecord = $this->getRecord('pages', $record['pid']);
393 $urlParameters = [
394 'edit' => [
395 $referenceRow['tablename'] => [
396 $referenceRow['recuid'] => 'edit'
397 ]
398 ],
399 'returnUrl' => $this->getModuleUrl('web_FormFormbuilder')
400 ];
401
402 $references[] = [
403 'recordPageTitle' => is_array($pageRecord) ? $this->getRecordTitle('pages', $pageRecord) : '',
404 'recordTitle' => $this->getRecordTitle($referenceRow['tablename'], $record, true),
405 'recordIcon' => $iconFactory->getIconForRecord($referenceRow['tablename'], $record, Icon::SIZE_SMALL)->render(),
406 'recordUid' => $referenceRow['recuid'],
407 'recordEditUrl' => $this->getModuleUrl('record_edit', $urlParameters),
408 ];
409 }
410 return $references;
411 }
412
413 /**
414 * Check if a given $templatePath for a given $prototypeName is valid
415 * and accessible.
416 *
417 * Valid template paths has to be configured within
418 * TYPO3.CMS.Form.formManager.selectablePrototypesConfiguration.[('identifier': $prototypeName)].newFormTemplates.[('templatePath': $templatePath)]
419 *
420 * @param string $prototypeName
421 * @param string $templatePath
422 * @return bool
423 */
424 protected function isValidTemplatePath(string $prototypeName, string $templatePath): bool
425 {
426 $isValid = false;
427 foreach ($this->formSettings['formManager']['selectablePrototypesConfiguration'] as $prototypesConfiguration) {
428 if ($prototypesConfiguration['identifier'] !== $prototypeName) {
429 continue;
430 }
431 foreach ($prototypesConfiguration['newFormTemplates'] as $templatesConfiguration) {
432 if ($templatesConfiguration['templatePath'] !== $templatePath) {
433 continue;
434 }
435 $isValid = true;
436 break;
437 }
438 }
439
440 $templatePath = GeneralUtility::getFileAbsFileName($templatePath);
441 if (!is_file($templatePath)) {
442 $isValid = false;
443 }
444
445 return $isValid;
446 }
447
448 /**
449 * Register document header buttons
450 *
451 * @throws \InvalidArgumentException
452 */
453 protected function registerDocheaderButtons()
454 {
455 /** @var ButtonBar $buttonBar */
456 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
457 $currentRequest = $this->request;
458 $moduleName = $currentRequest->getPluginName();
459 $getVars = $this->request->getArguments();
460
461 // Create new
462 $addFormButton = $buttonBar->makeLinkButton()
463 ->setDataAttributes(['identifier' => 'newForm'])
464 ->setHref('#')
465 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.create_new_form'))
466 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-add', Icon::SIZE_SMALL));
467 $buttonBar->addButton($addFormButton, ButtonBar::BUTTON_POSITION_LEFT);
468
469 // Reload
470 $reloadButton = $buttonBar->makeLinkButton()
471 ->setHref(GeneralUtility::getIndpEnv('REQUEST_URI'))
472 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
473 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL));
474 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
475
476 // Shortcut
477 $mayMakeShortcut = $this->getBackendUser()->mayMakeShortcut();
478 if ($mayMakeShortcut) {
479 $extensionName = $currentRequest->getControllerExtensionName();
480 if (count($getVars) === 0) {
481 $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
482 $getVars = ['id', 'route', $modulePrefix];
483 }
484
485 $shortcutButton = $buttonBar->makeShortcutButton()
486 ->setModuleName($moduleName)
487 ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:module.shortcut_name'))
488 ->setGetVariables($getVars);
489 $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
490 }
491 }
492
493 /**
494 * Returns a form identifier which is the lower cased form name.
495 *
496 * @param string $formName
497 * @return string
498 */
499 protected function convertFormNameToIdentifier(string $formName): string
500 {
501 $formIdentifier = preg_replace('/[^a-zA-Z0-9-_]/', '', $formName);
502 $formIdentifier = lcfirst($formIdentifier);
503 return $formIdentifier;
504 }
505
506 /**
507 * Wrapper used for unit testing.
508 *
509 * @param string $table
510 * @param int $uid
511 * @return array|null
512 */
513 protected function getRecord(string $table, int $uid)
514 {
515 return BackendUtility::getRecord($table, $uid);
516 }
517
518 /**
519 * Wrapper used for unit testing.
520 *
521 * @param string $table
522 * @param array $row
523 * @param bool $prep
524 * @return string
525 */
526 protected function getRecordTitle(string $table, array $row, bool $prep = false): string
527 {
528 return BackendUtility::getRecordTitle($table, $row, $prep);
529 }
530
531 /**
532 * Wrapper used for unit testing.
533 *
534 * @param string $moduleName
535 * @param array $urlParameters
536 * @return string
537 */
538 protected function getModuleUrl(string $moduleName, array $urlParameters = []): string
539 {
540 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
541 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
542 return (string)$uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
543 }
544
545 /**
546 * Returns the current BE user.
547 *
548 * @return BackendUserAuthentication
549 */
550 protected function getBackendUser(): BackendUserAuthentication
551 {
552 return $GLOBALS['BE_USER'];
553 }
554
555 /**
556 * Returns the Language Service
557 *
558 * @return LanguageService
559 */
560 protected function getLanguageService(): LanguageService
561 {
562 return $GLOBALS['LANG'];
563 }
564
565 /**
566 * Returns the page renderer
567 *
568 * @return PageRenderer
569 */
570 protected function getPageRenderer(): PageRenderer
571 {
572 return GeneralUtility::makeInstance(PageRenderer::class);
573 }
574 }