TYPO3 CMS  TYPO3_8-7
FormEditorController.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
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 
36 
43 {
44 
50  protected $defaultViewObjectName = BackendTemplateView::class;
51 
56 
65  public function indexAction(string $formPersistenceIdentifier, string $prototypeName = null)
66  {
67  $this->registerDocheaderButtons();
68  $this->view->getModuleTemplate()->setModuleName($this->request->getPluginName() . '_' . $this->request->getControllerName());
69  $this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
70 
71  if (
72  strpos($formPersistenceIdentifier, 'EXT:') === 0
73  && !$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']
74  ) {
75  throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
76  }
77 
78  $configurationService = $this->objectManager->get(ConfigurationService::class);
79  $formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
80 
81  if ($prototypeName === null) {
82  $prototypeName = $formDefinition['prototypeName'] ?? 'standard';
83  } else {
84  // Loading a form definition with another prototype is currently not implemented but is planned in the future.
85  // This safety check is a preventive measure.
86  $selectablePrototypeNames = $configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
87  if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
88  throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
89  }
90  }
91 
92  $formDefinition['prototypeName'] = $prototypeName;
93  $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
94 
95  $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
96  $formEditorDefinitions = $this->getFormEditorDefinitions();
97 
98  $formEditorAppInitialData = [
99  'formEditorDefinitions' => $formEditorDefinitions,
100  'formDefinition' => $formDefinition,
101  'formPersistenceIdentifier' => $formPersistenceIdentifier,
102  'prototypeName' => $prototypeName,
103  'endpoints' => [
104  'formPageRenderer' => $this->controllerContext->getUriBuilder()->uriFor('renderFormPage'),
105  'saveForm' => $this->controllerContext->getUriBuilder()->uriFor('saveForm')
106  ],
107  'additionalViewModelModules' => $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']['additionalViewModelModules'],
108  'maximumUndoSteps' => $this->prototypeConfiguration['formEditor']['maximumUndoSteps'],
109  ];
110 
111  $this->view->assign('formEditorAppInitialData', json_encode($formEditorAppInitialData));
112  $this->view->assign('stylesheets', $this->resolveResourcePaths($this->prototypeConfiguration['formEditor']['stylesheets']));
113  $this->view->assign('formEditorTemplates', $this->renderFormEditorTemplates($formEditorDefinitions));
114  $this->view->assign('dynamicRequireJsModules', $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']);
115 
116  $popupWindowWidth = 700;
117  $popupWindowHeight = 750;
118  $popupWindowSize = ($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
119  ? trim($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
120  : null;
121  if (!empty($popupWindowSize)) {
122  list($popupWindowWidth, $popupWindowHeight) = GeneralUtility::intExplode('x', $popupWindowSize);
123  }
124 
125  $addInlineSettings = [
126  'FormEditor' => [
127  'typo3WinBrowserUrl' => BackendUtility::getModuleUrl('wizard_element_browser'),
128  ],
129  'Popup' => [
130  'PopupWindow' => [
131  'width' => $popupWindowWidth,
132  'height' => $popupWindowHeight
133  ],
134  ]
135  ];
136 
137  $addInlineSettings = array_replace_recursive(
138  $addInlineSettings,
139  $this->prototypeConfiguration['formEditor']['addInlineSettings']
140  );
141  $this->view->assign('addInlineSettings', $addInlineSettings);
142  }
143 
150  public function initializeSaveFormAction()
151  {
152  $this->defaultViewObjectName = JsonView::class;
153  }
154 
162  public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
163  {
164  $formDefinition = $formDefinition->getArrayCopy();
165 
166  if (
167  isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
168  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
169  ) {
170  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] as $className) {
171  $hookObj = GeneralUtility::makeInstance($className);
172  if (method_exists($hookObj, 'beforeFormSave')) {
173  $formDefinition = $hookObj->beforeFormSave(
174  $formPersistenceIdentifier,
175  $formDefinition
176  );
177  }
178  }
179  }
180 
181  $response = [
182  'status' => 'success',
183  ];
184 
185  try {
186  $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
187  $configurationService = $this->objectManager->get(ConfigurationService::class);
188  $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
189  $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
190  $response['formDefinition'] = $formDefinition;
191  } catch (PersistenceManagerException $e) {
192  $response = [
193  'status' => 'error',
194  'message' => $e->getMessage(),
195  'code' => $e->getCode(),
196  ];
197  }
198 
199  $this->view->assign('response', $response);
200  // saveFormAction uses the extbase JsonView::class.
201  // That's why we have to set the view variables in this way.
202  $this->view->setVariablesToRender([
203  'response',
204  ]);
205  }
206 
217  public function renderFormPageAction(FormDefinitionArray $formDefinition, int $pageIndex, string $prototypeName = null): string
218  {
219  if (empty($prototypeName)) {
220  $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
221  }
222  $formDefinition = $formDefinition->getArrayCopy();
223 
224  $formFactory = $this->objectManager->get(ArrayFormFactory::class);
225  $formDefinition = $formFactory->build($formDefinition, $prototypeName);
226  $formDefinition->setRenderingOption('previewMode', true);
227  $form = $formDefinition->bind($this->request, $this->response);
228  $form->overrideCurrentPage($pageIndex);
229 
230  return $form->render();
231  }
232 
240  protected function getInsertRenderablesPanelConfiguration(array $formElementsDefinition): array
241  {
242  $formElementGroups = isset($this->prototypeConfiguration['formEditor']['formElementGroups']) ? $this->prototypeConfiguration['formEditor']['formElementGroups'] : [];
243  $formElementsByGroup = [];
244 
245  foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
246  if (!isset($formElementConfiguration['group'])) {
247  continue;
248  }
249  if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
250  $formElementsByGroup[$formElementConfiguration['group']] = [];
251  }
252 
253  $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
254  $formElementConfiguration,
255  $this->prototypeConfiguration['formEditor']['translationFile']
256  );
257 
258  $formElementsByGroup[$formElementConfiguration['group']][] = [
259  'key' => $formElementName,
260  'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
261  'label' => $formElementConfiguration['label'],
262  'sorting' => $formElementConfiguration['groupSorting'],
263  'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
264  ];
265  }
266 
267  $formGroups = [];
268  foreach ($formElementGroups as $groupName => $groupConfiguration) {
269  if (!isset($formElementsByGroup[$groupName])) {
270  continue;
271  }
272 
273  usort($formElementsByGroup[$groupName], function ($a, $b) {
274  return $a['sorting'] - $b['sorting'];
275  });
276  unset($formElementsByGroup[$groupName]['sorting']);
277 
278  $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
279  $groupConfiguration,
280  $this->prototypeConfiguration['formEditor']['translationFile']
281  );
282 
283  $formGroups[] = [
284  'key' => $groupName,
285  'elements' => $formElementsByGroup[$groupName],
286  'label' => $groupConfiguration['label'],
287  ];
288  }
289 
290  return $formGroups;
291  }
292 
298  protected function getFormEditorDefinitions(): array
299  {
300  $formEditorDefinitions = [];
301  foreach ([$this->prototypeConfiguration, $this->prototypeConfiguration['formEditor']] as $configuration) {
302  foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
303  if (substr($firstLevelItemKey, -10) !== 'Definition') {
304  continue;
305  }
306  $reducedKey = substr($firstLevelItemKey, 0, -10);
307  foreach ($configuration[$firstLevelItemKey] as $formEditorDefinitionKey => $formEditorDefinitionValue) {
308  if (isset($formEditorDefinitionValue['formEditor'])) {
309  $formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
310  $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
311  } else {
312  $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
313  }
314  }
315  }
316  }
317  $formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
318  $formEditorDefinitions = TranslationService::getInstance()->translateValuesRecursive(
319  $formEditorDefinitions,
320  $this->prototypeConfiguration['formEditor']['translationFile']
321  );
322  return $formEditorDefinitions;
323  }
324 
330  protected function registerDocheaderButtons()
331  {
333  $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
334  $getVars = $this->request->getArguments();
335 
336  if (isset($getVars['action']) && $getVars['action'] === 'index') {
337  $newPageButton = $buttonBar->makeInputButton()
338  ->setDataAttributes(['action' => 'formeditor-new-page', 'identifier' => 'headerNewPage'])
339  ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.new_page_button'))
340  ->setName('formeditor-new-page')
341  ->setValue('new-page')
342  ->setClasses('t3-form-element-new-page-button hidden')
343  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
344 
345  $closeButton = $buttonBar->makeLinkButton()
346  ->setDataAttributes(['identifier' => 'closeButton'])
347  ->setHref(BackendUtility::getModuleUrl('web_FormFormbuilder'))
348  ->setClasses('t3-form-element-close-form-button hidden')
349  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
350  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
351 
352  $saveButton = $buttonBar->makeInputButton()
353  ->setDataAttributes(['identifier' => 'saveButton'])
354  ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
355  ->setName('formeditor-save-form')
356  ->setValue('save')
357  ->setClasses('t3-form-element-save-form-button hidden')
358  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
359  ->setShowLabelText(true);
360 
361  $formSettingsButton = $buttonBar->makeInputButton()
362  ->setDataAttributes(['identifier' => 'formSettingsButton'])
363  ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
364  ->setName('formeditor-form-settings')
365  ->setValue('settings')
366  ->setClasses('t3-form-element-form-settings-button hidden')
367  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
368  ->setShowLabelText(true);
369 
370  $undoButton = $buttonBar->makeInputButton()
371  ->setDataAttributes(['identifier' => 'undoButton'])
372  ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
373  ->setName('formeditor-undo-form')
374  ->setValue('undo')
375  ->setClasses('t3-form-element-undo-form-button hidden disabled')
376  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
377 
378  $redoButton = $buttonBar->makeInputButton()
379  ->setDataAttributes(['identifier' => 'redoButton'])
380  ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
381  ->setName('formeditor-redo-form')
382  ->setValue('redo')
383  ->setClasses('t3-form-element-redo-form-button hidden disabled')
384  ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-view-go-forward', Icon::SIZE_SMALL));
385 
386  $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
387  $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
388  $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
389  $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
390  $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
391  $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
392  }
393  }
394 
401  protected function renderFormEditorTemplates(array $formEditorDefinitions): string
402  {
403  $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'];
404  $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'];
405 
406  if (!isset($fluidConfiguration['templatePathAndFilename'])) {
407  throw new RenderingException(
408  'The option templatePathAndFilename must be set.',
409  1485636499
410  );
411  }
412  if (
413  !isset($fluidConfiguration['layoutRootPaths'])
414  || !is_array($fluidConfiguration['layoutRootPaths'])
415  ) {
416  throw new RenderingException(
417  'The option layoutRootPaths must be set.',
418  1480294721
419  );
420  }
421  if (
422  !isset($fluidConfiguration['partialRootPaths'])
423  || !is_array($fluidConfiguration['partialRootPaths'])
424  ) {
425  throw new RenderingException(
426  'The option partialRootPaths must be set.',
427  1480294722
428  );
429  }
430 
431  $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
432 
433  $view = $this->objectManager->get(TemplateView::class);
434  $view->setControllerContext(clone $this->controllerContext);
435  $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
436  $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
437  $view->assignMultiple([
438  'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
439  'formEditorPartials' => $formEditorPartials,
440  ]);
441 
442  return $view->render();
443  }
444 
450  protected function transformFormDefinitionForFormEditor(array $formDefinition): array
451  {
452  $multiValueProperties = [];
453  foreach ($this->prototypeConfiguration['formElementsDefinition'] as $type => $configuration) {
454  if (!isset($configuration['formEditor']['editors'])) {
455  continue;
456  }
457  foreach ($configuration['formEditor']['editors'] as $editorConfiguration) {
458  if ($editorConfiguration['templateName'] === 'Inspector-PropertyGridEditor') {
459  $multiValueProperties[$type][] = $editorConfiguration['propertyPath'];
460  }
461  }
462  }
463 
464  $formDefinition = $this->filterEmptyArrays($formDefinition);
465 
466  // @todo: replace with rte parsing
467  $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
468  $formDefinition = $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);
469 
470  $formDefinitionConversionService = $this->getFormDefinitionConversionService();
471  $formDefinition = $formDefinitionConversionService->addHmacData($formDefinition);
472 
473  return $formDefinition;
474  }
475 
511  array $formDefinition,
512  array $multiValueProperties
513  ): array {
514  $output = $formDefinition;
515  foreach ($formDefinition as $key => $value) {
516  if (isset($value['type']) && array_key_exists($value['type'], $multiValueProperties)) {
517  $multiValuePropertiesForType = $multiValueProperties[$value['type']];
518  foreach ($multiValuePropertiesForType as $multiValueProperty) {
519  if (!ArrayUtility::isValidPath($value, $multiValueProperty, '.')) {
520  continue;
521  }
522  $multiValuePropertyData = ArrayUtility::getValueByPath($value, $multiValueProperty, '.');
523  if (!is_array($multiValuePropertyData)) {
524  continue;
525  }
526  $newMultiValuePropertyData = [];
527  foreach ($multiValuePropertyData as $k => $v) {
528  $newMultiValuePropertyData[] = [
529  '_label' => $v,
530  '_value' => $k
531  ];
532  }
533  $value = ArrayUtility::setValueByPath($value, $multiValueProperty, $newMultiValuePropertyData, '.');
534  }
535  }
536 
537  $output[$key] = $value;
538  if (is_array($value)) {
539  $output[$key] = $this->transformMultiValueElementsForFormEditor($value, $multiValueProperties);
540  }
541  }
542 
543  return $output;
544  }
545 
552  protected function filterEmptyArrays(array $array): array
553  {
554  foreach ($array as $key => $value) {
555  if (!is_array($value)) {
556  continue;
557  }
558  if (empty($value)) {
559  unset($array[$key]);
560  continue;
561  }
562  $array[$key] = $this->filterEmptyArrays($value);
563  if (empty($array[$key])) {
564  unset($array[$key]);
565  }
566  }
567 
568  return $array;
569  }
570 
575  {
576  return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
577  }
578 
585  {
586  return $GLOBALS['BE_USER'];
587  }
588 
594  protected function getLanguageService(): LanguageService
595  {
596  return $GLOBALS['LANG'];
597  }
598 }
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static setValueByPath(array $array, $path, $value, $delimiter='/')
saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
static getValueByPath(array $array, $path, $delimiter='/')
static reIndexNumericArrayKeysRecursive(array $array)
static makeInstance($className,... $constructorArguments)
transformMultiValueElementsForFormEditor(array $formDefinition, array $multiValueProperties)
indexAction(string $formPersistenceIdentifier, string $prototypeName=null)
renderFormEditorTemplates(array $formEditorDefinitions)
static isValidPath(array $array, $path, $delimiter='/')
renderFormPageAction(FormDefinitionArray $formDefinition, int $pageIndex, string $prototypeName=null)
getInsertRenderablesPanelConfiguration(array $formElementsDefinition)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static stripTagsFromValuesRecursive(array $array)