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