8887907c49b01a7503e644211dda794eee159c4d
[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 use TYPO3\CMS\Form\Type\FormDefinitionArray;
33
34 /**
35 * The form editor controller
36 *
37 * Scope: backend
38 */
39 class FormEditorController extends AbstractBackendController
40 {
41
42 /**
43 * Default View Container
44 *
45 * @var BackendTemplateView
46 */
47 protected $defaultViewObjectName = BackendTemplateView::class;
48
49 /**
50 * @var array
51 */
52 protected $prototypeConfiguration;
53
54 /**
55 * Displays the form editor
56 *
57 * @param string $formPersistenceIdentifier
58 * @param string $prototypeName
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 $prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
76 $formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
77 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
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 FormDefinitionArray $formDefinition
149 * @internal
150 */
151 public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
152 {
153 $formDefinition = $formDefinition->getArrayCopy();
154
155 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] ?? [] as $className) {
156 $hookObj = GeneralUtility::makeInstance($className);
157 if (method_exists($hookObj, 'beforeFormSave')) {
158 $formDefinition = $hookObj->beforeFormSave(
159 $formPersistenceIdentifier,
160 $formDefinition
161 );
162 }
163 }
164
165 $response = [
166 'status' => 'success',
167 ];
168
169 try {
170 $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
171 } catch (PersistenceManagerException $e) {
172 $response = [
173 'status' => 'error',
174 'message' => $e->getMessage(),
175 'code' => $e->getCode(),
176 ];
177 }
178
179 $response['formDefinition'] = $formDefinition;
180
181 $this->view->assign('response', $response);
182 // saveFormAction uses the extbase JsonView::class.
183 // That's why we have to set the view variables in this way.
184 $this->view->setVariablesToRender([
185 'response',
186 ]);
187 }
188
189 /**
190 * Render a page from the formDefinition which was build by the form editor.
191 * Use the frontend rendering and set the form framework to preview mode.
192 *
193 * @param FormDefinitionArray $formDefinition
194 * @param int $pageIndex
195 * @param string $prototypeName
196 * @return string
197 * @internal
198 */
199 public function renderFormPageAction(FormDefinitionArray $formDefinition, int $pageIndex, string $prototypeName = null): string
200 {
201 $prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
202
203 $formFactory = $this->objectManager->get(ArrayFormFactory::class);
204 $formDefinition = $formFactory->build($formDefinition->getArrayCopy(), $prototypeName);
205 $formDefinition->setRenderingOption('previewMode', true);
206 $form = $formDefinition->bind($this->request, $this->response);
207 $form->overrideCurrentPage($pageIndex);
208
209 return $form->render();
210 }
211
212 /**
213 * Prepare the formElements.*.formEditor section from the YAML settings.
214 * Sort all formElements into groups and add additional data.
215 *
216 * @param array $formElementsDefinition
217 * @return array
218 */
219 protected function getInsertRenderablesPanelConfiguration(array $formElementsDefinition): array
220 {
221 $formElementsByGroup = [];
222
223 foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
224 if (!isset($formElementConfiguration['group'])) {
225 continue;
226 }
227 if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
228 $formElementsByGroup[$formElementConfiguration['group']] = [];
229 }
230
231 $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
232 $formElementConfiguration,
233 $this->prototypeConfiguration['formEditor']['translationFile'] ?? null
234 );
235
236 $formElementsByGroup[$formElementConfiguration['group']][] = [
237 'key' => $formElementName,
238 'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
239 'label' => $formElementConfiguration['label'],
240 'sorting' => $formElementConfiguration['groupSorting'],
241 'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
242 ];
243 }
244
245 $formGroups = [];
246 foreach ($this->prototypeConfiguration['formEditor']['formElementGroups'] ?? [] as $groupName => $groupConfiguration) {
247 if (!isset($formElementsByGroup[$groupName])) {
248 continue;
249 }
250
251 usort($formElementsByGroup[$groupName], function ($a, $b) {
252 return $a['sorting'] - $b['sorting'];
253 });
254 unset($formElementsByGroup[$groupName]['sorting']);
255
256 $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
257 $groupConfiguration,
258 $this->prototypeConfiguration['formEditor']['translationFile'] ?? null
259 );
260
261 $formGroups[] = [
262 'key' => $groupName,
263 'elements' => $formElementsByGroup[$groupName],
264 'label' => $groupConfiguration['label'],
265 ];
266 }
267
268 return $formGroups;
269 }
270
271 /**
272 * Reduce the YAML settings by the 'formEditor' keyword.
273 *
274 * @return array
275 */
276 protected function getFormEditorDefinitions(): array
277 {
278 $formEditorDefinitions = [];
279 foreach ([$this->prototypeConfiguration, $this->prototypeConfiguration['formEditor']] as $configuration) {
280 foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
281 if (substr($firstLevelItemKey, -10) !== 'Definition') {
282 continue;
283 }
284 $reducedKey = substr($firstLevelItemKey, 0, -10);
285 foreach ($configuration[$firstLevelItemKey] as $formEditorDefinitionKey => $formEditorDefinitionValue) {
286 if (isset($formEditorDefinitionValue['formEditor'])) {
287 $formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
288 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
289 } else {
290 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
291 }
292 }
293 }
294 }
295 $formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
296 $formEditorDefinitions = TranslationService::getInstance()->translateValuesRecursive(
297 $formEditorDefinitions,
298 $this->prototypeConfiguration['formEditor']['translationFile'] ?? null
299 );
300 return $formEditorDefinitions;
301 }
302
303 /**
304 * Registers the Icons into the docheader
305 *
306 * @throws \InvalidArgumentException
307 */
308 protected function registerDocheaderButtons()
309 {
310 /** @var ButtonBar $buttonBar */
311 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
312 $getVars = $this->request->getArguments();
313
314 if (isset($getVars['action']) && $getVars['action'] === 'index') {
315 $newPageButton = $buttonBar->makeInputButton()
316 ->setDataAttributes(['action' => 'formeditor-new-page', 'identifier' => 'headerNewPage'])
317 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.new_page_button'))
318 ->setName('formeditor-new-page')
319 ->setValue('new-page')
320 ->setClasses('t3-form-element-new-page-button hidden')
321 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
322 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
323 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
324
325 $closeButton = $buttonBar->makeLinkButton()
326 ->setDataAttributes(['identifier' => 'closeButton'])
327 ->setHref((string)$uriBuilder->buildUriFromRoute('web_FormFormbuilder'))
328 ->setClasses('t3-form-element-close-form-button hidden')
329 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
330 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
331
332 $saveButton = $buttonBar->makeInputButton()
333 ->setDataAttributes(['identifier' => 'saveButton'])
334 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
335 ->setName('formeditor-save-form')
336 ->setValue('save')
337 ->setClasses('t3-form-element-save-form-button hidden')
338 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
339 ->setShowLabelText(true);
340
341 $formSettingsButton = $buttonBar->makeInputButton()
342 ->setDataAttributes(['identifier' => 'formSettingsButton'])
343 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
344 ->setName('formeditor-form-settings')
345 ->setValue('settings')
346 ->setClasses('t3-form-element-form-settings-button hidden')
347 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
348 ->setShowLabelText(true);
349
350 $undoButton = $buttonBar->makeInputButton()
351 ->setDataAttributes(['identifier' => 'undoButton'])
352 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
353 ->setName('formeditor-undo-form')
354 ->setValue('undo')
355 ->setClasses('t3-form-element-undo-form-button hidden disabled')
356 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
357
358 $redoButton = $buttonBar->makeInputButton()
359 ->setDataAttributes(['identifier' => 'redoButton'])
360 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
361 ->setName('formeditor-redo-form')
362 ->setValue('redo')
363 ->setClasses('t3-form-element-redo-form-button hidden disabled')
364 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-forward', Icon::SIZE_SMALL));
365
366 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
367 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
368 $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
369 $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
370 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
371 $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
372 }
373 }
374
375 /**
376 * Render the "text/x-formeditor-template" templates.
377 *
378 * @param array $formEditorDefinitions
379 * @return string
380 */
381 protected function renderFormEditorTemplates(array $formEditorDefinitions): string
382 {
383 $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'] ?? null;
384 $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'] ?? null;
385
386 if (!isset($fluidConfiguration['templatePathAndFilename'])) {
387 throw new RenderingException(
388 'The option templatePathAndFilename must be set.',
389 1485636499
390 );
391 }
392 if (
393 !isset($fluidConfiguration['layoutRootPaths'])
394 || !is_array($fluidConfiguration['layoutRootPaths'])
395 ) {
396 throw new RenderingException(
397 'The option layoutRootPaths must be set.',
398 1480294721
399 );
400 }
401 if (
402 !isset($fluidConfiguration['partialRootPaths'])
403 || !is_array($fluidConfiguration['partialRootPaths'])
404 ) {
405 throw new RenderingException(
406 'The option partialRootPaths must be set.',
407 1480294722
408 );
409 }
410
411 $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
412
413 $view = $this->objectManager->get(TemplateView::class);
414 $view->setControllerContext(clone $this->controllerContext);
415 $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
416 $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
417 $view->assignMultiple([
418 'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
419 'formEditorPartials' => $formEditorPartials,
420 ]);
421
422 return $view->render();
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 }