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