f9c2f60654ed9df9b193f250c3965aa7d4f837f9
[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\Localization\LanguageService;
24 use TYPO3\CMS\Core\Utility\ArrayUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
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
113 $addInlineSettings = [
114 'FormEditor' => [
115 'typo3WinBrowserUrl' => BackendUtility::getModuleUrl('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
297 $closeButton = $buttonBar->makeLinkButton()
298 ->setDataAttributes(['identifier' => 'closeButton'])
299 ->setHref(BackendUtility::getModuleUrl('web_FormFormbuilder'))
300 ->setClasses('t3-form-element-close-form-button hidden')
301 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
302 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
303
304 $saveButton = $buttonBar->makeInputButton()
305 ->setDataAttributes(['identifier' => 'saveButton'])
306 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
307 ->setName('formeditor-save-form')
308 ->setValue('save')
309 ->setClasses('t3-form-element-save-form-button hidden')
310 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
311 ->setShowLabelText(true);
312
313 $formSettingsButton = $buttonBar->makeInputButton()
314 ->setDataAttributes(['identifier' => 'formSettingsButton'])
315 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
316 ->setName('formeditor-form-settings')
317 ->setValue('settings')
318 ->setClasses('t3-form-element-form-settings-button hidden')
319 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
320 ->setShowLabelText(true);
321
322 $undoButton = $buttonBar->makeInputButton()
323 ->setDataAttributes(['identifier' => 'undoButton'])
324 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
325 ->setName('formeditor-undo-form')
326 ->setValue('undo')
327 ->setClasses('t3-form-element-undo-form-button hidden disabled')
328 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
329
330 $redoButton = $buttonBar->makeInputButton()
331 ->setDataAttributes(['identifier' => 'redoButton'])
332 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
333 ->setName('formeditor-redo-form')
334 ->setValue('redo')
335 ->setClasses('t3-form-element-redo-form-button hidden disabled')
336 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-forward', Icon::SIZE_SMALL));
337
338 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
339 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
340 $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
341 $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
342 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
343 $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
344 }
345 }
346
347 /**
348 * Some data which is build by the form editor needs a transformation before
349 * it can be used by the framework.
350 * Multivalue elements like select elements produce data like:
351 *
352 * [
353 * _label => 'label'
354 * _value => 'value'
355 * ]
356 *
357 * This method transform this into:
358 *
359 * [
360 * 'value' => 'label'
361 * ]
362 *
363 * @param array $input
364 * @return array
365 */
366 protected function convertJsonArrayToAssociativeArray(array $input): array
367 {
368 $output = [];
369 foreach ($input as $key => $value) {
370 if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
371 $key = $value['_value'];
372 $value = $value['_label'];
373 }
374 if (is_array($value)) {
375 $output[$key] = $this->convertJsonArrayToAssociativeArray($value);
376 } else {
377 $output[$key] = $value;
378 }
379 }
380 return $output;
381 }
382
383 /**
384 * Render the "text/x-formeditor-template" templates.
385 *
386 * @param array $formEditorDefinitions
387 * @return string
388 */
389 protected function renderFormEditorTemplates(array $formEditorDefinitions): string
390 {
391 $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'];
392 $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'];
393
394 if (!isset($fluidConfiguration['templatePathAndFilename'])) {
395 throw new RenderingException(
396 'The option templatePathAndFilename must be set.',
397 1485636499
398 );
399 }
400 if (
401 !isset($fluidConfiguration['layoutRootPaths'])
402 || !is_array($fluidConfiguration['layoutRootPaths'])
403 ) {
404 throw new RenderingException(
405 'The option layoutRootPaths must be set.',
406 1480294721
407 );
408 }
409 if (
410 !isset($fluidConfiguration['partialRootPaths'])
411 || !is_array($fluidConfiguration['partialRootPaths'])
412 ) {
413 throw new RenderingException(
414 'The option partialRootPaths must be set.',
415 1480294722
416 );
417 }
418
419 $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
420
421 $view = $this->objectManager->get(TemplateView::class);
422 $view->setControllerContext(clone $this->controllerContext);
423 $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
424 $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
425 $view->assignMultiple([
426 'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
427 'formEditorPartials' => $formEditorPartials,
428 ]);
429
430 return $view->render();
431 }
432
433 /**
434 * Returns the current BE user.
435 *
436 * @return BackendUserAuthentication
437 */
438 protected function getBackendUser(): BackendUserAuthentication
439 {
440 return $GLOBALS['BE_USER'];
441 }
442
443 /**
444 * Returns the language service
445 *
446 * @return LanguageService
447 */
448 protected function getLanguageService(): LanguageService
449 {
450 return $GLOBALS['LANG'];
451 }
452 }