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