getFormSettings(); if (!isset($formSettings['prototypes'][$prototypeName])) { throw new PrototypeNotFoundException(sprintf('The Prototype "%s" was not found.', $prototypeName), 1475924277); } return $formSettings['prototypes'][$prototypeName]; } /** * Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier" * * @internal */ public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array { $formSettings = $this->getFormSettings(); $returnValue = GeneralUtility::makeInstance( ArrayProcessor::class, $formSettings['formManager']['selectablePrototypesConfiguration'] ?? [] )->forEach( GeneralUtility::makeInstance( ArrayProcessing::class, 'selectablePrototypeNames', '^([\d]+)\.identifier$', static function ($_, $value) { return $value; } ) ); return array_values($returnValue['selectablePrototypeNames'] ?? []); } /** * Check if a form element property is defined in the form setup. * If a form element property is defined in the form setup then it * means that the form element property can be written by the form editor. * A form element property can be written if the property path is defined within * the following form editor properties: * * formElementsDefinition..formEditor.editors..propertyPath * * formElementsDefinition..formEditor.editors..*.propertyPath * * formElementsDefinition..formEditor.editors..additionalElementPropertyPaths * * formElementsDefinition..formEditor.propertyCollections...editors..additionalElementPropertyPaths * If a form editor property "templateName" is * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor" * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty". * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid. * If "formElementsDefinition..formEditor.editors..templateName = Inspector-PropertyGridEditor" * and * "formElementsDefinition..formEditor.editors..propertyPath = options.xxx" * then (for example) "options.xxx.yyy" is a valid property path to write. * If you use a custom form editor "inspector editor" implementation which does not define the writable * property paths by one of the above described inspector editor properties (e.g "propertyPath") within * the form setup, you must provide the writable property paths via the * AfterFormDefinitionValidationConfigurationIsBuiltEvent PSR-14 event. * * @internal */ public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? []; return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig); } /** * Check if a form elements finisher|validator property is defined in the form setup. * If a form elements finisher|validator property is defined in the form setup then it * means that the form elements finisher|validator property can be written by the form editor. * A form elements finisher|validator property can be written if the property path is defined within * the following form editor properties: * * formElementsDefinition..formEditor.propertyCollections...editors..propertyPath * * formElementsDefinition..formEditor.propertyCollections...editors..*.propertyPath * If a form elements finisher|validator property "templateName" is * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor" * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty". * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid. * If "formElementsDefinition..formEditor.propertyCollections...editors..templateName = Inspector-PropertyGridEditor" * and * "formElementsDefinition..formEditor.propertyCollections...editors..propertyPath = options.xxx" * that (for example) "options.xxx.yyy" is a valid property path to write. * If you use a custom form elements finisher|validator editor implementation which does not define the writable * property paths by one of the above described inspector editor properties (e.g "propertyPath") within * the form setup, you must provide the writable property paths via the * AfterFormDefinitionValidationConfigurationIsBuiltEvent PSR-14 event. * * @internal */ public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? []; return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig); } /** * If a form element editor has a property called "selectOptions" * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor") * then only the defined values within the selectOptions are allowed to be written * by the form editor. * * @internal */ public function formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup( ValidationDto $dto ): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto); return isset( $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['selectOptions'][$propertyPath] ); } /** * Get the "selectOptions" value for a form element property from the form setup. * * @throws PropertyException * @internal */ public function getAllowedValuesForFormElementPropertyFromFormEditorSetup( ValidationDto $dto, bool $translated = true ): array { if (!$this->formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) { throw new PropertyException( sprintf( 'No selectOptions found for form element type "%s" and property path "%s"', $dto->getFormElementType(), $dto->getPropertyPath() ), 1614264312 ); } $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions'; $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto); return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$propertyPath]; } /** * If a form elements finisher|validator editor has a property called "selectOptions" * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor") * then only the defined values within the selectOptions are allowed to be written * by the form editor. * * @internal */ public function propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup( ValidationDto $dto ): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto); return isset( $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['selectOptions'][$propertyPath] ); } /** * Get the "selectOptions" value for a form elements finisher|validator property from the form setup. * * @throws PropertyException * @internal */ public function getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup( ValidationDto $dto, bool $translated = true ): array { if (!$this->propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) { throw new PropertyException( sprintf( 'No selectOptions found for property collection "%s" and identifier "%s" and property path "%s"', $dto->getPropertyCollectionName(), $dto->getPropertyCollectionElementIdentifier(), $dto->getPropertyPath() ), 1614264313 ); } $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions'; $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto); return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$propertyPath]; } protected function getBasePropertyPathFromMultiValueFormElementProperty(ValidationDto $dto): string { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $propertyPath = $dto->getPropertyPath(); $multiValueProperties = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['multiValueProperties'] ?? []; foreach ($multiValueProperties as $multiValueProperty) { if (str_starts_with($propertyPath, $multiValueProperty)) { $propertyPath = $multiValueProperty; } } return $propertyPath; } protected function getBasePropertyPathFromMultiValuePropertyCollectionElement(ValidationDto $dto): string { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $propertyPath = $dto->getPropertyPath(); $multiValueProperties = $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['multiValueProperties'] ?? []; foreach ($multiValueProperties as $multiValueProperty) { if (str_starts_with($propertyPath, $multiValueProperty)) { $propertyPath = $multiValueProperty; } } return $propertyPath; } /** * Check if a form element property is defined in "predefinedDefaults" in the form setup. * If a form element property is defined in the "predefinedDefaults" in the form setup then it * means that the form element property can be written by the form editor. * A form element default property is defined within the following form editor properties: * * formElementsDefinition..formEditor.predefinedDefaults. = "default value" * * @internal */ public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(ValidationDto $dto): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); return isset( $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()] ); } /** * Get the "predefinedDefaults" value for a form element property from the form setup. * A form element default property is defined within the following form editor properties: * * formElementsDefinition..formEditor.predefinedDefaults. = "default value" * * @throws PropertyException * @internal */ public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true): mixed { if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) { throw new PropertyException( sprintf( 'No predefinedDefaults found for form element type "%s" and property path "%s"', $dto->getFormElementType(), $dto->getPropertyPath() ), 1528578401 ); } $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults'; return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$dto->getPropertyPath()]; } /** * Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup. * If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it * means that the form elements finisher|validator property can be written by the form editor. * A form elements finisher|validator default property is defined within the following form editor properties: * * ..formEditor.predefinedDefaults. = "default value" * * @internal */ public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(ValidationDto $dto): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); return isset( $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()] ); } /** * Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup. * A form elements finisher|validator default property is defined within the following form editor properties: * * ..formEditor.predefinedDefaults. = "default value" * * @throws PropertyException * @internal */ public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true): mixed { if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) { throw new PropertyException( sprintf( 'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"', $dto->getPropertyCollectionName(), $dto->getPropertyCollectionElementIdentifier(), $dto->getPropertyPath() ), 1528578402 ); } $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults'; return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$dto->getPropertyPath()]; } /** * Check if the form element is creatable through the form editor. * A form element is creatable if the following properties are set: * * formElementsDefinition..formEditor.group * * formElementsDefinition..formEditor.groupSorting * And the value from "formElementsDefinition..formEditor.group" is * one of the keys within "formEditor.formElementGroups" * * @internal */ public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool { if ($dto->getFormElementType() === 'Form') { return true; } $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false; } /** * Check if the form elements finisher|validator is creatable through the form editor. * A form elements finisher|validator is creatable if the following conditions are true: * "formElementsDefinition..formEditor.editors..templateName = Inspector-FinishersEditor" * or * "formElementsDefinition..formEditor.editors..templateName = Inspector-ValidatorsEditor" * and * "formElementsDefinition..formEditor.editors..selectOptions..value = " * * @internal */ public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool { $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup( $dto->getPrototypeName() ); return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false; } /** * Check if the form elements type is defined within the form setup. * * @internal */ public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool { $prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName()); return ArrayUtility::isValidPath( $prototypeConfiguration, 'formElementsDefinition.' . $dto->getFormElementType(), '.' ); } /** * @internal */ public function getAllBackendTranslationsForTranslationKeys(array $keys, string $prototypeName): array { $translations = []; foreach ($keys as $key) { if (!is_string($key)) { continue; } $translations[$key] = $this->getAllBackendTranslationsForTranslationKey($key, $prototypeName); } return $translations; } public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array { $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName); return $this->translationService->translateToAllBackendLanguages( $key, [], $prototypeConfiguration['formEditor']['translationFiles'] ?? [] ); } protected function getFormSettings(): array { // @todo: This is needed for extFormConfigurationManager to apply stdWrap on TS configuration. // Find a way to get rid of this. $isFrontend = false; $request = $GLOBALS['TYPO3_REQUEST'] ?? null; if ($request instanceof ServerRequestInterface) { $isFrontend = ApplicationType::fromRequest($request)->isFrontend(); } // @todo: Note this code relies on the fact that the request has been set to ExtbaseConfigurationManagerInterface already. $typoScriptSettings = $this->extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form'); return $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, $isFrontend, $isFrontend ? $request : null); } /** * Collect all the form editor configurations which are needed to check if a * form definition property can be written or not. */ protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array { $cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]); $configuration = $this->getCacheEntry($cacheKey); if ($configuration === null) { $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName); $extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration); GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach( GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementPropertyPaths', '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$', GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementAdditionalElementPropertyPaths', '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)', GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementRelativeMultiValueProperties', '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$', GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementSelectOptions', '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$', GeneralUtility::makeInstance(SelectOptionsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementPredefinedDefaults', '^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$', GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'formElementCreatable', '^formElementsDefinition\.(.*)\.formEditor.group$', GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionCreatable', '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$', GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionPropertyPaths', '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$', GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionAdditionalElementPropertyPaths', '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)', GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionRelativeMultiValueProperties', '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$', GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionSelectOptions', '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$', GeneralUtility::makeInstance(CollectionSelectOptionsExtractor::class, $extractorDto) ), GeneralUtility::makeInstance( ArrayProcessing::class, 'propertyCollectionPredefinedDefaults', '^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$', GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto) ) ); $configuration = $extractorDto->getResult(); $configuration = $this->translateValues($prototypeConfiguration, $configuration); $configuration = $this->eventDispatcher ->dispatch(new AfterFormDefinitionValidationConfigurationIsBuiltEvent($prototypeName, $configuration)) ->getConfiguration(); $this->setCacheEntry($cacheKey, $configuration); } return $configuration; } protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool { if (empty($subConfig)) { return false; } if (in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true) || in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true) || in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true) ) { return true; } foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) { if (str_starts_with($propertyPath, $relativeMultiValueProperty)) { return true; } } return false; } protected function translateValues(array $prototypeConfiguration, array $configuration): array { if (isset($configuration['formElements'])) { $configuration['formElements'] = $this->translatePredefinedDefaults( $prototypeConfiguration, $configuration['formElements'] ); $configuration['formElements'] = $this->translateSelectOptions( $prototypeConfiguration, $configuration['formElements'] ); } foreach ($configuration['collections'] ?? [] as $name => $collections) { $configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections); $configuration['collections'][$name] = $this->translateSelectOptions($prototypeConfiguration, $configuration['collections'][$name]); } return $configuration; } protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array { foreach ($formElements as $name => $formElement) { if (!isset($formElement['predefinedDefaults'])) { continue; } $formElement['untranslatedPredefinedDefaults'] = $formElement['predefinedDefaults']; $formElement['predefinedDefaults'] = $this->translationService->translateValuesRecursive( $formElement['predefinedDefaults'], $prototypeConfiguration['formEditor']['translationFiles'] ?? [] ); $formElements[$name] = $formElement; } return $formElements; } protected function translateSelectOptions(array $prototypeConfiguration, array $formElements): array { foreach ($formElements as $name => $formElement) { if (empty($formElement['selectOptions']) || !is_array($formElement['selectOptions'])) { continue; } $formElement['untranslatedSelectOptions'] = $formElement['selectOptions']; $formElement['selectOptions'] = $this->translationService->translateValuesRecursive( $formElement['selectOptions'], $prototypeConfiguration['formEditor']['translationFiles'] ?? [] ); $formElements[$name] = $formElement; } return $formElements; } protected function getCacheEntry(string $cacheKey): mixed { $cacheKey = 'form_' . $cacheKey; if ($this->runtimeCache->has($cacheKey)) { return $this->runtimeCache->get($cacheKey); } if ($this->assetsCache->has($cacheKey)) { return $this->assetsCache->get($cacheKey); } return null; } protected function setCacheEntry(string $cacheKey, mixed $value): void { $cacheKey = 'form_' . $cacheKey; $this->runtimeCache->set($cacheKey, $value); $this->assetsCache->set($cacheKey, $value); } }