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