[TASK] Unify spelling of "YAML" throughout the core
[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 if (
146 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
147 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
148 ) {
149 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] as $className) {
150 $hookObj = GeneralUtility::makeInstance($className);
151 if (method_exists($hookObj, 'beforeFormSave')) {
152 $formDefinition = $hookObj->beforeFormSave(
153 $formPersistenceIdentifier,
154 $formDefinition
155 );
156 }
157 }
158 }
159
160 $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
161 return '';
162 }
163
164 /**
165 * Render a page from the formDefinition which was build by the form editor.
166 * Use the frontend rendering and set the form framework to preview mode.
167 *
168 * @param array $formDefinition
169 * @param int $pageIndex
170 * @param string $prototypeName
171 * @return string
172 * @internal
173 */
174 public function renderFormPageAction(array $formDefinition, int $pageIndex, string $prototypeName = null): string
175 {
176 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
177 $formDefinition = $this->convertJsonArrayToAssociativeArray($formDefinition);
178 if (empty($prototypeName)) {
179 $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
180 }
181
182 $formFactory = $this->objectManager->get(ArrayFormFactory::class);
183 $formDefinition = $formFactory->build($formDefinition, $prototypeName);
184 $formDefinition->setRenderingOption('previewMode', true);
185 $form = $formDefinition->bind($this->request, $this->response);
186 $form->overrideCurrentPage($pageIndex);
187 return $form->render();
188 }
189
190 /**
191 * Prepare the formElements.*.formEditor section from the YAML settings.
192 * Sort all formElements into groups and add additional data.
193 *
194 * @param array $formElementsDefinition
195 * @return array
196 */
197 protected function getInsertRenderablesPanelConfiguration(array $formElementsDefinition): array
198 {
199 $formElementGroups = isset($this->prototypeConfiguration['formEditor']['formElementGroups']) ? $this->prototypeConfiguration['formEditor']['formElementGroups'] : [];
200 $formElementsByGroup = [];
201
202 foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
203 if (!isset($formElementConfiguration['group'])) {
204 continue;
205 }
206 if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
207 $formElementsByGroup[$formElementConfiguration['group']] = [];
208 }
209
210 $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
211 $formElementConfiguration,
212 $this->prototypeConfiguration['formEditor']['translationFile']
213 );
214
215 $formElementsByGroup[$formElementConfiguration['group']][] = [
216 'key' => $formElementName,
217 'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
218 'label' => $formElementConfiguration['label'],
219 'sorting' => $formElementConfiguration['groupSorting'],
220 'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
221 ];
222 }
223
224 $formGroups = [];
225 foreach ($formElementGroups as $groupName => $groupConfiguration) {
226 if (!isset($formElementsByGroup[$groupName])) {
227 continue;
228 }
229
230 usort($formElementsByGroup[$groupName], function ($a, $b) {
231 return $a['sorting'] - $b['sorting'];
232 });
233 unset($formElementsByGroup[$groupName]['sorting']);
234
235 $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
236 $groupConfiguration,
237 $this->prototypeConfiguration['formEditor']['translationFile']
238 );
239
240 $formGroups[] = [
241 'key' => $groupName,
242 'elements' => $formElementsByGroup[$groupName],
243 'label' => $groupConfiguration['label'],
244 ];
245 }
246
247 return $formGroups;
248 }
249
250 /**
251 * Reduce the YAML settings by the 'formEditor' keyword.
252 *
253 * @return array
254 */
255 protected function getFormEditorDefinitions(): array
256 {
257 $formEditorDefinitions = [];
258 foreach ([$this->prototypeConfiguration, $this->prototypeConfiguration['formEditor']] as $configuration) {
259 foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
260 if (substr($firstLevelItemKey, -10) !== 'Definition') {
261 continue;
262 }
263 $reducedKey = substr($firstLevelItemKey, 0, -10);
264 foreach ($configuration[$firstLevelItemKey] as $formEditorDefinitionKey => $formEditorDefinitionValue) {
265 if (isset($formEditorDefinitionValue['formEditor'])) {
266 $formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
267 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
268 } else {
269 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
270 }
271 }
272 }
273 }
274 $formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
275 $formEditorDefinitions = TranslationService::getInstance()->translateValuesRecursive(
276 $formEditorDefinitions,
277 $this->prototypeConfiguration['formEditor']['translationFile']
278 );
279 return $formEditorDefinitions;
280 }
281
282 /**
283 * Registers the Icons into the docheader
284 *
285 * @throws \InvalidArgumentException
286 */
287 protected function registerDocheaderButtons()
288 {
289 /** @var ButtonBar $buttonBar */
290 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
291 $getVars = $this->request->getArguments();
292
293 if (isset($getVars['action']) && $getVars['action'] === 'index') {
294 $newPageButton = $buttonBar->makeInputButton()
295 ->setDataAttributes(['action' => 'formeditor-new-page', 'identifier' => 'headerNewPage'])
296 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.new_page_button'))
297 ->setName('formeditor-new-page')
298 ->setValue('new-page')
299 ->setClasses('t3-form-element-new-page-button hidden')
300 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
301
302 $closeButton = $buttonBar->makeLinkButton()
303 ->setDataAttributes(['identifier' => 'closeButton'])
304 ->setHref(BackendUtility::getModuleUrl('web_FormFormbuilder'))
305 ->setClasses('t3-form-element-close-form-button hidden')
306 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
307 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
308
309 $saveButton = $buttonBar->makeInputButton()
310 ->setDataAttributes(['identifier' => 'saveButton'])
311 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
312 ->setName('formeditor-save-form')
313 ->setValue('save')
314 ->setClasses('t3-form-element-save-form-button hidden')
315 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
316 ->setShowLabelText(true);
317
318 $formSettingsButton = $buttonBar->makeInputButton()
319 ->setDataAttributes(['identifier' => 'formSettingsButton'])
320 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
321 ->setName('formeditor-form-settings')
322 ->setValue('settings')
323 ->setClasses('t3-form-element-form-settings-button hidden')
324 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
325 ->setShowLabelText(true);
326
327 $undoButton = $buttonBar->makeInputButton()
328 ->setDataAttributes(['identifier' => 'undoButton'])
329 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
330 ->setName('formeditor-undo-form')
331 ->setValue('undo')
332 ->setClasses('t3-form-element-undo-form-button hidden disabled')
333 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
334
335 $redoButton = $buttonBar->makeInputButton()
336 ->setDataAttributes(['identifier' => 'redoButton'])
337 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
338 ->setName('formeditor-redo-form')
339 ->setValue('redo')
340 ->setClasses('t3-form-element-redo-form-button hidden disabled')
341 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-forward', Icon::SIZE_SMALL));
342
343 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
344 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
345 $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
346 $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
347 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
348 $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
349 }
350 }
351
352 /**
353 * Some data which is build by the form editor needs a transformation before
354 * it can be used by the framework.
355 * Multivalue elements like select elements produce data like:
356 *
357 * [
358 * _label => 'label'
359 * _value => 'value'
360 * ]
361 *
362 * This method transform this into:
363 *
364 * [
365 * 'value' => 'label'
366 * ]
367 *
368 * @param array $input
369 * @return array
370 */
371 protected function convertJsonArrayToAssociativeArray(array $input): array
372 {
373 $output = [];
374 foreach ($input as $key => $value) {
375 if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
376 $key = $value['_value'];
377 $value = $value['_label'];
378 }
379 if (is_array($value)) {
380 $output[$key] = $this->convertJsonArrayToAssociativeArray($value);
381 } else {
382 $output[$key] = $value;
383 }
384 }
385 return $output;
386 }
387
388 /**
389 * Render the "text/x-formeditor-template" templates.
390 *
391 * @param array $formEditorDefinitions
392 * @return string
393 */
394 protected function renderFormEditorTemplates(array $formEditorDefinitions): string
395 {
396 $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'];
397 $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'];
398
399 if (!isset($fluidConfiguration['templatePathAndFilename'])) {
400 throw new RenderingException(
401 'The option templatePathAndFilename must be set.',
402 1485636499
403 );
404 }
405 if (
406 !isset($fluidConfiguration['layoutRootPaths'])
407 || !is_array($fluidConfiguration['layoutRootPaths'])
408 ) {
409 throw new RenderingException(
410 'The option layoutRootPaths must be set.',
411 1480294721
412 );
413 }
414 if (
415 !isset($fluidConfiguration['partialRootPaths'])
416 || !is_array($fluidConfiguration['partialRootPaths'])
417 ) {
418 throw new RenderingException(
419 'The option partialRootPaths must be set.',
420 1480294722
421 );
422 }
423
424 $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
425
426 $view = $this->objectManager->get(TemplateView::class);
427 $view->setControllerContext(clone $this->controllerContext);
428 $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
429 $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
430 $view->assignMultiple([
431 'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
432 'formEditorPartials' => $formEditorPartials,
433 ]);
434
435 return $view->render();
436 }
437
438 /**
439 * Returns the current BE user.
440 *
441 * @return BackendUserAuthentication
442 */
443 protected function getBackendUser(): BackendUserAuthentication
444 {
445 return $GLOBALS['BE_USER'];
446 }
447
448 /**
449 * Returns the language service
450 *
451 * @return LanguageService
452 */
453 protected function getLanguageService(): LanguageService
454 {
455 return $GLOBALS['LANG'];
456 }
457 }