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