[BUGFIX] Do not process original files
[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\Routing\UriBuilder;
19 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
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\Page\PageRenderer;
25 use TYPO3\CMS\Core\Site\Entity\Site;
26 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
27 use TYPO3\CMS\Core\Utility\ArrayUtility;
28 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
31 use TYPO3\CMS\Fluid\View\TemplateView;
32 use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
33 use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
34 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
35 use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
36 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
37 use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
38 use TYPO3\CMS\Form\Domain\Finishers\EmailFinisher;
39 use TYPO3\CMS\Form\Exception;
40 use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
41 use TYPO3\CMS\Form\Service\TranslationService;
42 use TYPO3\CMS\Form\Type\FormDefinitionArray;
43
44 /**
45 * The form editor controller
46 *
47 * Scope: backend
48 * @internal
49 */
50 class FormEditorController extends AbstractBackendController
51 {
52
53 /**
54 * Default View Container
55 *
56 * @var BackendTemplateView
57 */
58 protected $defaultViewObjectName = BackendTemplateView::class;
59
60 /**
61 * @var array
62 */
63 protected $prototypeConfiguration;
64
65 /**
66 * Displays the form editor
67 *
68 * @param string $formPersistenceIdentifier
69 * @param string $prototypeName
70 * @throws PersistenceManagerException
71 * @internal
72 */
73 public function indexAction(string $formPersistenceIdentifier, string $prototypeName = null)
74 {
75 $this->registerDocheaderButtons();
76 $this->view->getModuleTemplate()->setModuleName($this->request->getPluginName() . '_' . $this->request->getControllerName());
77 $this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
78
79 if (
80 strpos($formPersistenceIdentifier, 'EXT:') === 0
81 && !$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']
82 ) {
83 throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
84 }
85
86 $configurationService = $this->objectManager->get(ConfigurationService::class);
87 $formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
88
89 if ($prototypeName === null) {
90 $prototypeName = $formDefinition['prototypeName'] ?? 'standard';
91 } else {
92 // Loading a form definition with another prototype is currently not implemented but is planned in the future.
93 // This safety check is a preventive measure.
94 $selectablePrototypeNames = $configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
95 if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
96 throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
97 }
98 }
99
100 $formDefinition['prototypeName'] = $prototypeName;
101 $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
102
103 $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
104 $formEditorDefinitions = $this->getFormEditorDefinitions();
105
106 $formEditorAppInitialData = [
107 'formEditorDefinitions' => $formEditorDefinitions,
108 'formDefinition' => $formDefinition,
109 'formPersistenceIdentifier' => $formPersistenceIdentifier,
110 'prototypeName' => $prototypeName,
111 'endpoints' => [
112 'formPageRenderer' => $this->controllerContext->getUriBuilder()->uriFor('renderFormPage'),
113 'saveForm' => $this->controllerContext->getUriBuilder()->uriFor('saveForm')
114 ],
115 'additionalViewModelModules' => $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']['additionalViewModelModules'],
116 'maximumUndoSteps' => $this->prototypeConfiguration['formEditor']['maximumUndoSteps'],
117 ];
118
119 $this->view->assign('formEditorAppInitialData', json_encode($formEditorAppInitialData));
120 $this->view->assign('stylesheets', $this->resolveResourcePaths($this->prototypeConfiguration['formEditor']['stylesheets']));
121 $this->view->assign('formEditorTemplates', $this->renderFormEditorTemplates($formEditorDefinitions));
122 $this->view->assign('dynamicRequireJsModules', $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']);
123
124 $this->getPageRenderer()->addInlineLanguageLabelFile('EXT:form/Resources/Private/Language/locallang_formEditor_failSafeErrorHandling_javascript.xlf');
125
126 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
127 $addInlineSettings = [
128 'FormEditor' => [
129 'typo3WinBrowserUrl' => (string)$uriBuilder->buildUriFromRoute('wizard_element_browser'),
130 ],
131 ];
132
133 $addInlineSettings = array_replace_recursive(
134 $addInlineSettings,
135 $this->prototypeConfiguration['formEditor']['addInlineSettings']
136 );
137 $this->view->assign('addInlineSettings', $addInlineSettings);
138 }
139
140 /**
141 * Initialize the save action.
142 * This action uses the Fluid JsonView::class as view.
143 *
144 * @internal
145 */
146 public function initializeSaveFormAction()
147 {
148 $this->defaultViewObjectName = JsonView::class;
149 }
150
151 /**
152 * Save a formDefinition which was build by the form editor.
153 *
154 * @param string $formPersistenceIdentifier
155 * @param FormDefinitionArray $formDefinition
156 * @internal
157 */
158 public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
159 {
160 $formDefinition = $formDefinition->getArrayCopy();
161
162 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] ?? [] as $className) {
163 $hookObj = GeneralUtility::makeInstance($className);
164 if (method_exists($hookObj, 'beforeFormSave')) {
165 $formDefinition = $hookObj->beforeFormSave(
166 $formPersistenceIdentifier,
167 $formDefinition
168 );
169 }
170 }
171
172 $response = [
173 'status' => 'success',
174 ];
175
176 try {
177 $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
178 $configurationService = $this->objectManager->get(ConfigurationService::class);
179 $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
180 $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
181 $response['formDefinition'] = $formDefinition;
182 } catch (PersistenceManagerException $e) {
183 $response = [
184 'status' => 'error',
185 'message' => $e->getMessage(),
186 'code' => $e->getCode(),
187 ];
188 }
189
190 $this->view->assign('response', $response);
191 // saveFormAction uses the extbase JsonView::class.
192 // That's why we have to set the view variables in this way.
193 $this->view->setVariablesToRender([
194 'response',
195 ]);
196 }
197
198 /**
199 * Render a page from the formDefinition which was build by the form editor.
200 * Use the frontend rendering and set the form framework to preview mode.
201 *
202 * @param FormDefinitionArray $formDefinition
203 * @param int $pageIndex
204 * @param string $prototypeName
205 * @return string
206 * @internal
207 */
208 public function renderFormPageAction(FormDefinitionArray $formDefinition, int $pageIndex, string $prototypeName = null): string
209 {
210 $prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
211 $formDefinition = $formDefinition->getArrayCopy();
212
213 $formFactory = $this->objectManager->get(ArrayFormFactory::class);
214 $formDefinition = $formFactory->build($formDefinition, $prototypeName);
215 $formDefinition->setRenderingOption('previewMode', true);
216 $form = $formDefinition->bind($this->request, $this->response);
217 $form->setCurrentSiteLanguage($this->buildFakeSiteLanguage(0, 0));
218 $form->overrideCurrentPage($pageIndex);
219
220 return $form->render();
221 }
222
223 /**
224 * Build a SiteLanguage object to render the form preview with a
225 * specific language.
226 *
227 * @param int $pageId
228 * @param int $languageId
229 * @return SiteLanguage
230 */
231 protected function buildFakeSiteLanguage(int $pageId, int $languageId): SiteLanguage
232 {
233 $fakeSiteConfiguration = [
234 'languages' => [
235 [
236 'languageId' => $languageId,
237 'title' => 'Dummy',
238 'navigationTitle' => '',
239 'typo3Language' => '',
240 'flag' => '',
241 'locale' => '',
242 'iso-639-1' => '',
243 'hreflang' => '',
244 'direction' => '',
245 ],
246 ],
247 ];
248
249 /** @var \TYPO3\CMS\Core\Site\Entity\SiteLanguage $currentSiteLanguage */
250 $currentSiteLanguage = GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)
251 ->getLanguageById($languageId);
252 return $currentSiteLanguage;
253 }
254
255 /**
256 * Prepare the formElements.*.formEditor section from the YAML settings.
257 * Sort all formElements into groups and add additional data.
258 *
259 * @param array $formElementsDefinition
260 * @return array
261 */
262 protected function getInsertRenderablesPanelConfiguration(array $formElementsDefinition): array
263 {
264 /** @var array<string, array<string,string>> $formElementsByGroup */
265 $formElementsByGroup = [];
266
267 foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
268 if (!isset($formElementConfiguration['group'])) {
269 continue;
270 }
271 if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
272 $formElementsByGroup[$formElementConfiguration['group']] = [];
273 }
274
275 $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
276 $formElementConfiguration,
277 $this->prototypeConfiguration['formEditor']['translationFiles'] ?? []
278 );
279
280 $formElementsByGroup[$formElementConfiguration['group']][] = [
281 'key' => $formElementName,
282 'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
283 'label' => $formElementConfiguration['label'],
284 'sorting' => $formElementConfiguration['groupSorting'],
285 'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
286 ];
287 }
288
289 $formGroups = [];
290 foreach ($this->prototypeConfiguration['formEditor']['formElementGroups'] ?? [] as $groupName => $groupConfiguration) {
291 if (!isset($formElementsByGroup[$groupName])) {
292 continue;
293 }
294
295 usort($formElementsByGroup[$groupName], function ($a, $b) {
296 return $a['sorting'] - $b['sorting'];
297 });
298 unset($formElementsByGroup[$groupName]['sorting']);
299
300 $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
301 $groupConfiguration,
302 $this->prototypeConfiguration['formEditor']['translationFiles'] ?? []
303 );
304
305 $formGroups[] = [
306 'key' => $groupName,
307 'elements' => $formElementsByGroup[$groupName],
308 'label' => $groupConfiguration['label'],
309 ];
310 }
311
312 return $formGroups;
313 }
314
315 /**
316 * Reduce the YAML settings by the 'formEditor' keyword.
317 *
318 * @return array
319 */
320 protected function getFormEditorDefinitions(): array
321 {
322 $formEditorDefinitions = [];
323 foreach ([$this->prototypeConfiguration, $this->prototypeConfiguration['formEditor']] as $configuration) {
324 foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
325 if (substr($firstLevelItemKey, -10) !== 'Definition') {
326 continue;
327 }
328 $reducedKey = substr($firstLevelItemKey, 0, -10);
329 foreach ($configuration[$firstLevelItemKey] as $formEditorDefinitionKey => $formEditorDefinitionValue) {
330 if (isset($formEditorDefinitionValue['formEditor'])) {
331 $formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
332 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
333 } else {
334 $formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
335 }
336 }
337 }
338 }
339 $formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
340 $formEditorDefinitions = TranslationService::getInstance()->translateValuesRecursive(
341 $formEditorDefinitions,
342 $this->prototypeConfiguration['formEditor']['translationFiles'] ?? []
343 );
344 return $formEditorDefinitions;
345 }
346
347 /**
348 * Registers the Icons into the docheader
349 *
350 * @throws \InvalidArgumentException
351 */
352 protected function registerDocheaderButtons()
353 {
354 /** @var ButtonBar $buttonBar */
355 $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
356 $getVars = $this->request->getArguments();
357
358 if (isset($getVars['action']) && $getVars['action'] === 'index') {
359 $newPageButton = $buttonBar->makeInputButton()
360 ->setDataAttributes(['action' => 'formeditor-new-page', 'identifier' => 'headerNewPage'])
361 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.new_page_button'))
362 ->setName('formeditor-new-page')
363 ->setValue('new-page')
364 ->setClasses('t3-form-element-new-page-button hidden')
365 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
366 /** @var UriBuilder $uriBuilder */
367 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
368
369 $closeButton = $buttonBar->makeLinkButton()
370 ->setDataAttributes(['identifier' => 'closeButton'])
371 ->setHref((string)$uriBuilder->buildUriFromRoute('web_FormFormbuilder'))
372 ->setClasses('t3-form-element-close-form-button hidden')
373 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
374 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
375
376 $saveButton = $buttonBar->makeInputButton()
377 ->setDataAttributes(['identifier' => 'saveButton'])
378 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
379 ->setName('formeditor-save-form')
380 ->setValue('save')
381 ->setClasses('t3-form-element-save-form-button hidden')
382 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
383 ->setShowLabelText(true);
384
385 $formSettingsButton = $buttonBar->makeInputButton()
386 ->setDataAttributes(['identifier' => 'formSettingsButton'])
387 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.form_settings_button'))
388 ->setName('formeditor-form-settings')
389 ->setValue('settings')
390 ->setClasses('t3-form-element-form-settings-button hidden')
391 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-system-extension-configure', Icon::SIZE_SMALL))
392 ->setShowLabelText(true);
393
394 $undoButton = $buttonBar->makeInputButton()
395 ->setDataAttributes(['identifier' => 'undoButton'])
396 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
397 ->setName('formeditor-undo-form')
398 ->setValue('undo')
399 ->setClasses('t3-form-element-undo-form-button hidden disabled')
400 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-edit-undo', Icon::SIZE_SMALL));
401
402 $redoButton = $buttonBar->makeInputButton()
403 ->setDataAttributes(['identifier' => 'redoButton'])
404 ->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
405 ->setName('formeditor-redo-form')
406 ->setValue('redo')
407 ->setClasses('t3-form-element-redo-form-button hidden disabled')
408 ->setIcon($this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-edit-redo', Icon::SIZE_SMALL));
409
410 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
411 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
412 $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
413 $buttonBar->addButton($formSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
414 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
415 $buttonBar->addButton($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
416 }
417 }
418
419 /**
420 * Render the "text/x-formeditor-template" templates.
421 *
422 * @param array $formEditorDefinitions
423 * @return string
424 */
425 protected function renderFormEditorTemplates(array $formEditorDefinitions): string
426 {
427 $fluidConfiguration = $this->prototypeConfiguration['formEditor']['formEditorFluidConfiguration'] ?? null;
428 $formEditorPartials = $this->prototypeConfiguration['formEditor']['formEditorPartials'] ?? null;
429
430 if (!isset($fluidConfiguration['templatePathAndFilename'])) {
431 throw new RenderingException(
432 'The option templatePathAndFilename must be set.',
433 1485636499
434 );
435 }
436 if (
437 !isset($fluidConfiguration['layoutRootPaths'])
438 || !is_array($fluidConfiguration['layoutRootPaths'])
439 ) {
440 throw new RenderingException(
441 'The option layoutRootPaths must be set.',
442 1480294721
443 );
444 }
445 if (
446 !isset($fluidConfiguration['partialRootPaths'])
447 || !is_array($fluidConfiguration['partialRootPaths'])
448 ) {
449 throw new RenderingException(
450 'The option partialRootPaths must be set.',
451 1480294722
452 );
453 }
454
455 $insertRenderablesPanelConfiguration = $this->getInsertRenderablesPanelConfiguration($formEditorDefinitions['formElements']);
456
457 $view = $this->objectManager->get(TemplateView::class);
458 $view->setControllerContext(clone $this->controllerContext);
459 $view->getRenderingContext()->getTemplatePaths()->fillFromConfigurationArray($fluidConfiguration);
460 $view->setTemplatePathAndFilename($fluidConfiguration['templatePathAndFilename']);
461 $view->assignMultiple([
462 'insertRenderablesPanelConfiguration' => $insertRenderablesPanelConfiguration,
463 'formEditorPartials' => $formEditorPartials,
464 ]);
465
466 return $view->render();
467 }
468
469 /**
470 * @todo move this to FormDefinitionConversionService
471 * @param array $formDefinition
472 * @return array
473 */
474 protected function transformFormDefinitionForFormEditor(array $formDefinition): array
475 {
476 $multiValueFormElementProperties = [];
477 $multiValueFinisherProperties = [];
478
479 foreach ($this->prototypeConfiguration['formElementsDefinition'] as $type => $configuration) {
480 if (!isset($configuration['formEditor']['editors'])) {
481 continue;
482 }
483 foreach ($configuration['formEditor']['editors'] as $editorConfiguration) {
484 if ($editorConfiguration['templateName'] === 'Inspector-PropertyGridEditor') {
485 $multiValueFormElementProperties[$type][] = $editorConfiguration['propertyPath'];
486 }
487 }
488 }
489
490 foreach ($this->prototypeConfiguration['formElementsDefinition']['Form']['formEditor']['propertyCollections']['finishers'] ?? [] as $configuration) {
491 if (!isset($configuration['editors'])) {
492 continue;
493 }
494
495 foreach ($configuration['editors'] as $editorConfiguration) {
496 if ($editorConfiguration['templateName'] === 'Inspector-PropertyGridEditor') {
497 $multiValueFinisherProperties[$configuration['identifier']][] = $editorConfiguration['propertyPath'];
498 }
499 }
500 }
501
502 $formDefinition = $this->filterEmptyArrays($formDefinition);
503 $formDefinition = $this->migrateTranslationFileOptions($formDefinition);
504 $formDefinition = $this->migrateEmailFinisherRecipients($formDefinition);
505 $formDefinition = $this->migrateEmailFormatOption($formDefinition);
506
507 // @todo: replace with rte parsing
508 $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
509 $formDefinition = $this->transformMultiValuePropertiesForFormEditor(
510 $formDefinition,
511 'type',
512 $multiValueFormElementProperties
513 );
514 $formDefinition = $this->transformMultiValuePropertiesForFormEditor(
515 $formDefinition,
516 'identifier',
517 $multiValueFinisherProperties
518 );
519
520 $formDefinitionConversionService = $this->getFormDefinitionConversionService();
521 $formDefinition = $formDefinitionConversionService->addHmacData($formDefinition);
522
523 return $formDefinition;
524 }
525
526 /**
527 * Some data needs a transformation before it can be used by the
528 * form editor. This rules for multivalue elements like select
529 * elements. To ensure the right sorting if the data goes into
530 * javascript, we need to do transformations:
531 *
532 * [
533 * '5' => '5',
534 * '4' => '4',
535 * '3' => '3'
536 * ]
537 *
538 *
539 * This method transform this into:
540 *
541 * [
542 * [
543 * _label => '5'
544 * _value => 5
545 * ],
546 * [
547 * _label => '4'
548 * _value => 4
549 * ],
550 * [
551 * _label => '3'
552 * _value => 3
553 * ],
554 * ]
555 *
556 * @param array $formDefinition
557 * @param string $identifierProperty
558 * @param array $multiValueProperties
559 * @return array
560 */
561 protected function transformMultiValuePropertiesForFormEditor(
562 array $formDefinition,
563 string $identifierProperty,
564 array $multiValueProperties
565 ): array {
566 $output = $formDefinition;
567 foreach ($formDefinition as $key => $value) {
568 $identifier = $value[$identifierProperty] ?? null;
569
570 if (array_key_exists($identifier, $multiValueProperties)) {
571 $multiValuePropertiesForIdentifier = $multiValueProperties[$identifier];
572
573 foreach ($multiValuePropertiesForIdentifier as $multiValueProperty) {
574 if (!ArrayUtility::isValidPath($value, $multiValueProperty, '.')) {
575 continue;
576 }
577
578 $multiValuePropertyData = ArrayUtility::getValueByPath($value, $multiValueProperty, '.');
579
580 if (!is_array($multiValuePropertyData)) {
581 continue;
582 }
583
584 $newMultiValuePropertyData = [];
585
586 foreach ($multiValuePropertyData as $k => $v) {
587 $newMultiValuePropertyData[] = [
588 '_label' => $v,
589 '_value' => $k,
590 ];
591 }
592
593 $value = ArrayUtility::setValueByPath($value, $multiValueProperty, $newMultiValuePropertyData, '.');
594 }
595 }
596
597 $output[$key] = $value;
598
599 if (is_array($value)) {
600 $output[$key] = $this->transformMultiValuePropertiesForFormEditor(
601 $value,
602 $identifierProperty,
603 $multiValueProperties
604 );
605 }
606 }
607
608 return $output;
609 }
610
611 /**
612 * Remove keys from an array if the key value is an empty array
613 *
614 * @param array $array
615 * @return array
616 */
617 protected function filterEmptyArrays(array $array): array
618 {
619 foreach ($array as $key => $value) {
620 if (!is_array($value)) {
621 continue;
622 }
623 if (empty($value)) {
624 unset($array[$key]);
625 continue;
626 }
627 $array[$key] = $this->filterEmptyArrays($value);
628 if (empty($array[$key])) {
629 unset($array[$key]);
630 }
631 }
632
633 return $array;
634 }
635
636 /**
637 * Migrate singular "translationFile" options to plural "translationFiles"
638 *
639 * @param array $formDefinition
640 * @return array
641 * @deprecated since v10 and will be removed in TYPO3 v11
642 */
643 protected function migrateTranslationFileOptions(array $formDefinition): array
644 {
645 GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
646 GeneralUtility::makeInstance(
647 ArrayProcessing::class,
648 'translationFile',
649 '((.+)\.translationFile)(?:\.|$)',
650 function ($key, $value, $matches) use (&$formDefinition) {
651 [, $singleOptionPath, $parentOptionPath] = $matches;
652
653 try {
654 $translationFiles = ArrayUtility::getValueByPath($formDefinition, $singleOptionPath, '.');
655 } catch (MissingArrayPathException $e) {
656 // Already migrated by a previous "translationFile.N" entry
657 return;
658 }
659
660 if (is_string($translationFiles)) {
661 // 10 is usually used by EXT:form
662 $translationFiles = [20 => $translationFiles];
663 }
664
665 $formDefinition = ArrayUtility::setValueByPath(
666 $formDefinition,
667 sprintf('%s.translationFiles', $parentOptionPath),
668 $translationFiles,
669 '.'
670 );
671 $formDefinition = ArrayUtility::removeByPath($formDefinition, $singleOptionPath, '.');
672
673 return $value;
674 }
675 )
676 );
677
678 return $formDefinition;
679 }
680
681 /**
682 * Migrate single recipient options to their list successors
683 *
684 * @param array $formDefinition
685 * @return array
686 */
687 protected function migrateEmailFinisherRecipients(array $formDefinition): array
688 {
689 foreach ($formDefinition['finishers'] ?? [] as $i => $finisherConfiguration) {
690 if (!in_array($finisherConfiguration['identifier'], ['EmailToSender', 'EmailToReceiver'], true)) {
691 continue;
692 }
693
694 $recipientAddress = $finisherConfiguration['options']['recipientAddress'] ?? '';
695 $recipientName = $finisherConfiguration['options']['recipientName'] ?? '';
696 $carbonCopyAddress = $finisherConfiguration['options']['carbonCopyAddress'] ?? '';
697 $blindCarbonCopyAddress = $finisherConfiguration['options']['blindCarbonCopyAddress'] ?? '';
698
699 if (!empty($recipientAddress)) {
700 $finisherConfiguration['options']['recipients'][$recipientAddress] = $recipientName;
701 }
702
703 if (!empty($carbonCopyAddress)) {
704 $finisherConfiguration['options']['carbonCopyRecipients'][$carbonCopyAddress] = '';
705 }
706
707 if (!empty($blindCarbonCopyAddress)) {
708 $finisherConfiguration['options']['blindCarbonCopyRecipients'][$blindCarbonCopyAddress] = '';
709 }
710
711 unset(
712 $finisherConfiguration['options']['recipientAddress'],
713 $finisherConfiguration['options']['recipientName'],
714 $finisherConfiguration['options']['carbonCopyAddress'],
715 $finisherConfiguration['options']['blindCarbonCopyAddress']
716 );
717 $formDefinition['finishers'][$i] = $finisherConfiguration;
718 }
719
720 return $formDefinition;
721 }
722
723 /**
724 * Migrate email "format" option to "addHtmlPart"
725 *
726 * @param array $formDefinition
727 * @return array
728 * @deprecated since v10 and will be removed in TYPO3 v11
729 */
730 protected function migrateEmailFormatOption(array $formDefinition): array
731 {
732 foreach ($formDefinition['finishers'] ?? [] as $i => $finisherConfiguration) {
733 if (!in_array($finisherConfiguration['identifier'], ['EmailToSender', 'EmailToReceiver'], true)) {
734 continue;
735 }
736
737 $format = $finisherConfiguration['options']['format'] ?? null;
738
739 if (!empty($format)) {
740 $finisherConfiguration['options']['addHtmlPart'] = empty($format) || $format !== EmailFinisher::FORMAT_PLAINTEXT;
741 }
742
743 unset($finisherConfiguration['options']['format']);
744 $formDefinition['finishers'][$i] = $finisherConfiguration;
745 }
746
747 return $formDefinition;
748 }
749
750 /**
751 * @return FormDefinitionConversionService
752 */
753 protected function getFormDefinitionConversionService(): FormDefinitionConversionService
754 {
755 return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
756 }
757
758 /**
759 * Returns the current BE user.
760 *
761 * @return BackendUserAuthentication
762 */
763 protected function getBackendUser(): BackendUserAuthentication
764 {
765 return $GLOBALS['BE_USER'];
766 }
767
768 /**
769 * Returns the language service
770 *
771 * @return LanguageService
772 */
773 protected function getLanguageService(): LanguageService
774 {
775 return $GLOBALS['LANG'];
776 }
777
778 /**
779 * Returns the page renderer
780 *
781 * @return PageRenderer
782 */
783 protected function getPageRenderer(): PageRenderer
784 {
785 return GeneralUtility::makeInstance(PageRenderer::class);
786 }
787 }