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