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