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