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