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