[TASK] Use BE Routing / PSR-7 instead of BackendUtility::getModuleUrl
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Controller / FormEditorController.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 TYPO3\CMS\Backend\Template\Components\ButtonBar;
19 use TYPO3\CMS\Backend\View\BackendTemplateView;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Imaging\Icon;
22 use TYPO3\CMS\Core\Localization\LanguageService;
23 use TYPO3\CMS\Core\Utility\ArrayUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Fluid\View\TemplateView;
26 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
27 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
28 use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
29 use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
30 use TYPO3\CMS\Form\Service\TranslationService;
31
32 /**
33 * The form editor controller
34 *
35 * Scope: backend
36 */
37 class FormEditorController extends AbstractBackendController
38 {
39
40 /**
41 * Default View Container
42 *
43 * @var BackendTemplateView
44 */
45 protected $defaultViewObjectName = BackendTemplateView::class;
46
47 /**
48 * @var array
49 */
50 protected $prototypeConfiguration;
51
52 /**
53 * Displays the form editor
54 *
55 * @param string $formPersistenceIdentifier
56 * @param string $prototypeName
57 * @throws PersistenceManagerException
58 * @internal
59 */
60 public function indexAction(string $formPersistenceIdentifier, string $prototypeName = null)
61 {
62 $this->registerDocheaderButtons();
63 $this->view->getModuleTemplate()->setModuleName($this->request->getPluginName() . '_' . $this->request->getControllerName());
64 $this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
65
66 if (
67 strpos($formPersistenceIdentifier, 'EXT:') === 0
68 && !$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']
69 ) {
70 throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
71 }
72
73 $formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
74 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
75 if (empty($prototypeName)) {
76 $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
77 }
78 $formDefinition['prototypeName'] = $prototypeName;
79
80 $configurationService = $this->objectManager->get(ConfigurationService::class);
81 $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
82
83 $formEditorDefinitions = $this->getFormEditorDefinitions();
84
85 $formEditorAppInitialData = [
86 'formEditorDefinitions' => $formEditorDefinitions,
87 'formDefinition' => $formDefinition,
88 'formPersistenceIdentifier' => $formPersistenceIdentifier,
89 'prototypeName' => $prototypeName,
90 'endpoints' => [
91 'formPageRenderer' => $this->controllerContext->getUriBuilder()->uriFor('renderFormPage'),
92 'saveForm' => $this->controllerContext->getUriBuilder()->uriFor('saveForm')
93 ],
94 'additionalViewModelModules' => $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']['additionalViewModelModules'],
95 'maximumUndoSteps' => $this->prototypeConfiguration['formEditor']['maximumUndoSteps'],
96 ];
97
98 $this->view->assign('formEditorAppInitialData', json_encode($formEditorAppInitialData));
99 $this->view->assign('stylesheets', $this->resolveResourcePaths($this->prototypeConfiguration['formEditor']['stylesheets']));
100 $this->view->assign('formEditorTemplates', $this->renderFormEditorTemplates($formEditorDefinitions));
101 $this->view->assign('dynamicRequireJsModules', $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']);
102
103 $popupWindowWidth = 700;
104 $popupWindowHeight = 750;
105 $popupWindowSize = ($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
106 ? trim($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
107 : null;
108 if (!empty($popupWindowSize)) {
109 list($popupWindowWidth, $popupWindowHeight) = GeneralUtility::intExplode('x', $popupWindowSize);
110 }
111 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
112 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
113 $addInlineSettings = [
114 'FormEditor' => [
115 'typo3WinBrowserUrl' => (string)$uriBuilder->buildUriFromRoute('wizard_element_browser'),
116 ],
117 'Popup' => [
118 'PopupWindow' => [
119 'width' => $popupWindowWidth,
120 'height' => $popupWindowHeight
121 ],
122 ]
123 ];
124
125 $addInlineSettings = array_replace_recursive(
126 $addInlineSettings,
127 $this->prototypeConfiguration['formEditor']['addInlineSettings']
128 );
129 $this->view->assign('addInlineSettings', $addInlineSettings);
130 }
131
132 /**
133 * Save a formDefinition which was build by the form editor.
134 *
135 * @param string $formPersistenceIdentifier
136 * @param array $formDefinition
137 * @return string
138 * @internal
139 */
140 public function saveFormAction(string $formPersistenceIdentifier, array $formDefinition): string
141 {
142 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
143 $formDefinition = $this->convertJsonArrayToAssociativeArray($formDefinition);
144
145 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] ?? [] as $className) {
146 $hookObj = GeneralUtility::makeInstance($className);
147 if (method_exists($hookObj, 'beforeFormSave')) {
148 $formDefinition = $hookObj->beforeFormSave(
149 $formPersistenceIdentifier,
150 $formDefinition
151 );
152 }
153 }
154
155 $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
156 return '';
157 }
158
159 /**
160 * Render a page from the formDefinition which was build by the form editor.
161 * Use the frontend rendering and set the form framework to preview mode.
162 *
163 * @param array $formDefinition
164 * @param int $pageIndex
165 * @param string $prototypeName
166 * @return string
167 * @internal
168 */
169 public function renderFormPageAction(array $formDefinition, int $pageIndex, string $prototypeName = null): string
170 {
171 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
172 $formDefinition = $this->convertJsonArrayToAssociativeArray($formDefinition);
173 if (empty($prototypeName)) {
174 $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
175 }
176
177 $formFactory = $this->objectManager->get(ArrayFormFactory::class);
178 $formDefinition = $formFactory->build($formDefinition, $prototypeName);
179 $formDefinition->setRenderingOption('previewMode', true);
180 $form = $formDefinition->bind($this->request, $this->response);
181 $form->overrideCurrentPage($pageIndex);
182 return $form->render();
183 }
184
185 /**
186 * Prepare the formElements.*.formEditor section from the YAML settings.
187 * Sort all formElements into groups and add additional data.
188 *
189 * @param array $formElementsDefinition
190 * @return array
191 */
192 protected function getInsertRenderablesPanelConfiguration(array $formElementsDefinition): array
193 {
194 $formElementGroups = isset($this->prototypeConfiguration['formEditor']['formElementGroups']) ? $this->prototypeConfiguration['formEditor']['formElementGroups'] : [];
195 $formElementsByGroup = [];
196
197 foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
198 if (!isset($formElementConfiguration['group'])) {
199 continue;
200 }
201 if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
202 $formElementsByGroup[$formElementConfiguration['group']] = [];
203 }
204
205 $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
206 $formElementConfiguration,
207 $this->prototypeConfiguration['formEditor']['translationFile']
208 );
209
210 $formElementsByGroup[$formElementConfiguration['group']][] = [
211 'key' => $formElementName,
212 'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
213 'label' => $formElementConfiguration['label'],
214 'sorting' => $formElementConfiguration['groupSorting'],
215 'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
216 ];
217 }
218
219 $formGroups = [];
220 foreach ($formElementGroups as $groupName => $groupConfiguration) {
221 if (!isset($formElementsByGroup[$groupName])) {
222 continue;
223 }
224
225 usort($formElementsByGroup[$groupName], function ($a, $b) {
226 return $a['sorting'] - $b['sorting'];
227 });
228 unset($formElementsByGroup[$groupName]['sorting']);
229
230 $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
231 $groupConfiguration,
232 $this->prototypeConfiguration['formEditor']['translationFile']
233 );
234
235 $formGroups[] = [
236 'key' => $groupName,
237 'elements' => $formElementsByGroup[$groupName],
238 'label' => $groupConfiguration['label'],
239 ];
240 }
241
242 return $formGroups;
243 }
244
245 /**
246 * Reduce the YAML settings by the 'formEditor' keyword.
247 *
248 * @return array
249 */
250 protected function getFormEditorDefinitions(): array
251 {
252 $formEditorDefinitions = [];
253 foreach ([$this->prototypeConfiguration, $this->prototypeConfiguration['formEditor']] as $configuration) {
254 foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
255 if (substr($firstLevelItemKey, -10) !== 'Definition') {
256 continue;
257 }
258 $reducedKey = substr($firstLevelItemKey, 0, -10);
259 foreach ($configuration[$firstLevelItemKey] as $formEditorDefinitionKey => $formEditorDefinitionValue) {
260 if (isset($formEditorDefinitionValue['formEditor'])) {
261 $formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
262 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
263 } else {
264 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
265 }
266 }
267 }
268 }
269 $formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
270 $formEditorDefinitions = TranslationService::getInstance()->translateValuesRecursive(
271 $formEditorDefinitions,
272 $this->prototypeConfiguration['formEditor']['translationFile']
273 );
274 return $formEditorDefinitions;
275 }
276
277 /**
278 * Registers the Icons into the docheader
279 *
280 * @throws \InvalidArgumentException
281 */
282 protected function registerDocheaderButtons()
283 {
284 /** @var ButtonBar $buttonBar */
285 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
286 $getVars = $this->request->getArguments();
287
288 if (isset($getVars['action']) && $getVars['action'] === 'index') {
289 $newPageButton = $buttonBar->makeInputButton()
290 ->setDataAttributes(['action' => 'formeditor-new-page', 'identifier' => 'headerNewPage'])
291 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.new_page_button'))
292 ->setName('formeditor-new-page')
293 ->setValue('new-page')
294 ->setClasses('t3-form-element-new-page-button hidden')
295 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
296 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
297 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
298
299 $closeButton = $buttonBar->makeLinkButton()
300 ->setDataAttributes(['identifier' => 'closeButton'])
301 ->setHref((string)$uriBuilder->buildUriFromRoute('web_FormFormbuilder'))
302 ->setClasses('t3-form-element-close-form-button hidden')
303 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
304 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
305
306 $saveButton = $buttonBar->makeInputButton()
307 ->setDataAttributes(['identifier' => 'saveButton'])
308 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
309 ->setName('formeditor-save-form')
310 ->setValue('save')
311 ->setClasses('t3-form-element-save-form-button hidden')
312 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
313 ->setShowLabelText(true);
314
315 $formSettingsButton = $buttonBar->makeInputButton()
316 ->setDataAttributes(['identifier' => 'formSettingsButton'])
317 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
318 ->setName('formeditor-form-settings')
319 ->setValue('settings')
320 ->setClasses('t3-form-element-form-settings-button hidden')
321 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
322 ->setShowLabelText(true);
323
324 $undoButton = $buttonBar->makeInputButton()
325 ->setDataAttributes(['identifier' => 'undoButton'])
326 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
327 ->setName('formeditor-undo-form')
328 ->setValue('undo')
329 ->setClasses('t3-form-element-undo-form-button hidden disabled')
330 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
331
332 $redoButton = $buttonBar->makeInputButton()
333 ->setDataAttributes(['identifier' => 'redoButton'])
334 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
335 ->setName('formeditor-redo-form')
336 ->setValue('redo')
337 ->setClasses('t3-form-element-redo-form-button hidden disabled')
338 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-forward', Icon::SIZE_SMALL));
339
340 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
341 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
342 $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
343 $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
344 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
345 $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
346 }
347 }
348
349 /**
350 * Some data which is build by the form editor needs a transformation before
351 * it can be used by the framework.
352 * Multivalue elements like select elements produce data like:
353 *
354 * [
355 * _label => 'label'
356 * _value => 'value'
357 * ]
358 *
359 * This method transform this into:
360 *
361 * [
362 * 'value' => 'label'
363 * ]
364 *
365 * @param array $input
366 * @return array
367 */
368 protected function convertJsonArrayToAssociativeArray(array $input): array
369 {
370 $output = [];
371 foreach ($input as $key => $value) {
372 if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
373 $key = $value['_value'];
374 $value = $value['_label'];
375 }
376 if (is_array($value)) {
377 $output[$key] = $this->convertJsonArrayToAssociativeArray($value);
378 } else {
379 $output[$key] = $value;
380 }
381 }
382 return $output;
383 }
384
385 /**
386 * Render the "text/x-formeditor-template" templates.
387 *
388 * @param array $formEditorDefinitions
389 * @return string
390 */
391 protected function renderFormEditorTemplates(array $formEditorDefinitions): string
392 {
393 $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'];
394 $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'];
395
396 if (!isset($fluidConfiguration['templatePathAndFilename'])) {
397 throw new RenderingException(
398 'The option templatePathAndFilename must be set.',
399 1485636499
400 );
401 }
402 if (
403 !isset($fluidConfiguration['layoutRootPaths'])
404 || !is_array($fluidConfiguration['layoutRootPaths'])
405 ) {
406 throw new RenderingException(
407 'The option layoutRootPaths must be set.',
408 1480294721
409 );
410 }
411 if (
412 !isset($fluidConfiguration['partialRootPaths'])
413 || !is_array($fluidConfiguration['partialRootPaths'])
414 ) {
415 throw new RenderingException(
416 'The option partialRootPaths must be set.',
417 1480294722
418 );
419 }
420
421 $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
422
423 $view = $this->objectManager->get(TemplateView::class);
424 $view->setControllerContext(clone $this->controllerContext);
425 $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
426 $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
427 $view->assignMultiple([
428 'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
429 'formEditorPartials' => $formEditorPartials,
430 ]);
431
432 return $view->render();
433 }
434
435 /**
436 * Returns the current BE user.
437 *
438 * @return BackendUserAuthentication
439 */
440 protected function getBackendUser(): BackendUserAuthentication
441 {
442 return $GLOBALS['BE_USER'];
443 }
444
445 /**
446 * Returns the language service
447 *
448 * @return LanguageService
449 */
450 protected function getLanguageService(): LanguageService
451 {
452 return $GLOBALS['LANG'];
453 }
454 }