first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Condition;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\ExpressionLanguage\AbstractProvider;
use TYPO3\CMS\Form\Domain\Condition\Functions\FormConditionFunctionsProvider;
/**
* Scope: frontend
* **This classis NOT meant to be sub classed by developers .**
*
* @internal
*/
class ConditionProvider extends AbstractProvider
{
public function __construct()
{
$this->expressionLanguageProviders = [
FormConditionFunctionsProvider::class
];
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Condition\Functions;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* @internal
*/
class FormConditionFunctionsProvider implements ExpressionFunctionProviderInterface
{
/**
* @return ExpressionFunction[] An array of Function instances
*/
public function getFunctions()
{
return [
$this->getFormValueFunction()
];
}
/**
* Shortcut function to access field values
*
* @return \Symfony\Component\ExpressionLanguage\ExpressionFunction
*/
protected function getFormValueFunction(): ExpressionFunction
{
return new ExpressionFunction(
'getFormValue',
function () {
// Not implemented, we only use the evaluator
},
function ($arguments, $field) {
return $arguments['formValues'][$field] ?? null;
}
);
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Helper for array processing
*
* Scope: frontend / backend
* @internal
*/
class ArrayProcessing
{
/**
* @var string
*/
protected $identifier;
/**
* @var string
*/
protected $expression;
/**
* @var callable
*/
protected $processor;
/**
* @param string $identifier
* @param string $expression
* @param callable $processor
*/
public function __construct(string $identifier, string $expression, callable $processor)
{
$this->identifier = $identifier;
$this->expression = $expression;
$this->processor = $processor;
}
/**
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @return string
*/
public function getExpression(): string
{
return $this->expression;
}
/**
* @return callable
*/
public function getProcessor(): callable
{
return $this->processor;
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\Exception\ArrayProcessorException;
/**
* Helper for array processing
*
* Scope: frontend / backend
* @internal
*/
class ArrayProcessor
{
/**
* @var array
*/
protected $data;
/**
* @param array $data
*/
public function __construct(array $data)
{
$this->data = ArrayUtility::flatten($data);
}
/**
* @param ArrayProcessing[] $processings
* @return array
*/
public function forEach(...$processings): array
{
$result = [];
$processings = $this->getValidProcessings($processings);
foreach ($this->data as $key => $value) {
foreach ($processings as $processing) {
// explicitly escaping non-escaped '#' which is used
// as PCRE delimiter in the following processing
$expression = preg_replace(
'/(?<!\\\\)#/',
'\\#',
$processing->getExpression()
);
if (preg_match('#' . $expression . '#', $key, $matches)) {
$identifier = $processing->getIdentifier();
$processor = $processing->getProcessor();
$result[$identifier] = $result[$identifier] ?? [];
$result[$identifier][$key] = $processor($key, $value, $matches);
}
}
}
return $result;
}
/**
* @param array $allProcessings
* @return ArrayProcessing[]
* @throws ArrayProcessorException
*/
protected function getValidProcessings(array $allProcessings): array
{
$validProcessings = [];
$identifiers = [];
foreach ($allProcessings as $processing) {
if ($processing instanceof ArrayProcessing) {
if (in_array($processing->getIdentifier(), $identifiers, true)) {
throw new ArrayProcessorException(
'ArrayProcessing identifier must be unique.',
1528638085
);
}
$identifiers[] = $processing->getIdentifier();
$validProcessings[] = $processing;
}
}
return $validProcessings;
}
}
@@ -0,0 +1,928 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AdditionalElementPropertyPathsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\ExtractorDto;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\IsCreatableFormElementExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\MultiValuePropertiesExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PredefinedDefaultsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PropertyPathsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\SelectOptionsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\IsCreatablePropertyCollectionElementExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\MultiValuePropertiesExtractor as CollectionMultiValuePropertiesExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PredefinedDefaultsExtractor as CollectionPredefinedDefaultsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PropertyPathsExtractor as CollectionPropertyPathsExtractor;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\SelectOptionsExtractor as CollectionSelectOptionsExtractor;
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Form\Service\TranslationService;
/**
* Helper for configuration settings
* Scope: frontend / backend
*/
class ConfigurationService implements SingletonInterface
{
/**
* @var array
*/
protected $formSettings;
/**
* @var array
*/
protected $firstLevelCache = [];
/**
* @var TranslationService
*/
protected $translationService;
/**
* @internal
*/
public function initializeObject(): void
{
$this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
->get(ConfigurationManagerInterface::class)
->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
}
/**
* Get the prototype configuration
*
* @param string $prototypeName name of the prototype to get the configuration for
* @return array the prototype configuration
* @throws PrototypeNotFoundException if prototype with the name $prototypeName was not found
*/
public function getPrototypeConfiguration(string $prototypeName): array
{
if (!isset($this->formSettings['prototypes'][$prototypeName])) {
throw new PrototypeNotFoundException(
sprintf('The Prototype "%s" was not found.', $prototypeName),
1475924277
);
}
return $this->formSettings['prototypes'][$prototypeName];
}
/**
* Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
*
* @return array
* @internal
*/
public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
{
$returnValue = GeneralUtility::makeInstance(
ArrayProcessor::class,
$this->formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'selectablePrototypeNames',
'^([\d]+)\.identifier$',
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.<formElementType>.formEditor.editors.<index>.propertyPath
* * formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath
* * formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths
* * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.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.<formElementType>.formEditor.editors.<index>.templateName = Inspector-PropertyGridEditor"
* and
* "formElementsDefinition.<formElementType>.formEditor.editors.<index>.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 with a hook.
*
* @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
* @param ValidationDto $dto
* @return bool
* @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.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath
* * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.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.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName = Inspector-PropertyGridEditor"
* and
* "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath = options.xxx"
* that (for example) "options.xxx.yyy" is a valid property path to write.
* If you use a custom form editor "inspector editor" implementation which not defines 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 with a hook.
*
* @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
* @param ValidationDto $dto
* @return bool
* @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.
*
* @param ValidationDto $dto
* @return bool
* @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.
*
* @param ValidationDto $dto
* @return array
* @param bool $translated
* @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.
*
* @param ValidationDto $dto
* @return bool
* @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.
*
* @param ValidationDto $dto
* @param bool $translated
* @return array
* @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];
}
/**
* @param ValidationDto $dto
* @return string
*/
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 (strpos($propertyPath, $multiValueProperty) === 0) {
$propertyPath = $multiValueProperty;
continue;
}
}
return $propertyPath;
}
/**
* @param ValidationDto $dto
* @return string
*/
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 (strpos($propertyPath, $multiValueProperty) === 0) {
$propertyPath = $multiValueProperty;
continue;
}
}
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.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
*
* @param ValidationDto $dto
* @return bool
* @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.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
*
* @param ValidationDto $dto
* @param bool $translated
* @return mixed
* @throws PropertyException
* @internal
*/
public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
{
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:
* * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
*
* @param ValidationDto $dto
* @return bool
* @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:
* * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
*
* @param ValidationDto $dto
* @param bool $translated
* @return mixed
* @throws PropertyException
* @internal
*/
public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
{
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.<formElementType>.formEditor.group
* * formElementsDefinition.<formElementType>.formEditor.groupSorting
* And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
* one of the keys within "formEditor.formElementGroups"
*
* @param ValidationDto $dto
* @return bool
* @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.<formElementType>.formEditor.editors.<index>.templateName = Inspector-FinishersEditor"
* or
* "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-ValidatorsEditor"
* and
* "formElementsDefinition.<formElementType>.formEditor.editors.<index>.selectOptions.<index>.value = <finisherIdentifier|validatorIdentifier>"
*
* @param ValidationDto $dto
* @return bool
* @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.
*
* @param ValidationDto $dto
* @return bool
* @internal
*/
public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
{
$prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
return ArrayUtility::isValidPath(
$prototypeConfiguration,
'formElementsDefinition.' . $dto->getFormElementType(),
'.'
);
}
/**
* @param array $keys
* @param string $prototypeName
* @return array
* @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;
}
/**
* @param string $key
* @param string $prototypeName
* @return array
*/
public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array
{
$prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
$translationFiles = $prototypeConfiguration['formEditor']['translationFile'] ?? [];
if (is_string($translationFiles)) {
$translationFiles = [$translationFiles];
}
return $this->getTranslationService()->translateToAllBackendLanguages(
$key,
[],
$translationFiles
);
}
/**
* Collect all the form editor configurations which are needed to check if a
* form definition property can be written or not.
*
* @param string $prototypeName
* @return array
*/
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->executeBuildFormDefinitionValidationConfigurationHooks(
$prototypeName,
$configuration
);
$this->setCacheEntry($cacheKey, $configuration);
}
return $configuration;
}
/**
* If you use a custom form editor "inspector editor" implementation which does not define the writable
* property paths by one of the described inspector editor properties (e.g "propertyPath") within
* the form setup, you must provide the writable property paths with a hook.
*
* @see $this->isFormElementPropertyDefinedInFormEditorSetup()
* @see $this->isPropertyCollectionPropertyDefinedInFormEditorSetup()
* Connect to the hook:
* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
* Use the hook:
* public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
* {
* $textValidationDto = $validationDto->withFormElementType('Text');
* $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
* $dateValidationDto = $validationDto->withFormElementType('Date');
* $propertyPaths = [
* $textValidationDto->withPropertyPath('properties.my.custom.property'),
* $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
* $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('StringLength')->withPropertyPath('options.custom.property'),
* $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.other.custom.property'),
* $dateValidationDto->withPropertyPath('properties.custom.property'),
* // ..
* ];
* return $propertyPaths;
* }
* @param string $prototypeName
* @param array $configuration
* @return array
* @throws PropertyException
*/
protected function executeBuildFormDefinitionValidationConfigurationHooks(
string $prototypeName,
array $configuration
): array {
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'addAdditionalPropertyPaths')) {
$validationDto = GeneralUtility::makeInstance(ValidationDto::class, $prototypeName);
$propertyPathsFromHook = $hookObj->addAdditionalPropertyPaths($validationDto);
if (!is_array($propertyPathsFromHook)) {
$message = 'Return value of "%s->addAdditionalPropertyPaths() must be type "array"';
throw new PropertyException(sprintf($message, $className), 1528633965);
}
$configuration = $this->addAdditionalPropertyPathsFromHook(
$className,
$prototypeName,
$propertyPathsFromHook,
$configuration
);
}
}
return $configuration;
}
/**
* @param string $hookClassName
* @param string $prototypeName
* @param array $propertyPathsFromHook
* @param array $configuration
* @return array
* @throws PropertyException
*/
protected function addAdditionalPropertyPathsFromHook(
string $hookClassName,
string $prototypeName,
array $propertyPathsFromHook,
array $configuration
): array {
foreach ($propertyPathsFromHook as $index => $validationDto) {
if (!($validationDto instanceof ValidationDto)) {
$message = 'Return value of "%s->addAdditionalPropertyPaths()[%s] must be an instance of "%s"';
throw new PropertyException(
sprintf($message, $hookClassName, $index, ValidationDto::class),
1528633966
);
}
if ($validationDto->getPrototypeName() !== $prototypeName) {
$message = 'The prototype name "%s" does not match "%s" on "%s->addAdditionalPropertyPaths()[%s]';
throw new PropertyException(
sprintf(
$message,
$validationDto->getPrototypeName(),
$prototypeName,
$hookClassName,
$index,
ValidationDto::class
),
1528634966
);
}
$formElementType = $validationDto->getFormElementType();
if (!$this->isFormElementTypeDefinedInFormSetup($validationDto)) {
$message = 'Form element type "%s" does not exists in prototype configuration "%s"';
throw new PropertyException(
sprintf($message, $formElementType, $validationDto->getPrototypeName()),
1528633967
);
}
if ($validationDto->hasPropertyCollectionName() &&
$validationDto->hasPropertyCollectionElementIdentifier()) {
$propertyCollectionName = $validationDto->getPropertyCollectionName();
$propertyCollectionElementIdentifier = $validationDto->getPropertyCollectionElementIdentifier();
if ($propertyCollectionName !== 'finishers' && $propertyCollectionName !== 'validators') {
$message = 'The property collection name "%s" for form element "%s" must be "finishers" or "validators"';
throw new PropertyException(
sprintf($message, $propertyCollectionName, $formElementType),
1528636941
);
}
$configuration['formElements'][$formElementType]['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['additionalPropertyPaths'][]
= $validationDto->getPropertyPath();
} else {
$configuration['formElements'][$formElementType]['additionalPropertyPaths'][]
= $validationDto->getPropertyPath();
}
}
return $configuration;
}
/**
* @param string $propertyPath
* @param array $subConfig
* @return bool
*/
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 (strpos($propertyPath, $relativeMultiValueProperty) === 0) {
return true;
}
}
return false;
}
/**
* @param array $prototypeConfiguration
* @param array $configuration
* @return array
*/
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;
}
/**
* @param array $prototypeConfiguration
* @param array $formElements
* @return array
*/
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->getTranslationService()->translateValuesRecursive(
$formElement['predefinedDefaults'],
$prototypeConfiguration['formEditor']['translationFile'] ?? null
);
$formElements[$name] = $formElement;
}
return $formElements;
}
/**
* @param array $prototypeConfiguration
* @param array $formElements
* @return array
*/
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->getTranslationService()->translateValuesRecursive(
$formElement['selectOptions'],
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
);
$formElements[$name] = $formElement;
}
return $formElements;
}
/**
* @param string $cacheKey
* @return mixed
*/
protected function getCacheEntry(string $cacheKey)
{
if (isset($this->firstLevelCache[$cacheKey])) {
return $this->firstLevelCache[$cacheKey];
}
return $this->getCacheFrontend()->has('form_' . $cacheKey)
? $this->getCacheFrontend()->get('form_' . $cacheKey)
: null;
}
/**
* @param string $cacheKey
* @param mixed $value
*/
protected function setCacheEntry(string $cacheKey, $value): void
{
$this->getCacheFrontend()->set('form_' . $cacheKey, $value);
$this->firstLevelCache[$cacheKey] = $value;
}
/**
* @return TranslationService
*/
protected function getTranslationService(): TranslationService
{
return $this->translationService instanceof TranslationService
? $this->translationService
: TranslationService::getInstance();
}
/**
* @return FrontendInterface
*/
protected function getCacheFrontend(): FrontendInterface
{
return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* @internal
*/
class ArrayProcessorException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if a form setup property was not found.
*/
class PropertyException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if a form prototype for a given name was not found.
*/
class PrototypeNotFoundException extends Exception
{
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
abstract class AbstractProcessor implements ProcessorInterface
{
/**
* @var ProcessorDto
*/
protected $converterDto;
/**
* @param ProcessorDto $converterDto
*/
public function __construct(ProcessorDto $converterDto)
{
$this->converterDto = $converterDto;
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generate a FlexForm element for a finisher option
*
* @internal
*/
class FinisherOptionGenerator extends AbstractProcessor
{
/**
* @var \TYPO3\CMS\Core\Localization\LanguageService
*/
protected $languageService;
/**
* @param ProcessorDto $converterDto
*/
public function __construct(ProcessorDto $converterDto)
{
parent::__construct($converterDto);
$this->languageService = GeneralUtility::makeInstance(LanguageService::class);
$this->languageService->includeLLFile('EXT:form/Resources/Private/Language/Database.xlf');
}
/**
* @param string $_ unused in this context
* @param mixed $__ unused in this context
* @param array $matches the expression matches from the ArrayProcessor - for example matches of ^(.*)\.config\.type$
*/
public function __invoke(string $_, $__, array $matches)
{
[, $optionKey] = $matches;
$finisherIdentifier = $this->converterDto->getFinisherIdentifier();
$finisherDefinitionFromSetup = $this->converterDto->getFinisherDefinitionFromSetup();
$finisherDefinitionFromFormDefinition = $this->converterDto->getFinisherDefinitionFromFormDefinition();
try {
$elementConfiguration = ArrayUtility::getValueByPath(
$finisherDefinitionFromSetup['FormEngine']['elements'],
$optionKey,
'.'
);
} catch (MissingArrayPathException $exception) {
return;
}
// use the option value from the ext:form setup from the current finisher as default value
try {
$optionValue = ArrayUtility::getValueByPath(
$finisherDefinitionFromSetup,
sprintf('options.%s', $optionKey),
'.'
);
} catch (MissingArrayPathException $exception) {
$optionValue = null;
}
// use the option value from the form definition from the current finisher (if exists) as default value
try {
$optionValue = ArrayUtility::getValueByPath(
$finisherDefinitionFromFormDefinition,
sprintf('options.%s', $optionKey),
'.'
);
} catch (MissingArrayPathException $exception) {
}
if (empty($optionValue)) {
$elementConfiguration['label'] .= sprintf(' (%s: "%s")', $this->languageService->getLL('default'), $this->languageService->getLL('empty'));
} else {
$elementConfiguration['label'] .= sprintf(' (%s: "' . $optionValue . '")', $this->languageService->getLL('default'));
}
$elementConfiguration['config']['default'] = $optionValue;
$sheetElements = $this->converterDto->getResult();
$sheetElements['settings.finishers.' . $finisherIdentifier . '.' . $optionKey]['TCEforms'] = $elementConfiguration;
$this->converterDto->setResult($sheetElements);
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Data container for finisher FlexForm processing
*
* @internal
*/
class ProcessorDto
{
/**
* @var string
*/
protected $finisherIdentifier;
/**
* @var array
*/
protected $finisherDefinitionFromSetup;
/**
* @var array
*/
protected $finisherDefinitionFromFormDefinition;
/**
* @var array
*/
protected $result = [];
/**
* @param string $finisherIdentifier
* @param array $finisherDefinitionFromSetup
* @param array $finisherDefinitionFromFormDefinition
*/
public function __construct(
string $finisherIdentifier,
array $finisherDefinitionFromSetup,
array $finisherDefinitionFromFormDefinition
) {
$this->finisherIdentifier = $finisherIdentifier;
$this->finisherDefinitionFromSetup = $finisherDefinitionFromSetup;
$this->finisherDefinitionFromFormDefinition = $finisherDefinitionFromFormDefinition;
}
/**
* @return string
*/
public function getFinisherIdentifier(): string
{
return $this->finisherIdentifier;
}
/**
* @return array
*/
public function getFinisherDefinitionFromSetup(): array
{
return $this->finisherDefinitionFromSetup;
}
/**
* @return array
*/
public function getFinisherDefinitionFromFormDefinition(): array
{
return $this->finisherDefinitionFromFormDefinition;
}
/**
* @return array
*/
public function getResult(): array
{
return $this->result;
}
/**
* @param array $result
* @return ProcessorDto
*/
public function setResult(array $result): ProcessorDto
{
$this->result = $result;
return $this;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Interface for FlexForm processors
*
* @internal
*/
interface ProcessorInterface
{
/**
* @param ProcessorDto $converterDto
*/
public function __construct(ProcessorDto $converterDto);
/**
* @param string $key
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $key, $value, array $matches);
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
abstract class AbstractConverter implements ConverterInterface
{
/**
* @var string
*/
protected $sessionToken;
/**
* @var ConverterDto
*/
protected $converterDto;
/**
* @param ConverterDto $converterDto
* @param string $sessionToken
*/
public function __construct(ConverterDto $converterDto, string $sessionToken = '')
{
$this->converterDto = $converterDto;
$this->sessionToken = $sessionToken;
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
/**
* @internal
*/
class AddHmacDataConverter extends AbstractConverter
{
/**
* Add a new value "_orig_<propertyName>" as a sibling of the property key.
* "_orig_<propertyName>" is an array which contains the property value
* and a hmac hash for the property value.
* "_orig_<propertyName>" will be used to validate the form definition on saving.
* @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService::validateFormDefinitionProperties()
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$formDefinition = $this->converterDto->getFormDefinition();
$renderablePathParts = explode('.', $key);
array_pop($renderablePathParts);
if (count($renderablePathParts) > 1) {
$renderablePath = implode('.', $renderablePathParts);
$currentFormElement = ArrayUtility::getValueByPath($formDefinition, $renderablePath, '.');
} else {
$currentFormElement = $formDefinition;
}
$propertyCollectionElements = $currentFormElement['finishers'] ?? $currentFormElement['validators'] ?? [];
$propertyCollectionName = $currentFormElement['type'] === 'Form' ? 'finishers' : 'validators';
unset($currentFormElement['renderables'], $currentFormElement['finishers'], $currentFormElement['validators']);
$this->converterDto
->setRenderablePathParts($renderablePathParts)
->setFormElementIdentifier($value);
GeneralUtility::makeInstance(ArrayProcessor::class, $currentFormElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'addHmacData',
'^(?!(.*\._label|.*\._value)$).*',
GeneralUtility::makeInstance(
AddHmacDataToFormElementPropertyConverter::class,
$this->converterDto,
$this->sessionToken
)
)
);
$this->converterDto->setPropertyCollectionName($propertyCollectionName);
foreach ($propertyCollectionElements as $propertyCollectionIndex => $propertyCollectionElement) {
$this->converterDto
->setPropertyCollectionIndex((int)$propertyCollectionIndex)
->setPropertyCollectionElementIdentifier($propertyCollectionElement['identifier']);
GeneralUtility::makeInstance(ArrayProcessor::class, $propertyCollectionElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'addHmacData',
'^(?!(.*\._label|.*\._value)$).*',
GeneralUtility::makeInstance(
AddHmacDataToPropertyCollectionElementConverter::class,
$this->converterDto,
$this->sessionToken
)
)
);
}
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
class AddHmacDataToFormElementPropertyConverter extends AbstractConverter
{
/**
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$formDefinition = $this->converterDto->getFormDefinition();
$propertyPathParts = explode('.', $key);
$lastKeySegment = array_pop($propertyPathParts);
$propertyPathParts[] = '_orig_' . $lastKeySegment;
$hmacValuePath = implode('.', array_merge($this->converterDto->getRenderablePathParts(), $propertyPathParts));
$hmacValue = [
'value' => $value,
'hmac' => GeneralUtility::hmac(serialize([$this->converterDto->getFormElementIdentifier(), $key, $value]), $this->sessionToken)
];
$formDefinition = ArrayUtility::setValueByPath($formDefinition, $hmacValuePath, $hmacValue, '.');
$this->converterDto->setFormDefinition($formDefinition);
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
class AddHmacDataToPropertyCollectionElementConverter extends AbstractConverter
{
/**
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$formDefinition = $this->converterDto->getFormDefinition();
$propertyPathParts = explode('.', $key);
$lastKeySegment = array_pop($propertyPathParts);
$propertyPathParts[] = '_orig_' . $lastKeySegment;
$hmacValuePath = implode('.', array_merge(
$this->converterDto->getRenderablePathParts(),
[$this->converterDto->getPropertyCollectionName(), $this->converterDto->getPropertyCollectionIndex()],
$propertyPathParts
));
$hmacValue = [
'value' => $value,
'hmac' => GeneralUtility::hmac(
serialize([
$this->converterDto->getFormElementIdentifier(),
$this->converterDto->getPropertyCollectionName(),
$this->converterDto->getPropertyCollectionElementIdentifier(),
$key,
$value
]),
$this->sessionToken
)
];
$formDefinition = ArrayUtility::setValueByPath($formDefinition, $hmacValuePath, $hmacValue, '.');
$this->converterDto->setFormDefinition($formDefinition);
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class ConverterDto
{
/**
* @var array
*/
protected $formDefinition = [];
/**
* @var array
*/
protected $renderablePathParts = [];
/**
* @var string
*/
protected $formElementIdentifier = '';
/**
* @var int
*/
protected $propertyCollectionIndex = 0;
/**
* @var string
*/
protected $propertyCollectionName = '';
/**
* @var string
*/
protected $propertyCollectionElementIdentifier = '';
/**
* @param array $formDefinition
*/
public function __construct(array $formDefinition)
{
$this->formDefinition = $formDefinition;
}
/**
* @return array
*/
public function getFormDefinition(): array
{
return $this->formDefinition;
}
/**
* @param array $formDefinition
* @return ConverterDto
*/
public function setFormDefinition(array $formDefinition): ConverterDto
{
$this->formDefinition = $formDefinition;
return $this;
}
/**
* @return array
*/
public function getRenderablePathParts(): array
{
return $this->renderablePathParts;
}
/**
* @param array $renderablePathParts
* @return ConverterDto
*/
public function setRenderablePathParts(array $renderablePathParts): ConverterDto
{
$this->renderablePathParts = $renderablePathParts;
return $this;
}
/**
* @return string
*/
public function getFormElementIdentifier(): string
{
return $this->formElementIdentifier;
}
/**
* @param string $formElementIdentifier
* @return ConverterDto
*/
public function setFormElementIdentifier(string $formElementIdentifier): ConverterDto
{
$this->formElementIdentifier = $formElementIdentifier;
return $this;
}
/**
* @return int
*/
public function getPropertyCollectionIndex(): int
{
return $this->propertyCollectionIndex;
}
/**
* @param int $propertyCollectionIndex
* @return ConverterDto
*/
public function setPropertyCollectionIndex(int $propertyCollectionIndex): ConverterDto
{
$this->propertyCollectionIndex = $propertyCollectionIndex;
return $this;
}
/**
* @return string
*/
public function getPropertyCollectionName(): string
{
return $this->propertyCollectionName;
}
/**
* @param string $propertyCollectionName
* @return ConverterDto
*/
public function setPropertyCollectionName(string $propertyCollectionName): ConverterDto
{
$this->propertyCollectionName = $propertyCollectionName;
return $this;
}
/**
* @return string
*/
public function getPropertyCollectionElementIdentifier(): string
{
return $this->propertyCollectionElementIdentifier;
}
/**
* @param string $propertyCollectionElementIdentifier
* @return ConverterDto
*/
public function setPropertyCollectionElementIdentifier(string $propertyCollectionElementIdentifier): ConverterDto
{
$this->propertyCollectionElementIdentifier = $propertyCollectionElementIdentifier;
return $this;
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
interface ConverterInterface
{
/**
* @param ConverterDto $converterDto
* @param string $sessionToken
*/
public function __construct(ConverterDto $converterDto, string $sessionToken = '');
/**
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value);
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
/**
* Apply FlexForm finisher option overrides
*
* @internal
*/
class FinisherOptionsFlexFormOverridesConverter
{
/**
* @var FlexFormFinisherOverridesConverterDto
*/
protected $converterDto;
/**
* @param FlexFormFinisherOverridesConverterDto $converterDto
*/
public function __construct(FlexFormFinisherOverridesConverterDto $converterDto)
{
$this->converterDto = $converterDto;
}
/**
* Used for overriding finisher options with flexform settings
* Flexform settings "win": When a setting is set in the form
* definition and in flexform the one in flexform will overwrite the
* one defined in the form definition.
*
* Here we adjust the parsed configuration and apply the overrides.
*
* @param string $_ unused in this context
* @param mixed $__ unused in this context
* @param array $matches the expression matches from the ArrayProcessor - for example matches of ^(.*)\.config\.type$
*/
public function __invoke(string $_, $__, array $matches): void
{
[, $optionKey] = $matches;
$finisherDefinition = $this->converterDto->getFinisherDefinition();
$finisherIdentifier = $this->converterDto->getFinisherIdentifier();
$flexFormSheetSettings = $this->converterDto->getFlexFormSheetSettings();
try {
$value = ArrayUtility::getValueByPath(
$flexFormSheetSettings['finishers'][$finisherIdentifier],
$optionKey,
'.'
);
} catch (MissingArrayPathException $exception) {
return;
}
$finisherDefinition = ArrayUtility::setValueByPath($finisherDefinition, 'options.' . $optionKey, $value, '.');
$this->converterDto->setFinisherDefinition($finisherDefinition);
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* @internal
*/
class FinisherTranslationLanguageConverter extends AbstractConverter
{
/**
* If "finishers.x.options.translation.language" is empty then set the value to "default" and remove
* the hmac.
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
if (!empty($value)) {
return;
}
$formDefinition = $this->converterDto->getFormDefinition();
$formDefinition = ArrayUtility::setValueByPath($formDefinition, $key, 'default', '.');
$hmacPropertyPathParts = explode('.', $key);
$lastKeySegment = array_pop($hmacPropertyPathParts);
$hmacPropertyPathParts[] = '_orig_' . $lastKeySegment;
$hmacValuePath = implode('.', $hmacPropertyPathParts);
if (ArrayUtility::isValidPath($formDefinition, $hmacValuePath, '.')) {
$formDefinition = ArrayUtility::removeByPath($formDefinition, $hmacValuePath, '.');
}
$this->converterDto->setFormDefinition($formDefinition);
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class FlexFormFinisherOverridesConverterDto
{
/**
* @var array
*/
protected $finisherDefinition = [];
/**
* @var string
*/
protected $finisherIdentifier = '';
/**
* @var array
*/
protected $flexFormSheetSettings = [];
/**
* @param array $finisherDefinition
* @param string $finisherIdentifier
* @param array $flexFormSheetSettings
*/
public function __construct(
array $finisherDefinition,
string $finisherIdentifier,
array $flexFormSheetSettings
) {
$this->finisherDefinition = $finisherDefinition;
$this->finisherIdentifier = $finisherIdentifier;
$this->flexFormSheetSettings = $flexFormSheetSettings;
}
/**
* @return array
*/
public function getFinisherDefinition(): array
{
return $this->finisherDefinition;
}
/**
* @param array $finisherDefinition
* @return FlexFormFinisherOverridesConverterDto
*/
public function setFinisherDefinition(array $finisherDefinition): FlexFormFinisherOverridesConverterDto
{
$this->finisherDefinition = $finisherDefinition;
return $this;
}
/**
* @return string
*/
public function getFinisherIdentifier(): string
{
return $this->finisherIdentifier;
}
/**
* @return array
*/
public function getFlexFormSheetSettings(): array
{
return $this->flexFormSheetSettings;
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* @internal
*/
class RemoveHmacDataConverter extends AbstractConverter
{
/**
* Remove the hmac data ("_orig_<propertyName>") for the corresponding property.
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$formDefinition = $this->converterDto->getFormDefinition();
$propertyPathParts = explode('.', $key);
array_pop($propertyPathParts);
$propertyPath = implode('.', $propertyPathParts);
$formDefinition = ArrayUtility::removeByPath($formDefinition, $propertyPath, '.');
$this->converterDto->setFormDefinition($formDefinition);
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
/**
* @internal
*/
abstract class AbstractValidator implements ValidatorInterface
{
/**
* @var ConfigurationService
*/
protected $configurationService;
/**
* @var array
*/
protected $currentElement;
/**
* @var string
*/
protected $sessionToken;
/**
* @var ValidationDto
*/
protected $validationDto;
/**
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
public function __construct(array $currentElement, string $sessionToken, ValidationDto $validationDto)
{
$this->currentElement = $currentElement;
$this->sessionToken = $sessionToken;
$this->validationDto = $validationDto;
}
/**
* Builds the path in which the hmac value is expected based on the property path.
*
* @param string $propertyPath
* @return string
*/
protected function buildHmacDataPath(string $propertyPath): string
{
$pathParts = explode('.', $propertyPath);
$lastPathSegment = array_pop($pathParts);
$pathParts[] = '_orig_' . $lastPathSegment;
return implode('.', $pathParts);
}
/**
* @return FormDefinitionValidationService
*/
protected function getFormDefinitionValidationService(): FormDefinitionValidationService
{
return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
}
/**
* @return ConfigurationService
*/
protected function getConfigurationService(): ConfigurationService
{
if (!($this->configurationService instanceof ConfigurationService)) {
$this->configurationService = $this->getObjectManager()->get(ConfigurationService::class);
}
return $this->configurationService;
}
/**
* @return ObjectManager
*/
protected function getObjectManager(): ObjectManager
{
return GeneralUtility::makeInstance(ObjectManager::class);
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
/**
* @internal
*/
abstract class CollectionBasedValidator extends AbstractValidator
{
/**
* Throws an exception if value from a property collection property
* does not match its hmac hash or if there is no hmac hash
* available for the value.
*
* @param array $currentElement
* @param mixed $value
* @param string $sessionToken
* @param ValidationDto $dto
* @throws PropertyException
*/
public function validatePropertyCollectionElementPropertyValueByHmacData(
array $currentElement,
$value,
string $sessionToken,
ValidationDto $dto
): void {
$hmacDataPath = $this->buildHmacDataPath($dto->getPropertyPath());
if (ArrayUtility::isValidPath($currentElement, $hmacDataPath, '.')) {
$hmacData = ArrayUtility::getValueByPath($currentElement, $hmacDataPath, '.');
$hmacContent = [
$dto->getFormElementIdentifier(),
$dto->getPropertyCollectionName(),
$dto->getPropertyCollectionElementIdentifier(),
$dto->getPropertyPath()
];
if (!$this->getFormDefinitionValidationService()->isPropertyValueEqualToHistoricalValue($hmacContent, $value, $hmacData, $sessionToken)) {
$message = 'The value "%s" of property "%s" (form element "%s" / "%s.%s") is not equal to the historical value "%s" #1528591586';
throw new PropertyException(
sprintf(
$message,
$value,
$dto->getPropertyPath(),
$dto->getFormElementIdentifier(),
$dto->getPropertyCollectionName(),
$dto->getPropertyCollectionElementIdentifier(),
$hmacData['value'] ?? ''
),
1528591586
);
}
} else {
$message = 'No hmac found for property "%s" (form element "%s" / "%s.%s") #1528591585';
throw new PropertyException(
sprintf(
$message,
$dto->getPropertyPath(),
$dto->getFormElementIdentifier(),
$dto->getPropertyCollectionName(),
$dto->getPropertyCollectionElementIdentifier()
),
1528591585
);
}
}
}
@@ -0,0 +1,172 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
/**
* @internal
*/
class CreatableFormElementPropertiesValidator extends ElementBasedValidator
{
/**
* Checks if the form element property is defined within the form editor setup
* or if the property is definied within the "predefinedDefaults" in the form editor setup
* and the property value matches the predefined value
* or if there is a valid hmac hash for the value.
* If the form element property is defined within the form editor setup
* and there is no valid hmac hash for the value
* and is the form element property configured to only allow a limited set of values,
* check the current (submitted) value against the allowed set of values (defined within the form setup).
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value)
{
$dto = $this->validationDto->withPropertyPath($key);
if ($this->getConfigurationService()->isFormElementPropertyDefinedInFormEditorSetup($dto)) {
if ($this->getConfigurationService()->formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
$this->validateFormElementValue($value, $dto);
}
} elseif (
$this->getConfigurationService()->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)
&& !ArrayUtility::isValidPath($this->currentElement, $this->buildHmacDataPath($dto->getPropertyPath()), '.')
) {
$this->validateFormElementPredefinedDefaultValue($value, $dto);
} else {
$this->validateFormElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
/**
* Throws an exception if the value from a form element property
* does not match the default value from the form editor setup.
*
* @param mixed $value
* @param ValidationDto $dto
* @throws PropertyException
*/
protected function validateFormElementPredefinedDefaultValue(
$value,
ValidationDto $dto
): void {
// If the form element is newely created, we have to compare the $value (form definition) with $predefinedDefaultValue (form setup)
// to check the integrity (at this time we don't have a hmac for the $value to check the integrity)
$predefinedDefaultValue = $this->getConfigurationService()->getFormElementPredefinedDefaultValueFromFormEditorSetup($dto);
if ($value !== $predefinedDefaultValue) {
$throwException = true;
if (is_string($predefinedDefaultValue)) {
// Last chance:
// Get all translations (from all backend languages) for the untranslated! $predefinedDefaultValue and
// compare the (already translated) $value (from the form definition) against the possible
// translations from $predefinedDefaultValue.
// Usecase:
// * backend language is EN
// * open the form edtior and add a ContentElement form element
// * switch to another browser tab and change the backend language to DE
// * clear the cache
// * go back to the form editor and click the save button
// Out of scope:
// * the same scenario as above + delete the previous chosen backend language within the maintenance tool
$untranslatedPredefinedDefaultValue = $this->getConfigurationService()->getFormElementPredefinedDefaultValueFromFormEditorSetup($dto, false);
$translations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKey(
$untranslatedPredefinedDefaultValue,
$dto->getPrototypeName()
);
if (in_array($value, $translations, true)) {
$throwException = false;
}
}
if ($throwException) {
$message = 'The value "%s" of property "%s" (form element "%s") is not equal to the default value "%s" #1528588035';
throw new PropertyException(
sprintf(
$message,
$value,
$dto->getPropertyPath(),
$dto->getFormElementIdentifier(),
$predefinedDefaultValue
),
1528588035
);
}
}
}
/**
* Throws an exception if the value from a form element property
* does not match the allowed set of values (defined within the form setup).
*
* @param mixed $value
* @param ValidationDto $dto
* @throws PropertyException
*/
protected function validateFormElementValue(
$value,
ValidationDto $dto
): void {
$allowedValues = $this->getConfigurationService()->getAllowedValuesForFormElementPropertyFromFormEditorSetup($dto);
if (!in_array($value, $allowedValues, true)) {
$untranslatedAllowedValues = $this->getConfigurationService()->getAllowedValuesForFormElementPropertyFromFormEditorSetup($dto, false);
// Compare the $value against the untranslated set of allowed values
if (in_array($value, $untranslatedAllowedValues, true)) {
// All good, $value is within the untranslated set of allowed values
return;
}
// Get all translations (from all backend languages) for the untranslated! $allowedValues and
// compare the (already translated) $value (from the form definition) against all possible
// translations for $untranslatedAllowedValues.
$allPossibleAllowedValuesTranslations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKeys(
$untranslatedAllowedValues,
$dto->getPrototypeName()
);
foreach ($allPossibleAllowedValuesTranslations as $translations) {
if (in_array($value, $translations, true)) {
// All good, $value is within the set of translated allowed values
return;
}
}
// Last chance:
// If $value is not configured within the form setup as an allowed value
// but was written within the form definition by hand (and therefore contains a hmac),
// check if $value is manipulated.
// If $value has no hmac or if the hmac exists but is not valid,
// then $this->validatePropertyCollectionElementPropertyValueByHmacData() will
// throw an exception.
$this->validateFormElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
}
@@ -0,0 +1,167 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
/**
* @internal
*/
class CreatablePropertyCollectionElementPropertiesValidator extends CollectionBasedValidator
{
/**
* Checks if the property collection element property is defined
* within the form editor setup or if the property is definied within
* the "predefinedDefaults" in the form editor setup
* and the property value matches the predefined value
* or if there is a valid hmac hash for the value.
* If the property collection element property is defined within the form editor setup
* and there is no valid hmac hash for the value
* and is the form property collection element property configured to only allow a limited set of values,
* check the current (submitted) value against the allowed set of values (defined within the form setup).
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value)
{
$dto = $this->validationDto->withPropertyPath($key);
if ($this->getConfigurationService()->isPropertyCollectionPropertyDefinedInFormEditorSetup($dto)) {
if ($this->getConfigurationService()->propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
$this->validatePropertyCollectionPropertyValue($value, $dto);
}
} elseif (
$this->getConfigurationService()->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)
&& !ArrayUtility::isValidPath($this->currentElement, $this->buildHmacDataPath($dto->getPropertyPath()), '.')
) {
$this->validatePropertyCollectionElementPredefinedDefaultValue($value, $dto);
} else {
$this->validatePropertyCollectionElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
/**
* Throws an exception if the value from a property collection property
* does not match the default value from the form editor setup.
*
* @param mixed $value
* @param ValidationDto $dto
* @throws PropertyException
*/
protected function validatePropertyCollectionElementPredefinedDefaultValue(
$value,
ValidationDto $dto
): void {
// If the property collection element is newely created, we have to compare the $value (form definition) with $predefinedDefaultValue (form setup)
// to check the integrity (at this time we don't have a hmac on the value to check the integrity)
$predefinedDefaultValue = $this->getConfigurationService()->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($dto);
if ($value !== $predefinedDefaultValue) {
$throwException = true;
if (is_string($predefinedDefaultValue)) {
// Last chance:
// Get all translations (from all backend languages) for the untranslated! $predefinedDefaultValue and
// compare the (already translated) $value (from the form definition) against the possible
// translations from $predefinedDefaultValue.
$untranslatedPredefinedDefaultValue = $this->getConfigurationService()->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($dto, false);
$translations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKey(
$untranslatedPredefinedDefaultValue,
$dto->getPrototypeName()
);
if (in_array($value, $translations, true)) {
$throwException = false;
}
}
if ($throwException) {
$message = 'The value "%s" of property "%s" (form element "%s" / "%s.%s") is not equal to the default value "%s" #1528591502';
throw new PropertyException(
sprintf(
$message,
$value,
$dto->getPropertyPath(),
$dto->getFormElementIdentifier(),
$dto->getPropertyCollectionName(),
$dto->getPropertyCollectionElementIdentifier(),
$predefinedDefaultValue
),
1528591502
);
}
}
}
/**
* Throws an exception if the value from a property collection property
* does not match the allowed set of values (defined within the form setup).
*
* @param mixed $value
* @param ValidationDto $dto
* @throws PropertyException
*/
protected function validatePropertyCollectionPropertyValue(
$value,
ValidationDto $dto
): void {
$allowedValues = $this->getConfigurationService()->getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup($dto);
if (!in_array($value, $allowedValues, true)) {
$untranslatedAllowedValues = $this->getConfigurationService()->getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup($dto, false);
// Compare the $value against the untranslated set of allowed values
if (in_array($value, $untranslatedAllowedValues, true)) {
// All good, $value is within the untranslated set of allowed values
return;
}
// Get all translations (from all backend languages) for the untranslated! $allowedValues and
// compare the (already translated) $value (from the form definition) against all possible
// translations for $untranslatedAllowedValues.
$allPossibleAllowedValuesTranslations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKeys(
$untranslatedAllowedValues,
$dto->getPrototypeName()
);
foreach ($allPossibleAllowedValuesTranslations as $translations) {
if (in_array($value, $translations, true)) {
// All good, $value is within the set of translated allowed values
return;
}
}
// Last chance:
// If $value is not configured within the form setup as an allowed value
// but was written within the form definition by hand (and therefore contains a hmac),
// check if $value is manipulated.
// If $value has no hmac or if the hmac exists but is not valid,
// then $this->validatePropertyCollectionElementPropertyValueByHmacData() will
// throw an exception.
$this->validatePropertyCollectionElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
/**
* @internal
*/
abstract class ElementBasedValidator extends AbstractValidator
{
/**
* Throws an exception if value from a form element property
* does not match its hmac hash or if there is no hmac hash
* available for the value.
*
* @param array $currentElement
* @param mixed $value
* @param string $sessionToken
* @param ValidationDto $dto
* @throws PropertyException
*/
public function validateFormElementPropertyValueByHmacData(
array $currentElement,
$value,
string $sessionToken,
ValidationDto $dto
): void {
$hmacDataPath = $this->buildHmacDataPath($dto->getPropertyPath());
if (ArrayUtility::isValidPath($currentElement, $hmacDataPath, '.')) {
$hmacData = ArrayUtility::getValueByPath($currentElement, $hmacDataPath, '.');
$hmacContent = [$dto->getFormElementIdentifier(), $dto->getPropertyPath()];
if (!$this->getFormDefinitionValidationService()->isPropertyValueEqualToHistoricalValue($hmacContent, $value, $hmacData, $sessionToken)) {
$message = 'The value "%s" of property "%s" (form element "%s") is not equal to the historical value "%s" #1528588036';
throw new PropertyException(
sprintf(
$message,
$value,
$dto->getPropertyPath(),
$dto->getFormElementIdentifier(),
$hmacData['value'] ?? ''
),
1528588036
);
}
} else {
$message = 'No hmac found for property "%s" (form element "%s") #1528588037';
throw new PropertyException(
sprintf($message, $dto->getPropertyPath(), $dto->getFormElementIdentifier()),
1528588037
);
}
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class FormElementHmacDataValidator extends ElementBasedValidator
{
/**
* Checks if the form element property value matches to its hmac hash.
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$dto = $this->validationDto->withPropertyPath($key);
$this->validateFormElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class PropertyCollectionElementHmacDataValidator extends CollectionBasedValidator
{
/**
* Checks if the property collection element values matches to its hmac hash.
*
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value): void
{
$dto = $this->validationDto->withPropertyPath($key)->withPropertyCollectionElementIdentifier(
$this->currentElement['identifier']
);
$this->validatePropertyCollectionElementPropertyValueByHmacData(
$this->currentElement,
$value,
$this->sessionToken,
$dto
);
}
}
@@ -0,0 +1,228 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
*/
class ValidationDto
{
/**
* @var string
*/
protected $prototypeName;
/**
* @var string
*/
protected $formElementType;
/**
* @var string
*/
protected $formElementIdentifier;
/**
* @var string
*/
protected $propertyPath;
/**
* @var string
*/
protected $propertyCollectionName;
/**
* @var string
*/
protected $propertyCollectionElementIdentifier;
/**
* @param string $prototypeName
* @param string $formElementType
* @param string $formElementIdentifier
* @param string $propertyPath
* @param string $propertyCollectionName
* @param string $propertyCollectionElementIdentifier
*/
public function __construct(
string $prototypeName = null,
string $formElementType = null,
string $formElementIdentifier = null,
string $propertyPath = null,
string $propertyCollectionName = null,
string $propertyCollectionElementIdentifier = null
) {
$this->prototypeName = $prototypeName;
$this->formElementType = $formElementType;
$this->formElementIdentifier = $formElementIdentifier;
$this->propertyPath = $propertyPath;
$this->propertyCollectionName = $propertyCollectionName;
$this->propertyCollectionElementIdentifier = $propertyCollectionElementIdentifier;
}
/**
* @return string
*/
public function getPrototypeName(): string
{
return $this->prototypeName;
}
/**
* @return string
*/
public function getFormElementType(): string
{
return $this->formElementType;
}
/**
* @return string
*/
public function getFormElementIdentifier(): string
{
return $this->formElementIdentifier;
}
/**
* @return string
*/
public function getPropertyPath(): string
{
return $this->propertyPath;
}
/**
* @return string
*/
public function getPropertyCollectionName(): string
{
return $this->propertyCollectionName;
}
/**
* @return string
*/
public function getPropertyCollectionElementIdentifier(): string
{
return $this->propertyCollectionElementIdentifier;
}
/**
* @return bool
*/
public function hasPrototypeName(): bool
{
return !empty($this->prototypeName);
}
/**
* @return bool
*/
public function hasFormElementType(): bool
{
return !empty($this->formElementType);
}
/**
* @return bool
*/
public function hasFormElementIdentifier(): bool
{
return !empty($this->formElementIdentifier);
}
/**
* @return bool
*/
public function hasPropertyPath(): bool
{
return !empty($this->propertyPath);
}
/**
* @return bool
*/
public function hasPropertyCollectionName(): bool
{
return !empty($this->propertyCollectionName);
}
/**
* @return bool
*/
public function hasPropertyCollectionElementIdentifier(): bool
{
return !empty($this->propertyCollectionElementIdentifier);
}
/**
* @param string $prototypeName
* @return ValidationDto
*/
public function withPrototypeName(string $prototypeName): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
}
/**
* @param string $formElementType
* @return ValidationDto
*/
public function withFormElementType(string $formElementType): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
}
/**
* @param string $formElementIdentifier
* @return ValidationDto
*/
public function withFormElementIdentifier(string $formElementIdentifier): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
}
/**
* @param string $propertyPath
* @return ValidationDto
*/
public function withPropertyPath(string $propertyPath): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
}
/**
* @param string $propertyCollectionName
* @return ValidationDto
*/
public function withPropertyCollectionName(string $propertyCollectionName): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $propertyCollectionName, $this->propertyCollectionElementIdentifier);
}
/**
* @param string $propertyCollectionElementIdentifier
* @return ValidationDto
*/
public function withPropertyCollectionElementIdentifier(string $propertyCollectionElementIdentifier): ValidationDto
{
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $propertyCollectionElementIdentifier);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
interface ValidatorInterface
{
/**
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
public function __construct(array $currentElement, string $sessionToken, ValidationDto $validationDto);
/**
* @param string $key
* @param mixed $value
*/
public function __invoke(string $key, $value);
}
@@ -0,0 +1,145 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\AddHmacDataConverter;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\ConverterDto;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\FinisherTranslationLanguageConverter;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\RemoveHmacDataConverter;
/**
* @internal
*/
class FormDefinitionConversionService implements SingletonInterface
{
/**
* Add a new value "_orig_<propertyName>" for each scalar property value
* within the form definition as a sibling of the property key.
* "_orig_<propertyName>" is an array which contains the property value
* and a hmac hash for the property value.
* "_orig_<propertyName>" will be used to validate the form definition on saving.
* @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService::validateFormDefinitionProperties()
*
* @param array $formDefinition
* @return array
*/
public function addHmacData(array $formDefinition): array
{
// Extend the hmac hashing key with a "per form editor session" unique key.
$sessionToken = $this->generateSessionToken();
$this->persistSessionToken($sessionToken);
$converterDto = GeneralUtility::makeInstance(ConverterDto::class, $formDefinition);
GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'addHmacData',
'(^identifier$|renderables\.([\d]+)\.identifier$)',
GeneralUtility::makeInstance(
AddHmacDataConverter::class,
$converterDto,
$sessionToken
)
)
);
return $converterDto->getFormDefinition();
}
/**
* Remove the "_orig_<propertyName>" values from the form definition.
*
* @param array $formDefinition
* @return array
*/
public function removeHmacData(array $formDefinition): array
{
$converterDto = GeneralUtility::makeInstance(ConverterDto::class, $formDefinition);
GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'removeHmacData',
'(_orig_.*|.*\._orig_.*)\.hmac',
GeneralUtility::makeInstance(
RemoveHmacDataConverter::class,
$converterDto
)
)
);
return $converterDto->getFormDefinition();
}
/**
* Migrate various finisher options
*
* @param array $formDefinition
* @return array
*/
public function migrateFinisherConfiguration(array $formDefinition): array
{
$converterDto = GeneralUtility::makeInstance(ConverterDto::class, $formDefinition);
GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'migrateFinisherLanguageSettings',
'^finishers\.([\d]+)\.options.translation.language$',
GeneralUtility::makeInstance(
FinisherTranslationLanguageConverter::class,
$converterDto
)
)
);
return $converterDto->getFormDefinition();
}
/**
* @param string $sessionToken
*/
protected function persistSessionToken(string $sessionToken)
{
$this->getBackendUser()->setAndSaveSessionData('extFormProtectionSessionToken', $sessionToken);
}
/**
* Generates the random token which is used in the hash for the form tokens.
*
* @return string
*/
protected function generateSessionToken()
{
return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(64);
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,402 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatableFormElementPropertiesValidator;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatablePropertyCollectionElementPropertiesValidator;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\FormElementHmacDataValidator;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\PropertyCollectionElementHmacDataValidator;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
/**
* @internal
*/
class FormDefinitionValidationService implements SingletonInterface
{
/**
* @var ConfigurationService
*/
protected $configurationService;
/**
* Validate the form definition properties using the form setup.
* Pseudo workflow:
* Is the form element type creatable by the form editor?
* YES
* foreach(form element properties) (without finishers|validators)
* is the form element property defined in the form setup (can be manipulated)?
* YES
* is the form element property configured to only allow a limited set of values (e.g. Inspector-MultiSelectEditor, Inspector-SingleSelectEditor ...)
* YES
* is the form element property value within the set of allowed values?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* valid!
* NO
* is the form element property defined in "predefinedDefaults" in the form setup (cannot be manipulated but should be written)?
* YES
* is the form element property value equals to the value defined in "predefinedDefaults" in the form setup?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* is there a hmac hash available for the form element property value (cannot be manipulated but should be written)?
* YES
* is the form element property value equals the historical value (and is the historical value valid)?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* invalid! throw exception
* foreach(form elements finishers|validators)
* is the form elements finisher|validator creatable by the form editor?
* YES
* foreach(form elements finisher|validator properties)
* is the form elements finisher|validator property defined in the form setup (can be manipulated)?
* YES
* is the form elements finisher|validator property configured to only allow a limited set of values (e.g. Inspector-MultiSelectEditor, Inspector-SingleSelectEditor ...)
* YES
* is the form elements finisher|validator property value within the set of allowed values?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* valid!
* NO
* is the form elements finisher|validator property defined in "predefinedDefaults" in the form setup (cannot be manipulated but should be written)?
* YES
* is the form elements finisher|validator property value equals to the value defined in "predefinedDefaults" in the form setup?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
* YES
* is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* invalid! throw exception
* NO
* foreach(form elements finisher|validator properties)
* is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
* YES
* is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* invalid! throw exception
* NO
* foreach(form element properties) (without finishers|validators)
* is there a hmac hash available for the form element property value (cannot be manipulated but should be written)?
* YES
* is the form element property value equals the historical value (and is the historical value valid)?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* invalid! throw exception
* foreach(form elements finisher|validator properties)
* is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
* YES
* is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
* YES
* valid!
* NO
* invalid! throw exception
* NO
* invalid! throw exception
*
* @param array $currentFormElement
* @param string $prototypeName
* @param string $sessionToken
* @throws PropertyException
*/
public function validateFormDefinitionProperties(
array $currentFormElement,
string $prototypeName,
string $sessionToken
): void {
$renderables = $currentFormElement['renderables'] ?? [];
$propertyCollectionElements = $currentFormElement['finishers'] ?? $currentFormElement['validators'] ?? [];
$propertyCollectionName = $currentFormElement['type'] === 'Form' ? 'finishers' : 'validators';
unset($currentFormElement['renderables'], $currentFormElement['finishers'], $currentFormElement['validators']);
$validationDto = GeneralUtility::makeInstance(
ValidationDto::class,
$prototypeName,
$currentFormElement['type'],
$currentFormElement['identifier'],
null,
$propertyCollectionName
);
if ($this->getConfigurationService()->isFormElementTypeCreatableByFormEditor($validationDto)) {
$this->validateAllPropertyValuesFromCreatableFormElement(
$currentFormElement,
$sessionToken,
$validationDto
);
foreach ($propertyCollectionElements as $propertyCollectionElement) {
$validationDto = $validationDto->withPropertyCollectionElementIdentifier(
$propertyCollectionElement['identifier']
);
if ($this->getConfigurationService()->isPropertyCollectionElementIdentifierCreatableByFormEditor($validationDto)) {
$this->validateAllPropertyValuesFromCreatablePropertyCollectionElement(
$propertyCollectionElement,
$sessionToken,
$validationDto
);
} else {
$this->validateAllPropertyCollectionElementValuesByHmac(
$propertyCollectionElement,
$sessionToken,
$validationDto
);
}
}
} else {
$this->validateAllFormElementPropertyValuesByHmac($currentFormElement, $sessionToken, $validationDto);
foreach ($propertyCollectionElements as $propertyCollectionElement) {
$this->validateAllPropertyCollectionElementValuesByHmac(
$propertyCollectionElement,
$sessionToken,
$validationDto
);
}
}
foreach ($renderables as $renderable) {
$this->validateFormDefinitionProperties($renderable, $prototypeName, $sessionToken);
}
}
/**
* Returns TRUE if a property value is equals to the historical value
* and FALSE if not.
* "Historical values" means values which are available within the form definition
* while the form editor is loaded and the values which are available after a
* successful validation of the form definition on a save operation.
* The value must be equal to the historical value if the property key for the value
* is not defined within the form setup.
* This means that the property can not be changed by the form editor but we want to keep the value
* in its original state.
* If this is not the case (return value is FALSE), an exception must be thrown.
*
* @param array $hmacContent
* @param mixed $propertyValue
* @param array $hmacData
* @param string $sessionToken
* @return bool
* @throws PropertyException
*/
public function isPropertyValueEqualToHistoricalValue(
array $hmacContent,
$propertyValue,
array $hmacData,
string $sessionToken
): bool {
$this->checkHmacDataIntegrity($hmacData, $hmacContent, $sessionToken);
$hmacContent[] = $propertyValue;
$expectedHash = GeneralUtility::hmac(serialize($hmacContent), $sessionToken);
return hash_equals($expectedHash, $hmacData['hmac']);
}
/**
* Compares the historical value and the hmac hash to ensure the integrity
* of the data.
* An exception will be thrown if the value is modified.
*
* @param array $hmacData
* @param array $hmacContent
* @param string $sessionToken
* @throws PropertyException
*/
protected function checkHmacDataIntegrity(array $hmacData, array $hmacContent, string $sessionToken)
{
$hmac = $hmacData['hmac'] ?? null;
if (empty($hmac)) {
throw new PropertyException('Hmac must not be empty. #1528538222', 1528538222);
}
$hmacContent[] = $hmacData['value'] ?? '';
$expectedHash = GeneralUtility::hmac(serialize($hmacContent), $sessionToken);
if (!hash_equals($expectedHash, $hmac)) {
throw new PropertyException('Unauthorized modification of historical data. #1528538252', 1528538252);
}
}
/**
* Walk through all form element properties and checks
* if the values matches to their hmac hashes.
*
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
protected function validateAllFormElementPropertyValuesByHmac(
array $currentElement,
$sessionToken,
ValidationDto $validationDto
): void {
GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'validateProperties',
'^(?!(_orig_.*|.*\._orig_.*)$).*',
GeneralUtility::makeInstance(
FormElementHmacDataValidator::class,
$currentElement,
$sessionToken,
$validationDto
)
)
);
}
/**
* Walk through all property collection properties and checks
* if the values matches to their hmac hashes.
*
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
protected function validateAllPropertyCollectionElementValuesByHmac(
array $currentElement,
$sessionToken,
ValidationDto $validationDto
): void {
GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'validateProperties',
'^(?!(_orig_.*|.*\._orig_.*)$).*',
GeneralUtility::makeInstance(
PropertyCollectionElementHmacDataValidator::class,
$currentElement,
$sessionToken,
$validationDto
)
)
);
}
/**
* Walk through all form element properties and checks
* if the property is defined within the form editor setup
* or if the property is definied within the "predefinedDefaults" in the form editor setup
* and the property value matches the predefined value
* or if there is a valid hmac hash for the value.
*
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
protected function validateAllPropertyValuesFromCreatableFormElement(
array $currentElement,
$sessionToken,
ValidationDto $validationDto
): void {
GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'validateProperties',
'^(?!(_orig_.*|.*\._orig_.*|type|identifier)$).*',
GeneralUtility::makeInstance(
CreatableFormElementPropertiesValidator::class,
$currentElement,
$sessionToken,
$validationDto
)
)
);
}
/**
* Walk through all property collection properties and checks
* if the property is defined within the form editor setup
* or if the property is definied within the "predefinedDefaults" in the form editor setup
* and the property value matches the predefined value
* or if there is a valid hmac hash for the value.
*
* @param array $currentElement
* @param string $sessionToken
* @param ValidationDto $validationDto
*/
protected function validateAllPropertyValuesFromCreatablePropertyCollectionElement(
array $currentElement,
$sessionToken,
ValidationDto $validationDto
): void {
GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
GeneralUtility::makeInstance(
ArrayProcessing::class,
'validateProperties',
'^(?!(_orig_.*|.*\._orig_.*|identifier)$).*',
GeneralUtility::makeInstance(
CreatablePropertyCollectionElementPropertiesValidator::class,
$currentElement,
$sessionToken,
$validationDto
)
)
);
}
/**
* @return ConfigurationService
*/
protected function getConfigurationService(): ConfigurationService
{
if (!($this->configurationService instanceof ConfigurationService)) {
$this->configurationService = $this->getObjectManager()->get(ConfigurationService::class);
}
return $this->configurationService;
}
/**
* @return ObjectManager
*/
protected function getObjectManager(): ObjectManager
{
return GeneralUtility::makeInstance(ObjectManager::class);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
abstract class AbstractExtractor implements ExtractorInterface
{
/**
* @var ExtractorDto
*/
protected $extractorDto;
/**
* @param ExtractorDto $extractorDto
*/
public function __construct(ExtractorDto $extractorDto)
{
$this->extractorDto = $extractorDto;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class AdditionalElementPropertyPathsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType] = $matches;
$result = $this->extractorDto->getResult();
$result['formElements'][$formElementType]['additionalElementPropertyPaths'][] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
class ExtractorDto
{
/**
* @var array
*/
protected $prototypeConfiguration;
/**
* @var array
*/
protected $result = [];
/**
* @param array $prototypeConfiguration
*/
public function __construct(array $prototypeConfiguration)
{
$this->prototypeConfiguration = $prototypeConfiguration;
}
/**
* @return array
*/
public function getPrototypeConfiguration(): array
{
return $this->prototypeConfiguration;
}
/**
* @return array
*/
public function getResult(): array
{
return $this->result;
}
/**
* @param array $result
* @return ExtractorDto
*/
public function setResult(array $result): ExtractorDto
{
$this->result = $result;
return $this;
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal
*/
interface ExtractorInterface
{
/**
* @param ExtractorDto $extractorDto
*/
public function __construct(ExtractorDto $extractorDto);
/**
* @param string $key
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $key, $value, array $matches);
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class IsCreatableFormElementExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType] = $matches;
$formElementGroup = $value;
$result = $this->extractorDto->getResult();
if (!ArrayUtility::isValidPath(
$this->extractorDto->getPrototypeConfiguration(),
'formElementsDefinition.' . $formElementType . '.formEditor.groupSorting',
'.'
)) {
$result['formElements'][$formElementType]['creatable'] = false;
$this->extractorDto->setResult($result);
return;
}
$formElementGroups = array_keys(
ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), 'formEditor.formElementGroups', '.')
);
$result['formElements'][$formElementType]['creatable'] = in_array(
$formElementGroup,
$formElementGroups,
true
);
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class MultiValuePropertiesExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $formEditorIndex] = $matches;
if (
$value !== 'Inspector-PropertyGridEditor'
&& $value !== 'Inspector-MultiSelectEditor'
&& $value !== 'Inspector-ValidationErrorMessageEditor'
&& $value !== 'Inspector-RequiredValidatorEditor'
) {
return;
}
if ($value === 'Inspector-RequiredValidatorEditor') {
$propertyPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'configurationOptions',
'validationErrorMessage',
'propertyPath',
]
);
} else {
$propertyPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'propertyPath',
]
);
}
$result = $this->extractorDto->getResult();
if (ArrayUtility::isValidPath($this->extractorDto->getPrototypeConfiguration(), $propertyPath, '.')) {
$result['formElements'][$formElementType]['multiValueProperties'][] = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
$propertyPath,
'.'
);
}
if ($value === 'Inspector-PropertyGridEditor') {
$result['formElements'][$formElementType]['multiValueProperties'][] = 'defaultValue';
}
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class PredefinedDefaultsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $propertyPath] = $matches;
$result = $this->extractorDto->getResult();
$result['formElements'][$formElementType]['predefinedDefaults'][$propertyPath] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class PropertyPathsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
$formElementPropertyPaths = $this->getPropertyPaths($value, $matches);
$result = $this->extractorDto->getResult();
$result = array_merge_recursive($result, ['formElements' => $formElementPropertyPaths]);
$this->extractorDto->setResult($result);
}
/**
* @param string $value
* @param array $matches
* @return array
*/
protected function getPropertyPaths(string $value, array $matches): array
{
[, $formElementType, $formEditorIndex] = $matches;
$paths[$formElementType]['propertyPaths'] = [];
$templateNamePath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'templateName',
]
);
$templateName = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
$templateNamePath,
'.'
);
// Special processing of "Inspector-GridColumnViewPortConfigurationEditor" inspector editors.
// Expand the property path which contains a "{@viewPortIdentifier}" placeholder
// to X property paths which contain all available placeholder replacements.
if ($templateName === 'Inspector-GridColumnViewPortConfigurationEditor') {
$viewPortsPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'configurationOptions',
'viewPorts',
]
);
$viewPorts = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $viewPortsPath, '.');
foreach ($viewPorts as $viewPort) {
$viewPortIdentifier = $viewPort['viewPortIdentifier'];
$propertyPath = str_replace('{@viewPortIdentifier}', $viewPortIdentifier, $value);
$paths[$formElementType]['propertyPaths'][] = $propertyPath;
}
} else {
$paths[$formElementType]['propertyPaths'][] = $value;
}
return $paths;
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class SelectOptionsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $formEditorIndex] = $matches;
$templateName = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'templateName',
]
),
'.'
);
if ($templateName === 'Inspector-FinishersEditor') {
$propertyPath = '_finishers';
} elseif ($templateName === 'Inspector-ValidatorsEditor') {
$propertyPath = '_validators';
} else {
if ($templateName === 'Inspector-RequiredValidatorEditor') {
$propertyPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'configurationOptions',
'validationErrorMessage',
'propertyPath',
]
);
} else {
$propertyPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'propertyPath',
]
);
}
$propertyPath = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
$propertyPath,
'.'
);
}
$result = $this->extractorDto->getResult();
$result['formElements'][$formElementType]['selectOptions'][$propertyPath][] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class IsCreatablePropertyCollectionElementExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $formEditorIndex] = $matches;
if (
$value !== 'Inspector-FinishersEditor'
&& $value !== 'Inspector-ValidatorsEditor'
&& $value !== 'Inspector-RequiredValidatorEditor'
) {
return;
}
$propertyCollectionName = $value === 'Inspector-FinishersEditor' ? 'finishers' : 'validators';
$result = $this->extractorDto->getResult();
if (
$value === 'Inspector-FinishersEditor'
|| $value === 'Inspector-ValidatorsEditor'
) {
$selectOptionsPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'selectOptions',
]
);
if (!ArrayUtility::isValidPath($this->extractorDto->getPrototypeConfiguration(), $selectOptionsPath, '.')) {
return;
}
$selectOptions = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
$selectOptionsPath,
'.'
);
foreach ($selectOptions as $selectOption) {
$validatorIdentifier = $selectOption['value'] ?? '';
if (empty($validatorIdentifier)) {
continue;
}
$result['formElements'][$formElementType]['collections'][$propertyCollectionName][$validatorIdentifier]['creatable'] = true;
}
} else {
$validatorIdentifierPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'editors',
$formEditorIndex,
'validatorIdentifier',
]
);
if (!ArrayUtility::isValidPath($this->extractorDto->getPrototypeConfiguration(), $validatorIdentifierPath, '.')) {
return;
}
$validatorIdentifier = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
$validatorIdentifierPath,
'.'
);
$result['formElements'][$formElementType]['collections'][$propertyCollectionName][$validatorIdentifier]['creatable'] = true;
}
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class MultiValuePropertiesExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $propertyCollectionName, $propertyCollectionIndex, $propertyCollectionEditorIndex] = $matches;
if (
$value !== 'Inspector-PropertyGridEditor'
&& $value !== 'Inspector-MultiSelectEditor'
&& $value !== 'Inspector-ValidationErrorMessageEditor'
) {
return;
}
$propertyPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'propertyCollections',
$propertyCollectionName,
$propertyCollectionIndex,
'editors',
$propertyCollectionEditorIndex,
'propertyPath',
]
);
$propertyValue = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $propertyPath, '.');
$result = $this->extractorDto->getResult();
if (
$value === 'Inspector-PropertyGridEditor'
|| $value === 'Inspector-MultiSelectEditor'
) {
$identifierPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'propertyCollections',
$propertyCollectionName,
$propertyCollectionIndex,
'identifier',
]
);
$identifier = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $identifierPath, '.');
$result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['multiValueProperties'][] = $propertyValue;
if ($value === 'Inspector-PropertyGridEditor') {
$result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['multiValueProperties'][] = 'defaultValue';
}
} else {
$result['formElements'][$formElementType]['multiValueProperties'][] = $propertyValue;
}
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class PredefinedDefaultsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $propertyCollectionName, $propertyCollectionElementIdentifier, $propertyPath] = $matches;
$propertyCollectionName = str_replace('Definition', '', $propertyCollectionName);
$result = $this->extractorDto->getResult();
$result['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['predefinedDefaults'][$propertyPath] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class PropertyPathsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $propertyCollectionName, $propertyCollectionIndex] = $matches;
$identifierPath = implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'propertyCollections',
$propertyCollectionName,
$propertyCollectionIndex,
'identifier',
]
);
$identifier = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $identifierPath, '.');
$result = $this->extractorDto->getResult();
$result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['propertyPaths'][] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
/**
* @internal
*/
class SelectOptionsExtractor extends AbstractExtractor
{
/**
* @param string $_
* @param mixed $value
* @param array $matches
*/
public function __invoke(string $_, $value, array $matches)
{
[, $formElementType, $propertyCollectionName, $propertyCollectionIndex, $propertyCollectionEditorIndex] = $matches;
$propertyPath = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'propertyCollections',
$propertyCollectionName,
$propertyCollectionIndex,
'editors',
$propertyCollectionEditorIndex,
'propertyPath',
]
),
'.'
);
$propertyCollectionElementIdentifier = ArrayUtility::getValueByPath(
$this->extractorDto->getPrototypeConfiguration(),
implode(
'.',
[
'formElementsDefinition',
$formElementType,
'formEditor',
'propertyCollections',
$propertyCollectionName,
$propertyCollectionIndex,
'identifier'
]
),
'.'
);
$propertyCollectionName = str_replace('Definition', '', $propertyCollectionName);
$result = $this->extractorDto->getResult();
$result['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['selectOptions'][$propertyPath][] = $value;
$this->extractorDto->setResult($result);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Exception as FormException;
/**
* A generic Form domain Exception
*/
class Exception extends FormException
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if the "identifier" for a Form, a Page or a Form Element
* is invalid (i.e. empty or not a string)
*/
class IdentifierNotValidException extends Exception
{
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if a rendering error occurs
*/
class RenderingException extends Exception
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if a Type Definition for a form element was not found,
* or if the implementationClassName was not set.
*/
class TypeDefinitionNotFoundException extends Exception
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if a Type Definition for a form element was not valid,
* i.e. it has properties which are not supported.
*/
class TypeDefinitionNotValidException extends Exception
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown if the ArrayFormFactory want to create child
* elements within a unknown composite renderable.
*/
class UnknownCompositRenderableException extends Exception
{
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Factory;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
/**
* Base class for custom *Form Factories*. A Form Factory is responsible for building
* a {@link TYPO3\CMS\Form\Domain\Model\FormDefinition}.
*
* {@inheritDoc}
*
* Example
* =======
*
* Generally, you should use this class as follows:
*
* <pre>
* class MyFooBarFactory extends AbstractFormFactory {
* public function build(array $configuration, $prototypeName) {
* $configurationService = GeneralUtility::makeInstance(ObjectManager::class)->get(ConfigurationService::class);
* $prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
* $formDefinition = GeneralUtility::makeInstance(ObjectManager::class)->get(FormDefinition::class, 'nameOfMyForm', $prototypeConfiguration);
*
* // now, you should call methods on $formDefinition to add pages and form elements
*
* return $formDefinition;
* }
* }
* </pre>
*
* Scope: frontend / backend
* **This class is meant to be sub classed by developers.**
*/
abstract class AbstractFormFactory implements FormFactoryInterface
{
/**
* Helper to be called by every AbstractFormFactory after everything has been built to call the "afterBuildingFinished"
* hook on all form elements.
*
* @param FormDefinition $form
*/
protected function triggerFormBuildingFinished(FormDefinition $form)
{
foreach ($form->getRenderablesRecursively() as $renderable) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'afterBuildingFinished')) {
$hookObj->afterBuildingFinished(
$renderable
);
}
}
}
}
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Factory;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
use TYPO3\CMS\Form\Domain\Exception\UnknownCompositRenderableException;
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
use TYPO3\CMS\Form\Domain\Model\FormElements\AbstractSection;
use TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface;
/**
* A factory that creates a FormDefinition from an array
*
* Scope: frontend / backend
*/
class ArrayFormFactory extends AbstractFormFactory
{
/**
* Build a form definition, depending on some configuration.
*
* @param array $configuration
* @param string $prototypeName
* @return FormDefinition
* @throws RenderingException
* @internal
*/
public function build(array $configuration, string $prototypeName = null): FormDefinition
{
if (empty($prototypeName)) {
$prototypeName = $configuration['prototypeName'] ?? 'standard';
}
$persistenceIdentifier = $configuration['persistenceIdentifier'] ?? null;
if ($configuration['invalid'] === true) {
throw new RenderingException($configuration['label'], 1529710560);
}
$prototypeConfiguration = GeneralUtility::makeInstance(ObjectManager::class)
->get(ConfigurationService::class)
->getPrototypeConfiguration($prototypeName);
$form = GeneralUtility::makeInstance(ObjectManager::class)->get(
FormDefinition::class,
$configuration['identifier'],
$prototypeConfiguration,
'Form',
$persistenceIdentifier
);
if (isset($configuration['renderables'])) {
foreach ($configuration['renderables'] as $pageConfiguration) {
$this->addNestedRenderable($pageConfiguration, $form);
}
}
unset($configuration['persistenceIdentifier']);
unset($configuration['prototypeName']);
unset($configuration['renderables']);
unset($configuration['type']);
unset($configuration['identifier']);
$form->setOptions($configuration);
$this->triggerFormBuildingFinished($form);
return $form;
}
/**
* Add form elements to the $parentRenderable
*
* @param array $nestedRenderableConfiguration
* @param CompositeRenderableInterface $parentRenderable
* @return mixed
* @throws IdentifierNotValidException
* @throws UnknownCompositRenderableException
*/
protected function addNestedRenderable(array $nestedRenderableConfiguration, CompositeRenderableInterface $parentRenderable)
{
if (!isset($nestedRenderableConfiguration['identifier'])) {
throw new IdentifierNotValidException('Identifier not set.', 1329289436);
}
if ($parentRenderable instanceof FormDefinition) {
$renderable = $parentRenderable->createPage($nestedRenderableConfiguration['identifier'], $nestedRenderableConfiguration['type']);
} elseif ($parentRenderable instanceof AbstractSection) {
$renderable = $parentRenderable->createElement($nestedRenderableConfiguration['identifier'], $nestedRenderableConfiguration['type']);
} else {
throw new UnknownCompositRenderableException('Unknown composit renderable "' . get_class($parentRenderable) . '"', 1479593622);
}
if (isset($nestedRenderableConfiguration['renderables']) && is_array($nestedRenderableConfiguration['renderables'])) {
$childRenderables = $nestedRenderableConfiguration['renderables'];
} else {
$childRenderables = [];
}
unset($nestedRenderableConfiguration['type']);
unset($nestedRenderableConfiguration['identifier']);
unset($nestedRenderableConfiguration['renderables']);
$renderable->setOptions($nestedRenderableConfiguration);
if ($renderable instanceof CompositeRenderableInterface) {
foreach ($childRenderables as $elementConfiguration) {
$this->addNestedRenderable($elementConfiguration, $renderable);
}
}
return $renderable;
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Factory;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
/**
* A Form Factory is responsible for building a {@link TYPO3\CMS\Form\Domain\Model\FormDefinition}.
* **Instead of implementing this interface, subclassing {@link AbstractFormFactory} is more appropriate
* in most cases**.
*
* A Form Factory can be called anytime a FormDefinition should be built; in most cases
* it is done through an invocation of a Form Rendering ViewHelper.
*
* Scope: frontend / backend
*/
interface FormFactoryInterface
{
/**
* Build a form definition, depending on some configuration.
*
* The configuration array is factory-specific; for example a YAML or JSON factory
* could retrieve the path to the YAML / JSON file via the configuration array.
*
* @param array $configuration factory-specific configuration array
* @param string $prototypeName The name of the "PrototypeName" to use; it is factory-specific to implement this.
* @return FormDefinition a newly built form definition
*/
public function build(array $configuration, string $prototypeName = null): FormDefinition;
}
@@ -0,0 +1,377 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Finisher base class.
*
* Scope: frontend
* **This class is meant to be sub classed by developers**
*/
abstract class AbstractFinisher implements FinisherInterface
{
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* @var string
*/
protected $finisherIdentifier = '';
/**
* @var string
*/
protected $shortFinisherIdentifier = '';
/**
* The options which have been set from the outside. Instead of directly
* accessing them, you should rather use parseOption().
*
* @var array
*/
protected $options = [];
/**
* These are the default options of the finisher.
* Override them in your concrete implementation.
* Default options should not be changed from "outside"
*
* @var array
*/
protected $defaultOptions = [];
/**
* @var \TYPO3\CMS\Form\Domain\Finishers\FinisherContext
*/
protected $finisherContext;
/**
* @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
* @internal
*/
public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @param string $finisherIdentifier The identifier for this finisher
*/
public function __construct(string $finisherIdentifier = '')
{
if (empty($finisherIdentifier)) {
$this->finisherIdentifier = (new \ReflectionClass($this))->getShortName();
} else {
$this->finisherIdentifier = $finisherIdentifier;
}
$this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $this->finisherIdentifier);
}
/**
* @return string
*/
public function getFinisherIdentifier(): string
{
return $this->finisherIdentifier;
}
/**
* @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
*/
public function setOptions(array $options)
{
$this->options = $options;
}
/**
* Sets a single finisher option (@see setOptions())
*
* @param string $optionName name of the option to be set
* @param mixed $optionValue value of the option
*/
public function setOption(string $optionName, $optionValue)
{
$this->options[$optionName] = $optionValue;
}
/**
* Executes the finisher
*
* @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
* @return string|null
*/
final public function execute(FinisherContext $finisherContext)
{
$this->finisherContext = $finisherContext;
if (!$this->isEnabled()) {
return null;
}
return $this->executeInternal();
}
/**
* This method is called in the concrete finisher whenever self::execute() is called.
*
* Override and fill with your own implementation!
*
* @return string|null
*/
abstract protected function executeInternal();
/**
* Read the option called $optionName from $this->options, and parse {...}
* as object accessors.
*
* Then translate the value.
*
* If $optionName was not found, the corresponding default option is returned (from $this->defaultOptions)
*
* @param string $optionName
* @return string|array|null
*/
protected function parseOption(string $optionName)
{
if ($optionName === 'translation') {
return null;
}
try {
$optionValue = ArrayUtility::getValueByPath($this->options, $optionName, '.');
} catch (MissingArrayPathException $exception) {
$optionValue = null;
}
try {
$defaultValue = ArrayUtility::getValueByPath($this->defaultOptions, $optionName, '.');
} catch (MissingArrayPathException $exception) {
$defaultValue = null;
}
if ($optionValue === null && $defaultValue !== null) {
$optionValue = $defaultValue;
}
if ($optionValue === null) {
return null;
}
if (!is_string($optionValue) && !is_array($optionValue)) {
return $optionValue;
}
$formRuntime = $this->finisherContext->getFormRuntime();
$optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
if (is_string($optionValue)) {
$translationOptions = isset($this->options['translation']) && \is_array($this->options['translation'])
? $this->options['translation']
: [];
$optionValue = $this->translateFinisherOption(
$optionValue,
$formRuntime,
$optionName,
$optionValue,
$translationOptions
);
$optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
}
if (empty($optionValue)) {
if ($defaultValue !== null) {
$optionValue = $defaultValue;
}
}
return $optionValue;
}
/**
* Wraps TranslationService::translateFinisherOption to recursively
* invoke all array items of resolved form state values or nested
* finisher option configuration settings.
*
* @param string|array $subject
* @param FormRuntime $formRuntime
* @param string $optionName
* @param string|array $optionValue
* @param array $translationOptions
* @return array|string
*/
protected function translateFinisherOption(
$subject,
FormRuntime $formRuntime,
string $optionName,
$optionValue,
array $translationOptions
) {
if (is_array($subject)) {
foreach ($subject as $key => $value) {
$subject[$key] = $this->translateFinisherOption(
$value,
$formRuntime,
$optionName . '.' . $value,
$value,
$translationOptions
);
}
return $subject;
}
return TranslationService::getInstance()->translateFinisherOption(
$formRuntime,
$this->finisherIdentifier,
$optionName,
$optionValue,
$translationOptions
);
}
/**
* You can encapsulate a option value with {}.
* This enables you to access every getable property from the
* TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
*
* For example: {formState.formValues.<elemenIdentifier>}
* or {<elemenIdentifier>}
*
* Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elemenIdentifier>]"
* There is a special option value '{__currentTimestamp}'.
* This will be replaced with the current timestamp.
*
* @param string|array $needle
* @param FormRuntime $formRuntime
* @return mixed
*/
protected function substituteRuntimeReferences($needle, FormRuntime $formRuntime)
{
// neither array nor string, directly return
if (!is_array($needle) && !is_string($needle)) {
return $needle;
}
// resolve (recursively) all array items
if (is_array($needle)) {
return array_map(
function ($item) use ($formRuntime) {
return $this->substituteRuntimeReferences($item, $formRuntime);
},
$needle
);
}
// substitute one(!) variable in string which either could result
// again in a string or an array representing multiple values
if (preg_match('/^{([^}]+)}$/', $needle, $matches)) {
return $this->resolveRuntimeReference(
$matches[1],
$formRuntime
);
}
// in case string contains more than just one variable or just a static
// value that does not need to be substituted at all, candidates are:
// * "prefix{variable}suffix
// * "{variable-1},{variable-2}"
// * "some static value"
// * mixed cases of the above
return preg_replace_callback(
'/{([^}]+)}/',
function ($matches) use ($formRuntime) {
$value = $this->resolveRuntimeReference(
$matches[1],
$formRuntime
);
// substitute each match by returning the resolved value
if (!is_array($value)) {
return $value;
}
// now the resolve value is an array that shall substitute
// a variable in a string that probably is not the only one
// or is wrapped with other static string content (see above)
// ... which is just not possible
throw new FinisherException(
'Cannot convert array to string',
1519239265
);
},
$needle
);
}
/**
* Resolving property by name from submitted form data.
*
* @param string $property
* @param FormRuntime $formRuntime
* @return int|string|array
*/
protected function resolveRuntimeReference(string $property, FormRuntime $formRuntime)
{
if ($property === '__currentTimestamp') {
return time();
}
// try to resolve the path '{...}' within the FormRuntime
$value = ObjectAccess::getPropertyPath($formRuntime, $property);
if ($value === null) {
// try to resolve the path '{...}' within the FinisherVariableProvider
$value = ObjectAccess::getPropertyPath(
$this->finisherContext->getFinisherVariableProvider(),
$property
);
}
if ($value !== null) {
return $value;
}
// in case no value could be resolved
return '{' . $property . '}';
}
/**
* Returns whether this finisher is enabled
*
* @return bool
*/
public function isEnabled(): bool
{
return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
}
/**
* @return TypoScriptFrontendController
*/
protected function getTypoScriptFrontendController()
{
return $GLOBALS['TSFE'];
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
/**
* A simple finisher that invokes a closure when executed
*
* Usage:
* //...
* $closureFinisher = $this->objectManager->get(ClosureFinisher::class);
* $closureFinisher->setOption('closure', function($finisherContext) {
* $formRuntime = $finisherContext->getFormRuntime();
* // ...
* });
* $formDefinition->addFinisher($closureFinisher);
* // ...
*
* Scope: frontend
*/
class ClosureFinisher extends AbstractFinisher
{
/**
* @var array
*/
protected $defaultOptions = [
'closure' => null
];
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*
* @throws FinisherException
*/
protected function executeInternal()
{
/** @var \Closure $closure */
$closure = $this->parseOption('closure');
if ($closure === null) {
return;
}
if (!$closure instanceof \Closure) {
throw new FinisherException(sprintf('The option "closure" must be of type Closure, "%s" given.', gettype($closure)), 1332155239);
}
$closure($this->finisherContext);
}
}
@@ -0,0 +1,162 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* A finisher that outputs a given text
*
* Options:
*
* - message: A hard-coded message to be rendered
* - contentElementUid: A content element uid to be rendered
*
* Usage:
* //...
* $confirmationFinisher = $this->objectManager->get(ConfirmationFinisher::class);
* $confirmationFinisher->setOptions(
* [
* 'message' => 'foo',
* ]
* );
* $formDefinition->addFinisher($confirmationFinisher);
* // ...
*
* Scope: frontend
*/
class ConfirmationFinisher extends AbstractFinisher
{
/**
* @var array
*/
protected $defaultOptions = [
'message' => 'The form has been submitted.',
'contentElementUid' => 0,
'typoscriptObjectPath' => 'lib.tx_form.contentElementRendering'
];
/**
* @var array
*/
protected $typoScriptSetup = [];
/**
* @var ConfigurationManagerInterface
*/
protected $configurationManager;
/**
* @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
*/
public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
{
$this->configurationManager = $configurationManager;
$this->typoScriptSetup = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
}
/**
* @param ContentObjectRenderer $contentObjectRenderer
*/
public function injectContentObjectRenderer(ContentObjectRenderer $contentObjectRenderer)
{
$this->contentObjectRenderer = $contentObjectRenderer;
}
/**
* Executes this finisher
*
* @see AbstractFinisher::execute()
* @return string
*
* @throws FinisherException
*/
protected function executeInternal()
{
$contentElementUid = $this->parseOption('contentElementUid');
$typoscriptObjectPath = $this->parseOption('typoscriptObjectPath');
if (!empty($contentElementUid)) {
$pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath);
$lastSegment = array_pop($pathSegments);
$setup = $this->typoScriptSetup;
foreach ($pathSegments as $segment) {
if (!array_key_exists($segment . '.', $setup)) {
throw new FinisherException(
sprintf('TypoScript object path "%s" does not exist', $typoscriptObjectPath),
1489238980
);
}
$setup = $setup[$segment . '.'];
}
$this->contentObjectRenderer->start([$contentElementUid], '');
$this->contentObjectRenderer->setCurrentVal((string)$contentElementUid);
$message = $this->contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'], $lastSegment);
} else {
$message = $this->parseOption('message');
}
$standaloneView = $this->initializeStandaloneView(
$this->finisherContext->getFormRuntime()
);
$standaloneView->assignMultiple([
'message' => $message,
'isPreparedMessage' => !empty($contentElementUid),
]);
return $standaloneView->render();
}
/**
* @param FormRuntime $formRuntime
* @return StandaloneView
* @throws FinisherException
*/
protected function initializeStandaloneView(FormRuntime $formRuntime): StandaloneView
{
$standaloneView = $this->objectManager->get(StandaloneView::class);
if (!isset($this->options['templateName'])) {
throw new FinisherException(
'The option "templateName" must be set for the ConfirmationFinisher.',
1521573955
);
}
$standaloneView->setTemplate($this->options['templateName']);
$standaloneView->getTemplatePaths()->fillFromConfigurationArray($this->options);
if (isset($this->options['variables']) && is_array($this->options['variables'])) {
$standaloneView->assignMultiple($this->options['variables']);
}
$standaloneView->assign('form', $formRuntime);
$standaloneView->assign('finisherVariableProvider', $this->finisherContext->getFinisherVariableProvider());
$standaloneView->getRenderingContext()
->getViewHelperVariableContainer()
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
return $standaloneView;
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
/**
* This finisher remove the submited files.
* Use this e.g after the email finisher if you don't want
* to keep the files online.
*
* Scope: frontend
*/
class DeleteUploadsFinisher extends AbstractFinisher
{
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*/
protected function executeInternal()
{
$formRuntime = $this->finisherContext->getFormRuntime();
$uploadFolders = [];
$elements = $formRuntime->getFormDefinition()->getRenderablesRecursively();
foreach ($elements as $element) {
if (!$element instanceof FileUpload) {
continue;
}
$file = $formRuntime[$element->getIdentifier()];
if (!$file) {
continue;
}
if ($file instanceof FileReference) {
$file = $file->getOriginalResource();
}
$folder = $file->getParentFolder();
$uploadFolders[$folder->getCombinedIdentifier()] = $folder;
$file->getStorage()->deleteFile($file->getOriginalFile());
}
$this->deleteEmptyUploadFolders($uploadFolders);
}
/**
* note:
* TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter::importUploadedResource()
* creates a sub-folder for file uploads (e.g. .../form_<40-chars-hash>/actual.file)
* @param Folder[] $folders
*/
protected function deleteEmptyUploadFolders(array $folders): void
{
foreach ($folders as $folder) {
if ($this->isEmptyFolder($folder)) {
$folder->delete();
}
}
}
/**
* @param Folder $folder
* @return bool
*/
protected function isEmptyFolder(Folder $folder): bool
{
return $folder->getFileCount() === 0
&& $folder->getStorage()->countFoldersInFolder($folder) === 0;
}
}
@@ -0,0 +1,205 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
/**
* This finisher sends an email to one recipient
*
* Options:
*
* - templatePathAndFilename (mandatory): Template path and filename for the mail body
* - layoutRootPath: root path for the layouts
* - partialRootPath: root path for the partials
* - variables: associative array of variables which are available inside the Fluid template
*
* The following options control the mail sending. In all of them, placeholders in the form
* of {...} are replaced with the corresponding form value; i.e. {email} as recipientAddress
* makes the recipient address configurable.
*
* - subject (mandatory): Subject of the email
* - recipientAddress (mandatory): Email address of the recipient
* - recipientName: Human-readable name of the recipient
* - senderAddress (mandatory): Email address of the sender
* - senderName: Human-readable name of the sender
* - replyToAddress: Email address of to be used as reply-to email (use multiple addresses with an array)
* - carbonCopyAddress: Email address of the copy recipient (use multiple addresses with an array)
* - blindCarbonCopyAddress: Email address of the blind copy recipient (use multiple addresses with an array)
* - format: format of the email (one of the FORMAT_* constants). By default mails are sent as HTML
*
* Scope: frontend
*/
class EmailFinisher extends AbstractFinisher
{
const FORMAT_PLAINTEXT = 'plaintext';
const FORMAT_HTML = 'html';
/**
* @var array
*/
protected $defaultOptions = [
'recipientName' => '',
'senderName' => '',
'format' => self::FORMAT_HTML,
'attachUploads' => true
];
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*
* @throws FinisherException
*/
protected function executeInternal()
{
$formRuntime = $this->finisherContext->getFormRuntime();
$standaloneView = $this->initializeStandaloneView($formRuntime);
$translationService = TranslationService::getInstance();
if (isset($this->options['translation']['language']) && !empty($this->options['translation']['language'])) {
$languageBackup = $translationService->getLanguage();
$translationService->setLanguage($this->options['translation']['language']);
}
$message = $standaloneView->render();
if (!empty($languageBackup)) {
$translationService->setLanguage($languageBackup);
}
$subject = $this->parseOption('subject');
$recipientAddress = $this->parseOption('recipientAddress');
$recipientName = $this->parseOption('recipientName');
$senderAddress = $this->parseOption('senderAddress');
$senderName = $this->parseOption('senderName');
$replyToAddress = $this->parseOption('replyToAddress');
$carbonCopyAddress = $this->parseOption('carbonCopyAddress');
$blindCarbonCopyAddress = $this->parseOption('blindCarbonCopyAddress');
$format = $this->parseOption('format');
$attachUploads = $this->parseOption('attachUploads');
if (empty($subject)) {
throw new FinisherException('The option "subject" must be set for the EmailFinisher.', 1327060320);
}
if (empty($recipientAddress)) {
throw new FinisherException('The option "recipientAddress" must be set for the EmailFinisher.', 1327060200);
}
if (empty($senderAddress)) {
throw new FinisherException('The option "senderAddress" must be set for the EmailFinisher.', 1327060210);
}
$mail = $this->objectManager->get(MailMessage::class);
$mail->setFrom([$senderAddress => $senderName])
->setTo([$recipientAddress => $recipientName])
->setSubject($subject);
if (!empty($replyToAddress)) {
$mail->setReplyTo($replyToAddress);
}
if (!empty($carbonCopyAddress)) {
$mail->setCc($carbonCopyAddress);
}
if (!empty($blindCarbonCopyAddress)) {
$mail->setBcc($blindCarbonCopyAddress);
}
if ($format === self::FORMAT_PLAINTEXT) {
$mail->setBody($message, 'text/plain');
} else {
$mail->setBody($message, 'text/html');
}
$elements = $formRuntime->getFormDefinition()->getRenderablesRecursively();
if ($attachUploads) {
foreach ($elements as $element) {
if (!$element instanceof FileUpload) {
continue;
}
$file = $formRuntime[$element->getIdentifier()];
if ($file) {
if ($file instanceof FileReference) {
$file = $file->getOriginalResource();
}
$mail->attach(\Swift_Attachment::newInstance($file->getContents(), $file->getName(), $file->getMimeType()));
}
}
}
$mail->send();
}
/**
* @param FormRuntime $formRuntime
* @return StandaloneView
* @throws FinisherException
*/
protected function initializeStandaloneView(FormRuntime $formRuntime): StandaloneView
{
$format = ucfirst($this->parseOption('format'));
$standaloneView = $this->objectManager->get(StandaloneView::class);
if (isset($this->options['templatePathAndFilename'])) {
$this->options['templatePathAndFilename'] = strtr($this->options['templatePathAndFilename'], [
'{@format}' => $format
]);
$standaloneView->setTemplatePathAndFilename($this->options['templatePathAndFilename']);
} else {
if (!isset($this->options['templateName'])) {
throw new FinisherException('The option "templateName" must be set for the EmailFinisher.', 1327058829);
}
$this->options['templateName'] = strtr($this->options['templateName'], [
'{@format}' => $format
]);
$standaloneView->setTemplate($this->options['templateName']);
}
$standaloneView->assign('finisherVariableProvider', $this->finisherContext->getFinisherVariableProvider());
if (isset($this->options['templateRootPaths']) && is_array($this->options['templateRootPaths'])) {
$standaloneView->setTemplateRootPaths($this->options['templateRootPaths']);
}
if (isset($this->options['partialRootPaths']) && is_array($this->options['partialRootPaths'])) {
$standaloneView->setPartialRootPaths($this->options['partialRootPaths']);
}
if (isset($this->options['layoutRootPaths']) && is_array($this->options['layoutRootPaths'])) {
$standaloneView->setLayoutRootPaths($this->options['layoutRootPaths']);
}
if (isset($this->options['variables'])) {
$standaloneView->assignMultiple($this->options['variables']);
}
$standaloneView->assign('form', $formRuntime);
$standaloneView->getRenderingContext()
->getViewHelperVariableContainer()
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
return $standaloneView;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This exception is thrown in Form Finishers
*/
class FinisherException extends Exception
{
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
/**
* The context that is passed to each finisher when executed.
* It acts like an EventObject that is able to stop propagation.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
class FinisherContext
{
/**
* If TRUE further finishers won't be invoked
*
* @var bool
*/
protected $cancelled = false;
/**
* A reference to the Form Runtime that the finisher belongs to
*
* @var \TYPO3\CMS\Form\Domain\Runtime\FormRuntime
*/
protected $formRuntime;
/**
* The assigned controller context which might be needed by the finisher.
*
* @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
*/
protected $controllerContext;
/**
* The assigned controller context which might be needed by the finisher.
*
* @var FinisherVariableProvider
*/
protected $finisherVariableProvider;
/**
* @param FormRuntime $formRuntime
* @param ControllerContext $controllerContext
* @internal
*/
public function __construct(FormRuntime $formRuntime, ControllerContext $controllerContext)
{
$this->formRuntime = $formRuntime;
$this->controllerContext = $controllerContext;
}
/**
* Sets up the FinisherVariableProvider
*/
public function initializeObject()
{
$this->finisherVariableProvider = GeneralUtility::makeInstance(ObjectManager::class)
->get(FinisherVariableProvider::class);
}
/**
* Cancels the finisher invocation after the current finisher
*/
public function cancel()
{
$this->cancelled = true;
}
/**
* TRUE if no further finishers should be invoked. Defaults to FALSE
*
* @return bool
* @internal
*/
public function isCancelled(): bool
{
return $this->cancelled;
}
/**
* The Form Runtime that is associated with the current finisher
*
* @return FormRuntime
*/
public function getFormRuntime(): FormRuntime
{
return $this->formRuntime;
}
/**
* The values of the submitted form (after validation and property mapping)
*
* @return array
*/
public function getFormValues(): array
{
return $this->formRuntime->getFormState()->getFormValues();
}
/**
* @return ControllerContext
*/
public function getControllerContext(): ControllerContext
{
return $this->controllerContext;
}
/**
* @return FinisherVariableProvider
*/
public function getFinisherVariableProvider(): FinisherVariableProvider
{
return $this->finisherVariableProvider;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Finisher that can be attached to a form in order to be invoked
* as soon as the complete form is submitted
*
* Scope: frontend
*/
interface FinisherInterface
{
/**
* Executes the finisher
*
* @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
* @return string|null
*/
public function execute(FinisherContext $finisherContext);
/**
* @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
*/
public function setOptions(array $options);
/**
* Sets a single finisher option (@see setOptions())
*
* @param string $optionName name of the option to be set
* @param mixed $optionValue value of the option
*/
public function setOption(string $optionName, $optionValue);
/**
* Returns whether this finisher is enabled
*
* @return bool
*/
public function isEnabled(): bool;
}
@@ -0,0 +1,199 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
/**
* Store data for usage between the finishers.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
final class FinisherVariableProvider implements \ArrayAccess, \IteratorAggregate, \Countable
{
/**
* Two-dimensional object array storing the values. The first dimension is the finisher identifier,
* and the second dimension is the identifier for the data the finisher wants to store.
*
* @var array
*/
protected $objects = [];
/**
* Add a variable to the finisher container.
*
* @param string $finisherIdentifier
* @param string $key
* @param mixed $value
*/
public function add(string $finisherIdentifier, string $key, $value)
{
$this->addOrUpdate($finisherIdentifier, $key, $value);
}
/**
* Add a variable to the Variable Container.
* In case the value is already inside, it is silently overridden.
*
* @param string $finisherIdentifier
* @param string $key
* @param mixed $value
*/
public function addOrUpdate(string $finisherIdentifier, string $key, $value)
{
if (!array_key_exists($finisherIdentifier, $this->objects)) {
$this->objects[$finisherIdentifier] = [];
}
$this->objects[$finisherIdentifier] = ArrayUtility::setValueByPath(
$this->objects[$finisherIdentifier],
$key,
$value,
'.'
);
}
/**
* Gets a variable which is stored
*
* @param string $finisherIdentifier
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get(string $finisherIdentifier, string $key, $default = null)
{
if ($this->exists($finisherIdentifier, $key)) {
return ArrayUtility::getValueByPath($this->objects[$finisherIdentifier], $key, '.');
}
return $default;
}
/**
* Determine whether there is a variable stored for the given key
*
* @param string $finisherIdentifier
* @param string $key
* @return bool
*/
public function exists($finisherIdentifier, $key): bool
{
try {
ArrayUtility::getValueByPath($this->objects[$finisherIdentifier], $key, '.');
} catch (MissingArrayPathException $e) {
return false;
}
return true;
}
/**
* Remove a value from the variable container
*
* @param string $finisherIdentifier
* @param string $key
*/
public function remove(string $finisherIdentifier, string $key)
{
if ($this->exists($finisherIdentifier, $key)) {
$this->objects[$finisherIdentifier] = ArrayUtility::removeByPath(
$this->objects[$finisherIdentifier],
$key,
'.'
);
}
}
/**
* Clean up for serializing.
*
* @return array
*/
public function __sleep()
{
return ['objects'];
}
/**
* Whether an offset exists
*
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset An offset to check for.
* @return bool TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
return isset($this->objects[$offset]);
}
/**
* Offset to retrieve
*
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return $this->objects[$offset];
}
/**
* Offset to set
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function offsetSet($offset, $value)
{
$this->objects[$offset] = $value;
}
/**
* Offset to unset
*
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset The offset to unset.
*/
public function offsetUnset($offset)
{
unset($this->objects[$offset]);
}
/**
* @return \Traversable
*/
public function getIterator(): \Traversable
{
foreach ($this->objects as $offset => $value) {
yield $offset => $value;
}
}
/**
* Count elements of an object
*
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
*/
public function count()
{
return count($this->objects);
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Error\Message;
use TYPO3\CMS\Extbase\Error\Notice;
use TYPO3\CMS\Extbase\Error\Warning;
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
/**
* A simple finisher that adds a message to the FlashMessageContainer
*
* Usage:
* //...
* $flashMessageFinisher = $this->objectManager->get(FlashMessageFinisher::class);
* $flashMessageFinisher->setOptions(
* [
* 'messageBody' => 'Some message body',
* 'messageTitle' => 'Some message title',
* 'messageArguments' => ['foo' => 'bar'],
* 'severity' => \TYPO3\CMS\Core\Messaging\AbstractMessage::ERROR
* ]
* );
* $formDefinition->addFinisher($flashMessageFinisher);
* // ...
*
* Scope: frontend
*/
class FlashMessageFinisher extends AbstractFinisher
{
/**
* @var array
*/
protected $defaultOptions = [
'messageBody' => null,
'messageTitle' => '',
'messageArguments' => [],
'messageCode' => null,
'severity' => AbstractMessage::OK,
];
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*
* @throws FinisherException
*/
protected function executeInternal()
{
$messageBody = $this->parseOption('messageBody');
if (!is_string($messageBody)) {
throw new FinisherException(sprintf('The message body must be of type string, "%s" given.', gettype($messageBody)), 1335980069);
}
$messageTitle = $this->parseOption('messageTitle');
$messageArguments = $this->parseOption('messageArguments');
$messageCode = $this->parseOption('messageCode');
$severity = $this->parseOption('severity');
switch ($severity) {
case AbstractMessage::NOTICE:
$message = $this->objectManager->get(Notice::class, $messageBody, $messageCode, $messageArguments, $messageTitle);
break;
case AbstractMessage::WARNING:
$message = $this->objectManager->get(Warning::class, $messageBody, $messageCode, $messageArguments, $messageTitle);
break;
case AbstractMessage::ERROR:
$message = $this->objectManager->get(Error::class, $messageBody, $messageCode, $messageArguments, $messageTitle);
break;
default:
$message = $this->objectManager->get(Message::class, $messageBody, $messageCode, $messageArguments, $messageTitle);
break;
}
$flashMessage = $this->objectManager->get(
FlashMessage::class,
$message->render(),
$message->getTitle(),
$severity,
true
);
$this->finisherContext->getControllerContext()->getFlashMessageQueue()->addMessage($flashMessage);
}
}
@@ -0,0 +1,146 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
use TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException;
use TYPO3\CMS\Extbase\Mvc\Web\Request;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
/**
* This finisher redirects to another Controller.
*
* Scope: frontend
*/
class RedirectFinisher extends AbstractFinisher
{
/**
* @var array
*/
protected $defaultOptions = [
'pageUid' => 1,
'additionalParameters' => '',
'delay' => 0,
'statusCode' => 303,
];
/**
* @var \TYPO3\CMS\Extbase\Mvc\Web\Request
*/
protected $request;
/**
* @var \TYPO3\CMS\Extbase\Mvc\Web\Response
*/
protected $response;
/**
* @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
*/
protected $uriBuilder;
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*/
protected function executeInternal()
{
$formRuntime = $this->finisherContext->getFormRuntime();
$this->request = $formRuntime->getRequest();
$this->response = $formRuntime->getResponse();
$this->uriBuilder = $this->objectManager->get(UriBuilder::class);
$this->uriBuilder->setRequest($this->request);
$pageUid = (int)str_replace('pages_', '', $this->parseOption('pageUid'));
$additionalParameters = $this->parseOption('additionalParameters');
$additionalParameters = '&' . ltrim($additionalParameters, '&');
$delay = (int)$this->parseOption('delay');
$statusCode = (int)$this->parseOption('statusCode');
$this->finisherContext->cancel();
$this->redirect($pageUid, $additionalParameters, $delay, $statusCode);
}
/**
* Redirects the request to another page.
*
* Redirect will be sent to the client which then performs another request to the new URI.
*
* NOTE: This method only supports web requests and will thrown an exception
* if used with other request types.
*
* @param int $pageUid Target page uid. If NULL, the current page uid is used
* @param string $additionalParameters
* @param int $delay (optional) The delay in seconds. Default is no delay.
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
* @throws UnsupportedRequestTypeException If the request is not a web request
* @see forward()
*/
protected function redirect(int $pageUid = 1, string $additionalParameters = '', int $delay = 0, int $statusCode = 303)
{
if (!$this->request instanceof Request) {
throw new UnsupportedRequestTypeException('redirect() only supports web requests.', 1471776457);
}
$typolinkConfiguration = [
'parameter' => $pageUid,
'additionalParams' => $additionalParameters,
];
$redirectUri = $this->getTypoScriptFrontendController()->cObj->typoLink_URL($typolinkConfiguration);
$this->redirectToUri($redirectUri, $delay, $statusCode);
}
/**
* Redirects the web request to another uri.
*
* NOTE: This method only supports web requests and will thrown an exception if used with other request types.
*
* @param string $uri A string representation of a URI
* @param int $delay (optional) The delay in seconds. Default is no delay.
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
* @throws UnsupportedRequestTypeException If the request is not a web request
* @throws StopActionException
*/
protected function redirectToUri(string $uri, int $delay = 0, int $statusCode = 303)
{
if (!$this->request instanceof Request) {
throw new UnsupportedRequestTypeException('redirect() only supports web requests.', 1471776458);
}
$uri = $this->addBaseUriIfNecessary($uri);
$escapedUri = htmlentities($uri, ENT_QUOTES, 'utf-8');
$this->response->setContent('<html><head><meta http-equiv="refresh" content="' . (int)$delay . ';url=' . $escapedUri . '"/></head></html>');
if ($this->response instanceof \TYPO3\CMS\Extbase\Mvc\Web\Response) {
$this->response->setStatus($statusCode);
$this->response->setHeader('Location', (string)$uri);
}
throw new StopActionException('redirectToUri', 1477070964);
}
/**
* Adds the base uri if not already in place.
*
* @param string $uri The URI
* @return string
*/
protected function addBaseUriIfNecessary(string $uri): string
{
return GeneralUtility::locationHeaderUrl((string)$uri);
}
}
@@ -0,0 +1,372 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Finishers;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
/**
* This finisher saves the data from a submitted form into
* a database table.
*
* Configuration
* =============
*
* options.table (mandatory)
* -------------
* Save or update values into this table
*
* options.mode (default: insert)
* ------------
* Possible values are 'insert' or 'update'.
*
* insert: will create a new database row with the values from the
* submitted form and/or some predefined values.
* @see options.elements and options.databaseFieldMappings
* update: will update a given database row with the values from the
* submitted form and/or some predefined values.
* 'options.whereClause' is then required.
*
* options.whereClause
* -------------------
* This where clause will be used for an database update action
*
* options.elements
* ----------------
* Use this to map form element values to existing database columns.
* Each key within options.elements has to match with a
* form element identifier within your form definition.
* The value for each key within options.elements is an array with
* additional information.
*
* options.elements.<elementIdentifier>.mapOnDatabaseColumn (mandatory)
* --------------------------------------------------------
* The value from the submitted form element with the identifier
* '<elementIdentifier>' will be written into this database column
*
* options.elements.<elementIdentifier>.skipIfValueIsEmpty (default: false)
* ------------------------------------------------------
* Set this to true if the database column should not be written
* if the value from the submitted form element with the identifier
* '<elementIdentifier>' is empty (think about password fields etc.)
*
* options.elements.<elementIdentifier>.saveFileIdentifierInsteadOfUid (default: false)
* -------------------------------------------------------------------
* This setting only rules for form elements which creates a FAL object
* like FileUpload or ImageUpload.
* By default, the uid of the FAL object will be written into
* the database column. Set this to true if you want to store the
* FAL identifier (1:/user_uploads/some_uploaded_pic.jpg) instead.
*
* options.databaseColumnMappings
* ------------------------------
* Use this to map database columns to static values (which can be
* made dynamic through typoscript overrides of course).
* Each key within options.databaseColumnMappings has to match with a
* existing database column.
* The value for each key within options.databaseColumnMappings is an
* array with additional information.
*
* This mapping is done *before* the options.elements mapping.
* This means if you map a database column to a value through
* options.databaseColumnMappings and map a submitted form element
* value to the same database column, the submitted form element value
* will override the value you set within options.databaseColumnMappings.
*
* options.databaseColumnMappings.<databaseColumnName>.value
* ---------------------------------------------------------
* The value which will be written to the database column.
* You can use the FormRuntime accessor feature to access every
* getable property from the TYPO3\CMS\Form\Domain\Runtime\FormRuntime
* Read the description within
* TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher::parseOption
* In short: use something like {<elementIdentifier>} to get the value
* from the submitted form element with the identifier
* <elementIdentifier>
*
* Don't be confused. If you use the FormRuntime accessor feature within
* options.databaseColumnMappings, the functionality is nearly equal
* to the the options.elements configuration.
*
* options.databaseColumnMappings.<databaseColumnName>.skipIfValueIsEmpty (default: false)
* ---------------------------------------------------------------------
* Set this to true if the database column should not be written
* if the value from
* options.databaseColumnMappings.<databaseColumnName>.value is empty.
*
* Example
* =======
*
* finishers:
* -
* identifier: SaveToDatabase
* options:
* table: 'fe_users'
* mode: update
* whereClause:
* uid: 1
* databaseColumnMappings:
* pid:
* value: 1
* elements:
* text-1:
* mapOnDatabaseColumn: 'first_name'
* text-2:
* mapOnDatabaseColumn: 'last_name'
* text-3:
* mapOnDatabaseColumn: 'username'
* advancedpassword-1:
* mapOnDatabaseColumn: 'password'
* skipIfValueIsEmpty: true
*
* Multiple database operations
* ============================
*
* You can write options as an array to perform multiple database operations.
*
* finishers:
* -
* identifier: SaveToDatabase
* options:
* 1:
* table: 'my_table'
* mode: insert
* databaseColumnMappings:
* some_column:
* value: 'cool'
* 2:
* table: 'my_other_table'
* mode: update
* whereClause:
* pid: 1
* databaseColumnMappings:
* some_other_column:
* value: '{SaveToDatabase.insertedUids.1}'
*
* This would perform 2 database operations.
* One insert and one update.
* You can access the inserted uids with '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'
* If you perform an insert operation, the value of the inserted database row will be stored
* within the FinisherVariableProvider.
* <theArrayKeyNumberWithinOptions> references to the numeric key within options
* within which the insert operation is executed.
*
* Scope: frontend
*/
class SaveToDatabaseFinisher extends AbstractFinisher
{
/**
* @var array
*/
protected $defaultOptions = [
'table' => null,
'mode' => 'insert',
'whereClause' => [],
'elements' => [],
'databaseColumnMappings' => [],
];
/**
* @var \TYPO3\CMS\Core\Database\Connection
*/
protected $databaseConnection;
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*
* @throws FinisherException
*/
protected function executeInternal()
{
if (isset($this->options['table'])) {
$options[] = $this->options;
} else {
$options = $this->options;
}
foreach ($options as $optionKey => $option) {
$this->options = $option;
$this->process($optionKey);
}
}
/**
* Prepare data for saving to database
*
* @param array $elementsConfiguration
* @param array $databaseData
* @return mixed
*/
protected function prepareData(array $elementsConfiguration, array $databaseData)
{
foreach ($this->getFormValues() as $elementIdentifier => $elementValue) {
if (
($elementValue === null || $elementValue === '')
&& isset($elementsConfiguration[$elementIdentifier])
&& isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'])
&& $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true
) {
continue;
}
$element = $this->getElementByIdentifier($elementIdentifier);
if (
!$element instanceof FormElementInterface
|| !isset($elementsConfiguration[$elementIdentifier])
|| !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn'])
) {
continue;
}
if ($elementValue instanceof FileReference) {
if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) {
$saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'];
} else {
$saveFileIdentifierInsteadOfUid = false;
}
if ($saveFileIdentifierInsteadOfUid) {
$elementValue = $elementValue->getOriginalResource()->getCombinedIdentifier();
} else {
$elementValue = $elementValue->getOriginalResource()->getProperty('uid_local');
}
} elseif (is_array($elementValue)) {
$elementValue = implode(',', $elementValue);
} elseif ($elementValue instanceof \DateTimeInterface) {
$format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U';
$elementValue = $elementValue->format($format);
}
$databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
}
return $databaseData;
}
/**
* Perform the current database operation
*
* @param int $iterationCount
*/
protected function process(int $iterationCount)
{
$this->throwExceptionOnInconsistentConfiguration();
$table = $this->parseOption('table');
$elementsConfiguration = $this->parseOption('elements');
$databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings');
$this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
$databaseData = [];
foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) {
$value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value');
if (
empty($value)
&& $databaseColumnConfiguration['skipIfValueIsEmpty'] === true
) {
continue;
}
$databaseData[$databaseColumnName] = $value;
}
$databaseData = $this->prepareData($elementsConfiguration, $databaseData);
$this->saveToDatabase($databaseData, $table, $iterationCount);
}
/**
* Save or insert the values from
* $databaseData into the table $table
*
* @param array $databaseData
* @param string $table
* @param int $iterationCount
*/
protected function saveToDatabase(array $databaseData, string $table, int $iterationCount)
{
if (!empty($databaseData)) {
if ($this->options['mode'] === 'update') {
$whereClause = $this->options['whereClause'];
foreach ($whereClause as $columnName => $columnValue) {
$whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
}
$this->databaseConnection->update(
$table,
$databaseData,
$whereClause
);
} else {
$this->databaseConnection->insert($table, $databaseData);
$insertedUid = (int)$this->databaseConnection->lastInsertId($table);
$this->finisherContext->getFinisherVariableProvider()->add(
$this->shortFinisherIdentifier,
'insertedUids.' . $iterationCount,
$insertedUid
);
}
}
}
/**
* Throws an exception if some inconsistent configuration
* are detected.
*
* @throws FinisherException
*/
protected function throwExceptionOnInconsistentConfiguration()
{
if (
$this->options['mode'] === 'update'
&& empty($this->options['whereClause'])
) {
throw new FinisherException(
'An empty option "whereClause" is not allowed in update mode.',
1480469086
);
}
}
/**
* Returns the values of the submitted form
*
* @return array
*/
protected function getFormValues(): array
{
return $this->finisherContext->getFormValues();
}
/**
* Returns a form element object for a given identifier.
*
* @param string $elementIdentifier
* @return FormElementInterface|null
*/
protected function getElementByIdentifier(string $elementIdentifier)
{
return $this
->finisherContext
->getFormRuntime()
->getFormDefinition()
->getElementByIdentifier($elementIdentifier);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception as DomainException;
/**
* A generic Form model Exception
*/
class Exception extends DomainException
{
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\Exception;
/**
* This exception is thrown if two Form Elements with the same Identifier are added
* to a form.
*/
class DuplicateFormElementException extends Exception
{
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\Exception;
/**
* This exception is thrown if a Finisher Preset was not found,
* or if the implementationClassName was not set.
*/
class FinisherPresetNotFoundException extends Exception
{
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\Exception;
/**
* This exception is thrown if the form definition would get an inconsistent state, like
* adding a page to two different forms
*/
class FormDefinitionConsistencyException extends Exception
{
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\Exception;
/**
* This exception is thrown if a Validator Preset was not found,
* or if the implementationClassName was not set.
*/
class ValidatorPresetNotFoundException extends Exception
{
}
@@ -0,0 +1,725 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Extbase\Mvc\Web\Request;
use TYPO3\CMS\Extbase\Mvc\Web\Response;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotFoundException;
use TYPO3\CMS\Form\Domain\Finishers\FinisherInterface;
use TYPO3\CMS\Form\Domain\Model\Exception\DuplicateFormElementException;
use TYPO3\CMS\Form\Domain\Model\Exception\FinisherPresetNotFoundException;
use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractCompositeRenderable;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Exception as FormException;
use TYPO3\CMS\Form\Mvc\ProcessingRule;
/**
* This class encapsulates a complete *Form Definition*, with all of its pages,
* form elements, validation rules which apply and finishers which should be
* executed when the form is completely filled in.
*
* It is *not modified* when the form executes.
*
* The Anatomy Of A Form
* =====================
*
* A FormDefinition consists of multiple *Page* ({@link Page}) objects. When a
* form is displayed to the user, only one *Page* is visible at any given time,
* and there is a navigation to go back and forth between the pages.
*
* A *Page* consists of multiple *FormElements* ({@link FormElementInterface}, {@link AbstractFormElement}),
* which represent the input fields, textareas, checkboxes shown inside the page.
*
* *FormDefinition*, *Page* and *FormElement* have *identifier* properties, which
* must be unique for each given type (i.e. it is allowed that the FormDefinition and
* a FormElement have the *same* identifier, but two FormElements are not allowed to
* have the same identifier.
*
* Simple Example
* --------------
*
* Generally, you can create a FormDefinition manually by just calling the API
* methods on it, or you use a *Form Definition Factory* to build the form from
* another representation format such as YAML.
*
* /---code php
* $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm');
*
* $page1 = $this->objectManager->get(Page::class, 'page1');
* $formDefinition->addPage($page);
*
* $element1 = $this->objectManager->get(GenericFormElement::class, 'title', 'Textfield'); # the second argument is the type of the form element
* $page1->addElement($element1);
* \---
*
* Creating a Form, Using Abstract Form Element Types
* =====================================================
*
* While you can use the {@link FormDefinition::addPage} or {@link Page::addElement}
* methods and create the Page and FormElement objects manually, it is often better
* to use the corresponding create* methods ({@link FormDefinition::createPage}
* and {@link Page::createElement}), as you pass them an abstract *Form Element Type*
* such as *Text* or *Page*, and the system **automatically
* resolves the implementation class name and sets default values**.
*
* So the simple example from above should be rewritten as follows:
*
* /---code php
* $prototypeConfiguration = []; // We'll talk about this later
*
* $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm', $prototypeConfiguration);
* $page1 = $formDefinition->createPage('page1');
* $element1 = $page1->addElement('title', 'Textfield');
* \---
*
* Now, you might wonder how the system knows that the element *Textfield*
* is implemented using a GenericFormElement: **This is configured in the $prototypeConfiguration**.
*
* To make the example from above actually work, we need to add some sensible
* values to *$prototypeConfiguration*:
*
* <pre>
* $prototypeConfiguration = [
* 'formElementsDefinition' => [
* 'Page' => [
* 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page'
* ],
* 'Textfield' => [
* 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
* ]
* ]
* ]
* </pre>
*
* For each abstract *Form Element Type* we add some configuration; in the above
* case only the *implementation class name*. Still, it is possible to set defaults
* for *all* configuration options of such an element, as the following example
* shows:
*
* <pre>
* $prototypeConfiguration = [
* 'formElementsDefinition' => [
* 'Page' => [
* 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page',
* 'label' => 'this is the label of the page if nothing is specified'
* ],
* 'Textfield' => [
* 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement',
* 'label' = >'Default Label',
* 'defaultValue' => 'Default form element value',
* 'properties' => [
* 'placeholder' => 'Text which is shown if element is empty'
* ]
* ]
* ]
* ]
* </pre>
*
* Using Preconfigured $prototypeConfiguration
* ---------------------------------
*
* Often, it is not really useful to manually create the $prototypeConfiguration array.
*
* Most of it comes pre-configured inside the YAML settings of the extensions,
* and the {@link \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService} contains helper methods
* which return the ready-to-use *$prototypeConfiguration*.
*
* Property Mapping and Validation Rules
* =====================================
*
* Besides Pages and FormElements, the FormDefinition can contain information
* about the *format of the data* which is inputted into the form. This generally means:
*
* - expected Data Types
* - Property Mapping Configuration to be used
* - Validation Rules which should apply
*
* Background Info
* ---------------
* You might wonder why Data Types and Validation Rules are *not attached
* to each FormElement itself*.
*
* If the form should create a *hierarchical output structure* such as a multi-
* dimensional array or a PHP object, your expected data structure might look as follows:
* <pre>
* - person
* -- firstName
* -- lastName
* -- address
* --- street
* --- city
* </pre>
*
* Now, let's imagine you want to edit *person.address.street* and *person.address.city*,
* but want to validate that the *combination* of *street* and *city* is valid
* according to some address database.
*
* In this case, the form elements would be configured to fill *street* and *city*,
* but the *validator* needs to be attached to the *compound object* *address*,
* as both parts need to be validated together.
*
* Connecting FormElements to the output data structure
* ====================================================
*
* The *identifier* of the *FormElement* is most important, as it determines
* where in the output structure the value which is entered by the user is placed,
* and thus also determines which validation rules need to apply.
*
* Using the above example, if you want to create a FormElement for the *street*,
* you should use the identifier *person.address.street*.
*
* Rendering a FormDefinition
* ==========================
*
* In order to trigger *rendering* on a FormDefinition,
* the current {@link \TYPO3\CMS\Extbase\Mvc\Web\Request} needs to be bound to the FormDefinition,
* resulting in a {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} object which contains the *Runtime State* of the form
* (such as the currently inserted values).
*
* /---code php
* # $currentRequest and $currentResponse need to be available, f.e. inside a controller you would
* # use $this->request and $this->response; inside a ViewHelper you would use $this->controllerContext->getRequest()
* # and $this->controllerContext->getResponse()
* $form = $formDefinition->bind($currentRequest, $currentResponse);
*
* # now, you can use the $form object to get information about the currently
* # entered values into the form, etc.
* \---
*
* Refer to the {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} API doc for further information.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
class FormDefinition extends AbstractCompositeRenderable implements VariableRenderableInterface
{
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* The finishers for this form
*
* @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
*/
protected $finishers = [];
/**
* Property Mapping Rules, indexed by element identifier
*
* @var \TYPO3\CMS\Form\Mvc\ProcessingRule[]
*/
protected $processingRules = [];
/**
* Contains all elements of the form, indexed by identifier.
* Is used as internal cache as we need this really often.
*
* @var \TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface[]
*/
protected $elementsByIdentifier = [];
/**
* Form element default values in the format ['elementIdentifier' => 'default value']
*
* @var array
*/
protected $elementDefaultValues = [];
/**
* Renderer class name to be used.
*
* @var string
*/
protected $rendererClassName = '';
/**
* @var array
*/
protected $typeDefinitions;
/**
* @var array
*/
protected $validatorsDefinition;
/**
* @var array
*/
protected $finishersDefinition;
/**
* @var array
*/
protected $conditionContextDefinition;
/**
* The persistence identifier of the form
*
* @var string
*/
protected $persistenceIdentifier;
/**
* @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
* @internal
*/
public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* Constructor. Creates a new FormDefinition with the given identifier.
*
* @param string $identifier The Form Definition's identifier, must be a non-empty string.
* @param array $prototypeConfiguration overrides form defaults of this definition
* @param string $type element type of this form
* @param string $persistenceIdentifier the persistence identifier of the form
* @throws IdentifierNotValidException if the identifier was not valid
*/
public function __construct(
string $identifier,
array $prototypeConfiguration = [],
string $type = 'Form',
string $persistenceIdentifier = null
) {
$this->typeDefinitions = $prototypeConfiguration['formElementsDefinition'] ?? [];
$this->validatorsDefinition = $prototypeConfiguration['validatorsDefinition'] ?? [];
$this->finishersDefinition = $prototypeConfiguration['finishersDefinition'] ?? [];
$this->conditionContextDefinition = $prototypeConfiguration['conditionContextDefinition'] ?? [];
if (!is_string($identifier) || strlen($identifier) === 0) {
throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082503);
}
$this->identifier = $identifier;
$this->type = $type;
$this->persistenceIdentifier = $persistenceIdentifier;
if ($prototypeConfiguration !== []) {
$this->initializeFromFormDefaults();
}
}
/**
* Initialize the form defaults of the current type
*
* @throws TypeDefinitionNotFoundException
* @internal
*/
protected function initializeFromFormDefaults()
{
if (!isset($this->typeDefinitions[$this->type])) {
throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $this->type), 1474905835);
}
$typeDefinition = $this->typeDefinitions[$this->type];
$this->setOptions($typeDefinition);
}
/**
* Set multiple properties of this object at once.
* Every property which has a corresponding set* method can be set using
* the passed $options array.
*
* @param array $options
* @param bool $resetFinishers
* @internal
*/
public function setOptions(array $options, bool $resetFinishers = false)
{
if (isset($options['rendererClassName'])) {
$this->setRendererClassName($options['rendererClassName']);
}
if (isset($options['label'])) {
$this->setLabel($options['label']);
}
if (isset($options['renderingOptions'])) {
foreach ($options['renderingOptions'] as $key => $value) {
$this->setRenderingOption($key, $value);
}
}
if (isset($options['finishers'])) {
if ($resetFinishers) {
$this->finishers = [];
}
foreach ($options['finishers'] as $finisherConfiguration) {
$this->createFinisher($finisherConfiguration['identifier'], $finisherConfiguration['options'] ?? []);
}
}
if (isset($options['variants'])) {
foreach ($options['variants'] as $variantConfiguration) {
$this->createVariant($variantConfiguration);
}
}
ArrayUtility::assertAllArrayKeysAreValid(
$options,
['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label', 'variants']
);
}
/**
* Create a page with the given $identifier and attach this page to the form.
*
* - Create Page object based on the given $typeName
* - set defaults inside the Page object
* - attach Page object to this form
* - return the newly created Page object
*
* @param string $identifier Identifier of the new page
* @param string $typeName Type of the new page
* @return Page the newly created page
* @throws TypeDefinitionNotFoundException
*/
public function createPage(string $identifier, string $typeName = 'Page'): Page
{
if (!isset($this->typeDefinitions[$typeName])) {
throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1474905953);
}
$typeDefinition = $this->typeDefinitions[$typeName];
if (!isset($typeDefinition['implementationClassName'])) {
throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1477083126);
}
$implementationClassName = $typeDefinition['implementationClassName'];
$page = $this->objectManager->get($implementationClassName, $identifier, $typeName);
if (isset($typeDefinition['label'])) {
$page->setLabel($typeDefinition['label']);
}
if (isset($typeDefinition['renderingOptions'])) {
foreach ($typeDefinition['renderingOptions'] as $key => $value) {
$page->setRenderingOption($key, $value);
}
}
ArrayUtility::assertAllArrayKeysAreValid(
$typeDefinition,
['implementationClassName', 'label', 'renderingOptions', 'formEditor']
);
$this->addPage($page);
return $page;
}
/**
* Add a new page at the end of the form.
*
* Instead of this method, you should often use {@link createPage} instead.
*
* @param Page $page
* @throws FormDefinitionConsistencyException if Page is already added to a FormDefinition
* @see createPage
*/
public function addPage(Page $page)
{
$this->addRenderable($page);
}
/**
* Get the Form's pages
*
* @return array<Page> The Form's pages in the correct order
*/
public function getPages(): array
{
return $this->renderables;
}
/**
* Check whether a page with the given $index exists
*
* @param int $index
* @return bool TRUE if a page with the given $index exists, otherwise FALSE
*/
public function hasPageWithIndex(int $index): bool
{
return isset($this->renderables[$index]);
}
/**
* Get the page with the passed index. The first page has index zero.
*
* If page at $index does not exist, an exception is thrown. @see hasPageWithIndex()
*
* @param int $index
* @return Page the page, or NULL if none found.
* @throws FormException if the specified index does not exist
*/
public function getPageByIndex(int $index)
{
if (!$this->hasPageWithIndex($index)) {
throw new FormException(sprintf('There is no page with an index of %d', $index), 1329233627);
}
return $this->renderables[$index];
}
/**
* Adds the specified finisher to this form
*
* @param FinisherInterface $finisher
*/
public function addFinisher(FinisherInterface $finisher)
{
$this->finishers[] = $finisher;
}
/**
* @param string $finisherIdentifier identifier of the finisher as registered in the current form (for example: "Redirect")
* @param array $options options for this finisher in the format ['option1' => 'value1', 'option2' => 'value2', ...]
* @return FinisherInterface
* @throws FinisherPresetNotFoundException
*/
public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
{
if (isset($this->finishersDefinition[$finisherIdentifier]) && is_array($this->finishersDefinition[$finisherIdentifier]) && isset($this->finishersDefinition[$finisherIdentifier]['implementationClassName'])) {
$implementationClassName = $this->finishersDefinition[$finisherIdentifier]['implementationClassName'];
$defaultOptions = $this->finishersDefinition[$finisherIdentifier]['options'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
$finisher = $this->objectManager->get($implementationClassName, $finisherIdentifier);
$finisher->setOptions($defaultOptions);
$this->addFinisher($finisher);
return $finisher;
}
throw new FinisherPresetNotFoundException('The finisher preset identified by "' . $finisherIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328709784);
}
/**
* Gets all finishers of this form
*
* @return \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
*/
public function getFinishers(): array
{
return $this->finishers;
}
/**
* Add an element to the ElementsByIdentifier Cache.
*
* @param RenderableInterface $renderable
* @throws DuplicateFormElementException
* @internal
*/
public function registerRenderable(RenderableInterface $renderable)
{
if ($renderable instanceof FormElementInterface) {
if (isset($this->elementsByIdentifier[$renderable->getIdentifier()])) {
throw new DuplicateFormElementException(sprintf('A form element with identifier "%s" is already part of the form.', $renderable->getIdentifier()), 1325663761);
}
$this->elementsByIdentifier[$renderable->getIdentifier()] = $renderable;
}
}
/**
* Remove an element from the ElementsByIdentifier cache
*
* @param RenderableInterface $renderable
* @internal
*/
public function unregisterRenderable(RenderableInterface $renderable)
{
if ($renderable instanceof FormElementInterface) {
unset($this->elementsByIdentifier[$renderable->getIdentifier()]);
}
}
/**
* Get a Form Element by its identifier
*
* If identifier does not exist, returns NULL.
*
* @param string $elementIdentifier
* @return FormElementInterface The element with the given $elementIdentifier or NULL if none found
*/
public function getElementByIdentifier(string $elementIdentifier)
{
return $this->elementsByIdentifier[$elementIdentifier] ?? null;
}
/**
* Sets the default value of a form element
*
* @param string $elementIdentifier identifier of the form element. This supports property paths!
* @param mixed $defaultValue
* @internal
*/
public function addElementDefaultValue(string $elementIdentifier, $defaultValue)
{
$this->elementDefaultValues = ArrayUtility::setValueByPath(
$this->elementDefaultValues,
$elementIdentifier,
$defaultValue,
'.'
);
}
/**
* returns the default value of the specified form element
* or NULL if no default value was set
*
* @param string $elementIdentifier identifier of the form element. This supports property paths!
* @return mixed The elements default value
* @internal
*/
public function getElementDefaultValueByIdentifier(string $elementIdentifier)
{
return ObjectAccess::getPropertyPath($this->elementDefaultValues, $elementIdentifier);
}
/**
* Move $pageToMove before $referencePage
*
* @param Page $pageToMove
* @param Page $referencePage
*/
public function movePageBefore(Page $pageToMove, Page $referencePage)
{
$this->moveRenderableBefore($pageToMove, $referencePage);
}
/**
* Move $pageToMove after $referencePage
*
* @param Page $pageToMove
* @param Page $referencePage
*/
public function movePageAfter(Page $pageToMove, Page $referencePage)
{
$this->moveRenderableAfter($pageToMove, $referencePage);
}
/**
* Remove $pageToRemove from form
*
* @param Page $pageToRemove
*/
public function removePage(Page $pageToRemove)
{
$this->removeRenderable($pageToRemove);
}
/**
* Bind the current request & response to this form instance, effectively creating
* a new "instance" of the Form.
*
* @param Request $request
* @param Response $response
* @return FormRuntime
*/
public function bind(Request $request, Response $response): FormRuntime
{
return $this->objectManager->get(FormRuntime::class, $this, $request, $response);
}
/**
* @param string $propertyPath
* @return ProcessingRule
*/
public function getProcessingRule(string $propertyPath): ProcessingRule
{
if (!isset($this->processingRules[$propertyPath])) {
$this->processingRules[$propertyPath] = $this->objectManager->get(ProcessingRule::class);
}
return $this->processingRules[$propertyPath];
}
/**
* Get all mapping rules
*
* @return \TYPO3\CMS\Form\Mvc\ProcessingRule[]
* @internal
*/
public function getProcessingRules(): array
{
return $this->processingRules;
}
/**
* @return array
* @internal
*/
public function getTypeDefinitions(): array
{
return $this->typeDefinitions;
}
/**
* @return array
* @internal
*/
public function getValidatorsDefinition(): array
{
return $this->validatorsDefinition;
}
/**
* @return array
* @internal
*/
public function getConditionContextDefinition(): array
{
return $this->conditionContextDefinition;
}
/**
* Get the persistence identifier of the form
*
* @return string
* @internal
*/
public function getPersistenceIdentifier(): string
{
return $this->persistenceIdentifier;
}
/**
* Set the renderer class name
*
* @param string $rendererClassName
*/
public function setRendererClassName(string $rendererClassName)
{
$this->rendererClassName = $rendererClassName;
}
/**
* Get the classname of the renderer
*
* @return string
*/
public function getRendererClassName(): string
{
return $this->rendererClassName;
}
}
@@ -0,0 +1,166 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractRenderable;
/**
* A base form element, which is the starting point for creating custom (PHP-based)
* Form Elements.
*
* A *FormElement* is a part of a *Page*, which in turn is part of a FormDefinition.
* See {@link FormDefinition} for an in-depth explanation.
*
* Subclassing this class is a good starting-point for implementing custom PHP-based
* Form Elements.
*
* Most of the functionality and API is implemented in {@link \TYPO3\CMS\Form\Domain\Model\Renderable\AbstractRenderable}, so
* make sure to check out this class as well.
*
* Still, it is quite rare that you need to subclass this class; often
* you can just use the {@link \TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement} and replace some templates.
*
* Scope: frontend
* **This class is meant to be sub classed by developers.**
*/
abstract class AbstractFormElement extends AbstractRenderable implements FormElementInterface
{
/**
* @var array
*/
protected $properties = [];
/**
* Constructor. Needs this FormElement's identifier and the FormElement type
*
* @param string $identifier The FormElement's identifier
* @param string $type The Form Element Type
* @throws IdentifierNotValidException
*/
public function __construct(string $identifier, string $type)
{
if (!is_string($identifier) || strlen($identifier) === 0) {
throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082502);
}
$this->identifier = $identifier;
$this->type = $type;
}
/**
* Override this method in your custom FormElements if needed
*/
public function initializeFormElement()
{
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'initializeFormElement')) {
$hookObj->initializeFormElement(
$this
);
}
}
}
/**
* Get the global unique identifier of the element
*
* @return string
*/
public function getUniqueIdentifier(): string
{
$formDefinition = $this->getRootForm();
$uniqueIdentifier = sprintf('%s-%s', $formDefinition->getIdentifier(), $this->identifier);
$uniqueIdentifier = preg_replace('/[^a-zA-Z0-9_-]/', '_', $uniqueIdentifier);
return lcfirst($uniqueIdentifier);
}
/**
* Get the default value of the element
*
* @return mixed
*/
public function getDefaultValue()
{
$formDefinition = $this->getRootForm();
return $formDefinition->getElementDefaultValueByIdentifier($this->identifier);
}
/**
* Set the default value of the element
*
* @param mixed $defaultValue
*/
public function setDefaultValue($defaultValue)
{
$formDefinition = $this->getRootForm();
$currentDefaultValue = $formDefinition->getElementDefaultValueByIdentifier($this->identifier);
if (is_array($currentDefaultValue) && is_array($defaultValue)) {
ArrayUtility::mergeRecursiveWithOverrule($currentDefaultValue, $defaultValue);
$defaultValue = ArrayUtility::removeNullValuesRecursive($currentDefaultValue);
}
$formDefinition->addElementDefaultValue($this->identifier, $defaultValue);
}
/**
* Check if the element is required
*
* @return bool
*/
public function isRequired(): bool
{
foreach ($this->getValidators() as $validator) {
if ($validator instanceof NotEmptyValidator) {
return true;
}
}
return false;
}
/**
* Set a property of the element
*
* @param string $key
* @param mixed $value
*/
public function setProperty(string $key, $value)
{
if (is_array($value) && isset($this->properties[$key]) && is_array($this->properties[$key])) {
ArrayUtility::mergeRecursiveWithOverrule($this->properties[$key], $value);
$this->properties[$key] = ArrayUtility::removeNullValuesRecursive($this->properties[$key]);
} elseif ($value === null) {
unset($this->properties[$key]);
} else {
$this->properties[$key] = $value;
}
}
/**
* Get all properties
*
* @return array
*/
public function getProperties(): array
{
return $this->properties;
}
}
@@ -0,0 +1,179 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotFoundException;
use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotValidException;
use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractCompositeRenderable;
/**
* A base class for "section-like" form parts like "Page" or "Section" (which
* is rendered as "Fieldset")
*
* This class contains multiple FormElements ({@link FormElementInterface}).
*
* Please see {@link FormDefinition} for an in-depth explanation.
*
* **This class is NOT meant to be sub classed by developers.**
* Scope: frontend
*/
abstract class AbstractSection extends AbstractCompositeRenderable
{
/**
* Constructor. Needs the identifier and type of this element
*
* @param string $identifier The Section identifier
* @param string $type The Section type
* @throws IdentifierNotValidException if the identifier was no non-empty string
*/
public function __construct(string $identifier, string $type)
{
if (!is_string($identifier) || strlen($identifier) === 0) {
throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082501);
}
$this->identifier = $identifier;
$this->type = $type;
}
/**
* Get the child Form Elements
*
* @return FormElementInterface[] The Page's elements
*/
public function getElements(): array
{
return $this->renderables;
}
/**
* Get the child Form Elements
*
* @return FormElementInterface[] The Page's elements
*/
public function getElementsRecursively(): array
{
return $this->getRenderablesRecursively();
}
/**
* Add a new form element at the end of the section
*
* @param FormElementInterface $formElement The form element to add
* @throws FormDefinitionConsistencyException if FormElement is already added to a section
*/
public function addElement(FormElementInterface $formElement)
{
$this->addRenderable($formElement);
}
/**
* Create a form element with the given $identifier and attach it to this section/page.
*
* - Create Form Element object based on the given $typeName
* - set defaults inside the Form Element (based on the parent form's field defaults)
* - attach Form Element to this Section/Page
* - return the newly created Form Element object
*
*
* @param string $identifier Identifier of the new form element
* @param string $typeName type of the new form element
* @return FormElementInterface the newly created form element
* @throws TypeDefinitionNotFoundException
* @throws TypeDefinitionNotValidException
*/
public function createElement(string $identifier, string $typeName): FormElementInterface
{
$formDefinition = $this->getRootForm();
$typeDefinitions = $formDefinition->getTypeDefinitions();
if (isset($typeDefinitions[$typeName])) {
$typeDefinition = $typeDefinitions[$typeName];
} else {
$renderingOptions = $formDefinition->getRenderingOptions();
$skipUnknownElements = isset($renderingOptions['skipUnknownElements']) && $renderingOptions['skipUnknownElements'] === true;
if (!$skipUnknownElements) {
throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1382364019);
}
$element = GeneralUtility::makeInstance(ObjectManager::class)
->get(UnknownFormElement::class, $identifier, $typeName);
$this->addElement($element);
return $element;
}
if (!isset($typeDefinition['implementationClassName'])) {
throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1325689855);
}
$implementationClassName = $typeDefinition['implementationClassName'];
$element = GeneralUtility::makeInstance(ObjectManager::class)
->get($implementationClassName, $identifier, $typeName);
if (!$element instanceof FormElementInterface) {
throw new TypeDefinitionNotValidException(sprintf('The "implementationClassName" for element "%s" ("%s") does not implement the FormElementInterface.', $identifier, $implementationClassName), 1327318156);
}
unset($typeDefinition['implementationClassName']);
$this->addElement($element);
$element->setOptions($typeDefinition);
$element->initializeFormElement();
return $element;
}
/**
* Move FormElement $element before $referenceElement.
*
* Both $element and $referenceElement must be direct descendants of this Section/Page.
*
* @param FormElementInterface $elementToMove
* @param FormElementInterface $referenceElement
*/
public function moveElementBefore(FormElementInterface $elementToMove, FormElementInterface $referenceElement)
{
$this->moveRenderableBefore($elementToMove, $referenceElement);
}
/**
* Move FormElement $element after $referenceElement
*
* Both $element and $referenceElement must be direct descendants of this Section/Page.
*
* @param FormElementInterface $elementToMove
* @param FormElementInterface $referenceElement
*/
public function moveElementAfter(FormElementInterface $elementToMove, FormElementInterface $referenceElement)
{
$this->moveRenderableAfter($elementToMove, $referenceElement);
}
/**
* Remove $elementToRemove from this Section/Page
*
* @param FormElementInterface $elementToRemove
*/
public function removeElement(FormElementInterface $elementToRemove)
{
$this->removeRenderable($elementToRemove);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* A date picker form element
*
* Scope: frontend
*/
class DatePicker extends AbstractFormElement
{
/**
* Initializes the Form Element by setting the data type to "DateTime"
* @internal
*/
public function initializeFormElement()
{
$this->setDataType('DateTime');
parent::initializeFormElement();
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* A generic file upload form element
*
* Scope: frontend
*/
class FileUpload extends AbstractFormElement
{
/**
* Initializes the Form Element by setting the data type to a Extbase File Reference
* @internal
*/
public function initializeFormElement()
{
$this->setDataType('TYPO3\CMS\Extbase\Domain\Model\FileReference');
parent::initializeFormElement();
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
/**
* A base form element interface, which can be the starting point for creating
* custom (PHP-based) Form Elements.
*
* A *FormElement* is a part of a *Page*, which in turn is part of a FormDefinition.
* See {@link FormDefinition} for an in-depth explanation.
*
* **Often, you should rather subclass {@link AbstractFormElement} instead of
* implementing this interface.**
*
* Scope: frontend
*/
interface FormElementInterface extends RenderableInterface
{
/**
* Will be called as soon as the element is (tried to be) added to a form
* @see registerInFormIfPossible()
*
* @internal
*/
public function initializeFormElement();
/**
* Returns a unique identifier of this element.
* While element identifiers are only unique within one form,
* this includes the identifier of the form itself, making it "globally" unique
*
* @return string the "globally" unique identifier of this element
*/
public function getUniqueIdentifier(): string;
/**
* Get the default value with which the Form Element should be initialized
* during display.
*
* @return mixed the default value for this Form Element
*/
public function getDefaultValue();
/**
* Set the default value with which the Form Element should be initialized
* during display.
*
* @param mixed $defaultValue the default value for this Form Element
*/
public function setDefaultValue($defaultValue);
/**
* Set an element-specific configuration property.
*
* @param string $key
* @param mixed $value
*/
public function setProperty(string $key, $value);
/**
* Get all element-specific configuration properties
*
* @return array
*/
public function getProperties(): array;
/**
* Set a rendering option
*
* @param string $key
* @param mixed $value
*/
public function setRenderingOption(string $key, $value);
/**
* Returns the child validators of the ConjunctionValidator that is registered for this element
*
* @return \SplObjectStorage<ValidatorInterface>
* @internal
*/
public function getValidators(): \SplObjectStorage;
/**
* Registers a validator for this element
*
* @param ValidatorInterface $validator
*/
public function addValidator(ValidatorInterface $validator);
/**
* Set the target data type for this element
*
* @param string $dataType the target data type
*/
public function setDataType(string $dataType);
/**
* Whether or not this element is required
*
* @return bool
*/
public function isRequired(): bool;
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* A generic form element
*
* Scope: frontend
*/
class GenericFormElement extends AbstractFormElement
{
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotValidException;
/**
* A GridContainer, being part of a bigger Page
*
* This class contains multiple GridRow elements.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
class GridContainer extends Section implements GridContainerInterface
{
/**
* Initializes the Form Element
*
* @internal
*/
public function initializeFormElement()
{
trigger_error(
'"GridContainer" form elements will be removed in TYPO3 v10.0. Use "GridRow" form elements instead.',
E_USER_DEPRECATED
);
parent::initializeFormElement();
}
/**
* Register this element at the parent form, if there is a connection to the parent form.
*
* @throws TypeDefinitionNotValidException
* @internal
*/
public function registerInFormIfPossible()
{
foreach ($this->getElementsRecursively() as $renderable) {
if ($renderable instanceof GridContainerInterface) {
throw new TypeDefinitionNotValidException(
sprintf('Grid containers ("%s") within grid containers ("%s") are not allowed.', $renderable->getIdentifier(), $this->getIdentifier()),
1489412790
);
}
}
parent::registerInFormIfPossible();
}
/**
* Add a new row element at the end of the grid container
*
* @param FormElementInterface $formElement The form element to add
*/
public function addElement(FormElementInterface $formElement)
{
if (!$formElement instanceof GridRowInterface) {
throw new TypeDefinitionNotValidException(
sprintf('The "implementationClassName" for element "%s" (type "%s") does not implement the GridRowInterface.', $formElement->getIdentifier(), $formElement->getType()),
1489486301
);
}
$this->addRenderable($formElement);
}
/**
* Create a form element with the given $identifier and attach it to this container.
*
* @param string $identifier Identifier of the new form element
* @param string $typeName type of the new form element
* @return FormElementInterface the newly created grid row
* @throws TypeDefinitionNotValidException
*/
public function createElement(string $identifier, string $typeName): FormElementInterface
{
$element = parent::createElement($identifier, $typeName);
if (!$element instanceof GridRowInterface) {
throw new TypeDefinitionNotValidException(
sprintf('The "implementationClassName" for element "%s" (type "%s") does not implement the GridRowInterface.', $identifier, $typeName),
1489486302
);
}
return $element;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Scope: frontend
*/
interface GridContainerInterface extends FormElementInterface
{
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotValidException;
/**
* A grid row, being part of a grid container
*
* This class contains multiple FormElements ({@link FormElementInterface}).
*
* Please see {@link FormDefinition} for an in-depth explanation.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
class GridRow extends Section implements GridRowInterface
{
/**
* Add a new form element at the end of the grid row
*
* @param FormElementInterface $formElement The form element to add
* @throws TypeDefinitionNotValidException if FormElement is already added to a section
*/
public function addElement(FormElementInterface $formElement)
{
if ($formElement instanceof GridContainerInterface) {
throw new TypeDefinitionNotValidException(
sprintf('Grid containers ("%s") within grid rows ("%s") are not allowed.', $formElement->getIdentifier(), $this->getIdentifier()),
1489413379
);
}
$this->addRenderable($formElement);
}
/**
* Create a form element with the given $identifier and attach it to this container.
*
* @param string $identifier Identifier of the new form element
* @param string $typeName type of the new form element
* @return GridRowInterface the newly created frid row
* @throws TypeDefinitionNotValidException
*/
public function createElement(string $identifier, string $typeName): FormElementInterface
{
$element = parent::createElement($identifier, $typeName);
if ($element instanceof GridContainerInterface) {
throw new TypeDefinitionNotValidException(
sprintf('Grid containers ("%s") within grid rows ("%s") are not allowed.', $element->getIdentifier(), $this->getIdentifier()),
1489413538
);
}
return $element;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Scope: frontend
*/
interface GridRowInterface extends FormElementInterface
{
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
use TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface;
use TYPO3\CMS\Form\Exception as FormException;
/**
* A Page, being part of a bigger FormDefinition. It contains numerous FormElements
* as children.
*
* A FormDefinition consists of multiple Pages, where only one page is visible
* at any given time.
*
* Most of the API of this object is implemented in {@link AbstractSection},
* so make sure to review this class as well.
*
* Please see {@link FormDefinition} for an in-depth explanation.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
class Page extends AbstractSection
{
/**
* Constructor. Needs this Page's identifier
*
* @param string $identifier The Page's identifier
* @param string $type The Page's type
* @throws \TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException if the identifier was no non-empty string
*/
public function __construct(string $identifier, string $type = 'Page')
{
parent::__construct($identifier, $type);
}
/**
* Set the parent renderable
*
* @param CompositeRenderableInterface $parentRenderable
* @throws FormException
*/
public function setParentRenderable(CompositeRenderableInterface $parentRenderable)
{
if (!($parentRenderable instanceof FormDefinition)) {
throw new FormException(sprintf('The specified parentRenderable must be a FormDefinition, got "%s"', is_object($parentRenderable) ? get_class($parentRenderable) : gettype($parentRenderable)), 1329233747);
}
parent::setParentRenderable($parentRenderable);
}
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator;
/**
* A Section, being part of a bigger Page
*
* This class contains multiple FormElements ({@link FormElementInterface}).
*
* Please see {@link FormDefinition} for an in-depth explanation.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
class Section extends AbstractSection implements FormElementInterface
{
/**
* @var array
*/
protected $properties = [];
/**
* Will be called as soon as the element is (tried to be) added to a form
* @see registerInFormIfPossible()
*
* @internal
*/
public function initializeFormElement()
{
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'initializeFormElement')) {
$hookObj->initializeFormElement(
$this
);
}
}
}
/**
* Returns a unique identifier of this element.
* While element identifiers are only unique within one form,
* this includes the identifier of the form itself, making it "globally" unique
*
* @return string the "globally" unique identifier of this element
*/
public function getUniqueIdentifier(): string
{
$formDefinition = $this->getRootForm();
return sprintf('%s-%s', $formDefinition->getIdentifier(), $this->identifier);
}
/**
* Get the default value with which the Form Element should be initialized
* during display.
* Note: This is currently not used for section elements
*
* @return mixed the default value for this Form Element
*/
public function getDefaultValue()
{
return null;
}
/**
* Set the default value with which the Form Element should be initialized
* during display.
* Note: This is currently ignored for section elements
*
* @param mixed $defaultValue the default value for this Form Element
*/
public function setDefaultValue($defaultValue)
{
}
/**
* Get all element-specific configuration properties
*
* @return array
*/
public function getProperties(): array
{
return $this->properties;
}
/**
* Set an element-specific configuration property.
*
* @param string $key
* @param mixed $value
*/
public function setProperty(string $key, $value)
{
if (is_array($value) && isset($this->properties[$key]) && is_array($this->properties[$key])) {
ArrayUtility::mergeRecursiveWithOverrule($this->properties[$key], $value);
$this->properties[$key] = ArrayUtility::removeNullValuesRecursive($this->properties[$key]);
} elseif ($value === null) {
unset($this->properties[$key]);
} else {
$this->properties[$key] = $value;
}
}
/**
* Whether or not this element is required
*
* @return bool
*/
public function isRequired(): bool
{
foreach ($this->getValidators() as $validator) {
if ($validator instanceof NotEmptyValidator) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,135 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractRenderable;
/**
* A Form Element that has no definition.
*
* Scope: frontend
*/
class UnknownFormElement extends AbstractRenderable implements FormElementInterface
{
/**
* Constructor. Needs this FormElement's identifier and the FormElement type
*
* @param string $identifier The FormElement's identifier
* @param string $type The Form Element Type
* @throws IdentifierNotValidException
*/
public function __construct(string $identifier, string $type)
{
if (!is_string($identifier) || strlen($identifier) === 0) {
throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1382364370);
}
$this->identifier = $identifier;
$this->type = $type;
}
/**
* Sets up the form element
*/
public function initializeFormElement()
{
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'initializeFormElement')) {
$hookObj->initializeFormElement(
$this
);
}
}
}
/**
* Returns a unique identifier of this element.
* While element identifiers are only unique within one form,
* this includes the identifier of the form itself, making it "globally" unique
*
* @return string the "globally" unique identifier of this element
*/
public function getUniqueIdentifier(): string
{
$formDefinition = $this->getRootForm();
$uniqueIdentifier = sprintf('%s-%s', $formDefinition->getIdentifier(), $this->identifier);
$uniqueIdentifier = preg_replace('/[^a-zA-Z0-9-_]/', '_', $uniqueIdentifier);
return lcfirst($uniqueIdentifier);
}
/**
* Get the template name of the renderable
*
* @return string
*/
public function getTemplateName(): string
{
return 'UnknownElement';
}
/**
* @return mixed the default value for this Form Element
* @internal
*/
public function getDefaultValue()
{
return null;
}
/**
* Not used in this implementation
*
* @param mixed $defaultValue the default value for this Form Element
* @internal
*/
public function setDefaultValue($defaultValue)
{
}
/**
* Not used in this implementation
*
* @param string $key
* @param mixed $value
* @internal
*/
public function setProperty(string $key, $value)
{
}
/**
* @return array
* @internal
*/
public function getProperties(): array
{
return [];
}
/**
* @return bool
* @internal
*/
public function isRequired(): bool
{
return false;
}
}
@@ -0,0 +1,206 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
/**
* Convenience base class which implements common functionality for most
* classes which implement CompositeRenderableInterface, i.e. have **child renderable elements**.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
abstract class AbstractCompositeRenderable extends AbstractRenderable implements CompositeRenderableInterface
{
/**
* array of child renderables
*
* @var \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface[]
*/
protected $renderables = [];
/**
* Add a renderable to the list of child renderables.
*
* This function will be wrapped by the subclasses, f.e. with an "addPage"
* or "addElement" method with the correct type hint.
*
* @param RenderableInterface $renderable
* @throws FormDefinitionConsistencyException
* @internal
*/
protected function addRenderable(RenderableInterface $renderable)
{
if ($renderable->getParentRenderable() !== null) {
throw new FormDefinitionConsistencyException(sprintf('The renderable with identifier "%s" is already added to another element (element identifier: "%s").', $renderable->getIdentifier(), $renderable->getParentRenderable()->getIdentifier()), 1325665144);
}
$renderable->setIndex(count($this->renderables));
$renderable->setParentRenderable($this);
$this->renderables[] = $renderable;
}
/**
* Move $renderableToMove before $referenceRenderable
*
* This function will be wrapped by the subclasses, f.e. with an "movePageBefore"
* or "moveElementBefore" method with the correct type hint.
*
* @param RenderableInterface $renderableToMove
* @param RenderableInterface $referenceRenderable
* @throws FormDefinitionConsistencyException
* @internal
*/
protected function moveRenderableBefore(RenderableInterface $renderableToMove, RenderableInterface $referenceRenderable)
{
if ($renderableToMove->getParentRenderable() !== $referenceRenderable->getParentRenderable() || $renderableToMove->getParentRenderable() !== $this) {
throw new FormDefinitionConsistencyException('Moved renderables need to be part of the same parent element.', 1326089744);
}
$reorderedRenderables = [];
$i = 0;
foreach ($this->renderables as $renderable) {
if ($renderable === $renderableToMove) {
continue;
}
if ($renderable === $referenceRenderable) {
$reorderedRenderables[] = $renderableToMove;
$renderableToMove->setIndex($i);
$i++;
}
$reorderedRenderables[] = $renderable;
$renderable->setIndex($i);
$i++;
}
$this->renderables = $reorderedRenderables;
}
/**
* Move $renderableToMove after $referenceRenderable
*
* This function will be wrapped by the subclasses, f.e. with an "movePageAfter"
* or "moveElementAfter" method with the correct type hint.
*
* @param RenderableInterface $renderableToMove
* @param RenderableInterface $referenceRenderable
* @throws FormDefinitionConsistencyException
* @internal
*/
protected function moveRenderableAfter(RenderableInterface $renderableToMove, RenderableInterface $referenceRenderable)
{
if ($renderableToMove->getParentRenderable() !== $referenceRenderable->getParentRenderable() || $renderableToMove->getParentRenderable() !== $this) {
throw new FormDefinitionConsistencyException('Moved renderables need to be part of the same parent element.', 1477083145);
}
$reorderedRenderables = [];
$i = 0;
foreach ($this->renderables as $renderable) {
if ($renderable === $renderableToMove) {
continue;
}
$reorderedRenderables[] = $renderable;
$renderable->setIndex($i);
$i++;
if ($renderable === $referenceRenderable) {
$reorderedRenderables[] = $renderableToMove;
$renderableToMove->setIndex($i);
$i++;
}
}
$this->renderables = $reorderedRenderables;
}
/**
* Returns all RenderableInterface instances of this composite renderable recursively
*
* @return RenderableInterface[]
* @internal
*/
public function getRenderablesRecursively(): array
{
$renderables = [];
foreach ($this->renderables as $renderable) {
$renderables[] = $renderable;
if ($renderable instanceof CompositeRenderableInterface) {
$renderables = array_merge($renderables, $renderable->getRenderablesRecursively());
}
}
return $renderables;
}
/**
* Remove a renderable from this renderable.
*
* This function will be wrapped by the subclasses, f.e. with an "removePage"
* or "removeElement" method with the correct type hint.
*
* @param RenderableInterface $renderableToRemove
* @throws FormDefinitionConsistencyException
* @internal
*/
protected function removeRenderable(RenderableInterface $renderableToRemove)
{
if ($renderableToRemove->getParentRenderable() !== $this) {
throw new FormDefinitionConsistencyException('The renderable to be removed must be part of the calling parent renderable.', 1326090127);
}
$updatedRenderables = [];
foreach ($this->renderables as $renderable) {
if ($renderable === $renderableToRemove) {
continue;
}
$updatedRenderables[] = $renderable;
}
$this->renderables = $updatedRenderables;
$renderableToRemove->onRemoveFromParentRenderable();
}
/**
* Register this element at the parent form, if there is a connection to the parent form.
*
* @internal
*/
public function registerInFormIfPossible()
{
parent::registerInFormIfPossible();
foreach ($this->renderables as $renderable) {
$renderable->registerInFormIfPossible();
}
}
/**
* This function is called after a renderable has been removed from its parent
* renderable.
* This just passes the event down to all child renderables of this composite renderable.
*
* @internal
*/
public function onRemoveFromParentRenderable()
{
foreach ($this->renderables as $renderable) {
$renderable->onRemoveFromParentRenderable();
}
parent::onRemoveFromParentRenderable();
}
}
@@ -0,0 +1,482 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
use TYPO3\CMS\Form\Domain\Model\Exception\ValidatorPresetNotFoundException;
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
/**
* Convenience base class which implements common functionality for most
* classes which implement RenderableInterface.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
abstract class AbstractRenderable implements RenderableInterface, VariableRenderableInterface
{
/**
* Abstract "type" of this Renderable. Is used during the rendering process
* to determine the template file or the View PHP class being used to render
* the particular element.
*
* @var string
*/
protected $type;
/**
* The identifier of this renderable
*
* @var string
*/
protected $identifier;
/**
* The parent renderable
*
* @var CompositeRenderableInterface
*/
protected $parentRenderable;
/**
* The label of this renderable
*
* @var string
*/
protected $label = '';
/**
* associative array of rendering options
*
* @var array
*/
protected $renderingOptions = [];
/**
* The position of this renderable inside the parent renderable.
*
* @var int
*/
protected $index = 0;
/**
* The name of the template file of the renderable.
*
* @var string
*/
protected $templateName = '';
/**
* associative array of rendering variants
*
* @var array
*/
protected $variants = [];
/**
* Get the type of the renderable
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Get the identifier of the element
*
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* Set the identifier of the element
*
* @param string $identifier
*/
public function setIdentifier(string $identifier)
{
$this->identifier = $identifier;
}
/**
* Set multiple properties of this object at once.
* Every property which has a corresponding set* method can be set using
* the passed $options array.
*
* @param array $options
* @param bool $resetValidators
*/
public function setOptions(array $options, bool $resetValidators = false)
{
if (isset($options['label'])) {
$this->setLabel($options['label']);
}
if (isset($options['defaultValue'])) {
$this->setDefaultValue($options['defaultValue']);
}
if (isset($options['properties'])) {
foreach ($options['properties'] as $key => $value) {
$this->setProperty($key, $value);
}
}
if (isset($options['renderingOptions'])) {
foreach ($options['renderingOptions'] as $key => $value) {
$this->setRenderingOption($key, $value);
}
}
if (isset($options['validators'])) {
$runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
$configurationHashes = $runtimeCache->get('formAbstractRenderableConfigurationHashes') ?: [];
if ($resetValidators) {
$processingRule = $this->getRootForm()->getProcessingRule($this->getIdentifier());
foreach ($this->getValidators() as $validator) {
$processingRule->removeValidator($validator);
}
$configurationHashes = [];
}
foreach ($options['validators'] as $validatorConfiguration) {
$configurationHash = md5(
spl_object_hash($this) .
json_encode($validatorConfiguration)
);
if (in_array($configurationHash, $configurationHashes)) {
continue;
}
$this->createValidator($validatorConfiguration['identifier'], $validatorConfiguration['options'] ?? []);
$configurationHashes[] = $configurationHash;
$runtimeCache->set('formAbstractRenderableConfigurationHashes', $configurationHashes);
}
}
if (isset($options['variants'])) {
foreach ($options['variants'] as $variantConfiguration) {
$this->createVariant($variantConfiguration);
}
}
ArrayUtility::assertAllArrayKeysAreValid(
$options,
['label', 'defaultValue', 'properties', 'renderingOptions', 'validators', 'formEditor', 'variants']
);
}
/**
* Create a validator for the element
*
* @param string $validatorIdentifier
* @param array $options
* @return mixed
* @throws ValidatorPresetNotFoundException
*/
public function createValidator(string $validatorIdentifier, array $options = [])
{
$validatorsDefinition = $this->getRootForm()->getValidatorsDefinition();
if (isset($validatorsDefinition[$validatorIdentifier]) && is_array($validatorsDefinition[$validatorIdentifier]) && isset($validatorsDefinition[$validatorIdentifier]['implementationClassName'])) {
$implementationClassName = $validatorsDefinition[$validatorIdentifier]['implementationClassName'];
$defaultOptions = $validatorsDefinition[$validatorIdentifier]['options'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
$validator = GeneralUtility::makeInstance(ObjectManager::class)
->get($implementationClassName, $defaultOptions);
$this->addValidator($validator);
return $validator;
}
throw new ValidatorPresetNotFoundException('The validator preset identified by "' . $validatorIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328710202);
}
/**
* Add a validator to the element
*
* @param ValidatorInterface $validator
*/
public function addValidator(ValidatorInterface $validator)
{
$formDefinition = $this->getRootForm();
$formDefinition->getProcessingRule($this->getIdentifier())->addValidator($validator);
}
/**
* Get all validators on the element
*
* @return \SplObjectStorage
* @internal
*/
public function getValidators(): \SplObjectStorage
{
$formDefinition = $this->getRootForm();
return $formDefinition->getProcessingRule($this->getIdentifier())->getValidators();
}
/**
* Set the datatype
*
* @param string $dataType
*/
public function setDataType(string $dataType)
{
$formDefinition = $this->getRootForm();
$formDefinition->getProcessingRule($this->getIdentifier())->setDataType($dataType);
}
/**
* Get the classname of the renderer
*
* @return string
*/
public function getRendererClassName(): string
{
return $this->getRootForm()->getRendererClassName();
}
/**
* Get all rendering options
*
* @return array
*/
public function getRenderingOptions(): array
{
return $this->renderingOptions;
}
/**
* Set the rendering option $key to $value.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function setRenderingOption(string $key, $value)
{
if (is_array($value) && isset($this->renderingOptions[$key]) && is_array($this->renderingOptions[$key])) {
ArrayUtility::mergeRecursiveWithOverrule($this->renderingOptions[$key], $value);
$this->renderingOptions[$key] = ArrayUtility::removeNullValuesRecursive($this->renderingOptions[$key]);
} elseif ($value === null) {
unset($this->renderingOptions[$key]);
} else {
$this->renderingOptions[$key] = $value;
}
}
/**
* Get the parent renderable
*
* @return CompositeRenderableInterface|null
*/
public function getParentRenderable()
{
return $this->parentRenderable;
}
/**
* Set the parent renderable
*
* @param CompositeRenderableInterface $parentRenderable
*/
public function setParentRenderable(CompositeRenderableInterface $parentRenderable)
{
$this->parentRenderable = $parentRenderable;
$this->registerInFormIfPossible();
}
/**
* Get the root form this element belongs to
*
* @return FormDefinition
* @throws FormDefinitionConsistencyException
*/
public function getRootForm(): FormDefinition
{
$rootRenderable = $this->parentRenderable;
while ($rootRenderable !== null && !($rootRenderable instanceof FormDefinition)) {
$rootRenderable = $rootRenderable->getParentRenderable();
}
if ($rootRenderable === null) {
throw new FormDefinitionConsistencyException(sprintf('The form element "%s" is not attached to a parent form.', $this->identifier), 1326803398);
}
return $rootRenderable;
}
/**
* Register this element at the parent form, if there is a connection to the parent form.
*
* @internal
*/
public function registerInFormIfPossible()
{
try {
$rootForm = $this->getRootForm();
$rootForm->registerRenderable($this);
} catch (FormDefinitionConsistencyException $exception) {
}
}
/**
* Triggered when the renderable is removed from it's parent
*
* @internal
*/
public function onRemoveFromParentRenderable()
{
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'beforeRemoveFromParentRenderable')) {
$hookObj->beforeRemoveFromParentRenderable(
$this
);
}
}
try {
$rootForm = $this->getRootForm();
$rootForm->unregisterRenderable($this);
} catch (FormDefinitionConsistencyException $exception) {
}
$this->parentRenderable = null;
}
/**
* Get the index of the renderable
*
* @return int
* @internal
*/
public function getIndex(): int
{
return $this->index;
}
/**
* Set the index of the renderable
*
* @param int $index
* @internal
*/
public function setIndex(int $index)
{
$this->index = $index;
}
/**
* Get the label of the renderable
*
* @return string
*/
public function getLabel(): string
{
return $this->label;
}
/**
* Set the label which shall be displayed next to the form element
*
* @param string $label
*/
public function setLabel(string $label)
{
$this->label = $label;
}
/**
* Get the templateName name of the renderable
*
* @return string
*/
public function getTemplateName(): string
{
return empty($this->renderingOptions['templateName'])
? $this->type
: $this->renderingOptions['templateName'];
}
/**
* Returns whether this renderable is enabled
*
* @return bool
*/
public function isEnabled(): bool
{
return !isset($this->renderingOptions['enabled']) || (bool)$this->renderingOptions['enabled'] === true;
}
/**
* Get all rendering variants
*
* @return RenderableVariantInterface[]
*/
public function getVariants(): array
{
return $this->variants;
}
/**
* @param array $options
* @return RenderableVariantInterface
*/
public function createVariant(array $options): RenderableVariantInterface
{
$identifier = $options['identifier'] ?? '';
unset($options['identifier']);
$variant = GeneralUtility::makeInstance(ObjectManager::class)
->get(RenderableVariant::class, $identifier, $options, $this);
$this->addVariant($variant);
return $variant;
}
/**
* Adds the specified variant to this form element
*
* @param RenderableVariantInterface $variant
*/
public function addVariant(RenderableVariantInterface $variant)
{
$this->variants[$variant->getIdentifier()] = $variant;
}
/**
* Apply the specified variant to this form element
* regardless of their conditions
*
* @param RenderableVariantInterface $variant
*/
public function applyVariant(RenderableVariantInterface $variant)
{
$variant->apply();
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Interface which all Form Parts must adhere to **when they have sub elements**.
* This includes especially "FormDefinition" and "Page".
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
interface CompositeRenderableInterface extends RenderableInterface
{
/**
* Returns all RenderableInterface instances of this composite renderable recursively
*
* @return \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface[]
* @internal
*/
public function getRenderablesRecursively(): array;
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Base interface which all Form Parts except the FormDefinition must adhere
* to (i.e. all elements which are NOT the root of a Form).
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
interface RenderableInterface extends RootRenderableInterface
{
/**
* Return the parent renderable
*
* @return CompositeRenderableInterface|null the parent renderable
* @internal
*/
public function getParentRenderable();
/**
* Set the new parent renderable. You should not call this directly;
* it is automatically called by addRenderable.
*
* This method should also register itself at the parent form, if possible.
*
* @param CompositeRenderableInterface $renderable
* @internal
*/
public function setParentRenderable(CompositeRenderableInterface $renderable);
/**
* Set the index of this renderable inside the parent renderable
*
* @param int $index
* @internal
*/
public function setIndex(int $index);
/**
* Get the index inside the parent renderable
*
* @return int
*/
public function getIndex(): int;
/**
* This function is called after a renderable has been removed from its parent
* renderable. The function should make sure to clean up the internal state,
* like reseting $this->parentRenderable or deregistering the renderable
* at the form.
*
* @internal
*/
public function onRemoveFromParentRenderable();
/**
* Register this element at the parent form, if there is a connection to the parent form.
*
* @internal
*/
public function registerInFormIfPossible();
/**
* Get the template name of the renderable
*
* @return string
*/
public function getTemplateName(): string;
/**
* Returns whether this renderable is enabled
*
* @return bool
*/
public function isEnabled(): bool;
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
/**
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
class RenderableVariant implements RenderableVariantInterface
{
/**
* @var string
*/
protected $identifier;
/**
* @var array
*/
protected $options;
/**
* @var VariableRenderableInterface
*/
protected $renderable;
/**
* @var string
*/
protected $condition = '';
/**
* @var bool
*/
protected $applied = false;
/**
* @param string $identifier
* @param array $options
* @param VariableRenderableInterface $renderable
* @throws IdentifierNotValidException
*/
public function __construct(
string $identifier,
array $options,
VariableRenderableInterface $renderable
) {
if ('' === $identifier) {
throw new IdentifierNotValidException('The given variant identifier was empty.', 1519998923);
}
$this->identifier = $identifier;
$this->renderable = $renderable;
if (isset($options['condition']) && is_string($options['condition'])) {
$this->condition = $options['condition'];
}
unset($options['condition'], $options['identifier'], $options['variants']);
$this->options = $options;
}
/**
* Apply the specified variant to this form element
* regardless of their conditions
*/
public function apply(): void
{
$this->renderable->setOptions($this->options, true);
$this->applied = true;
}
/**
* @param Resolver $conditionResolver
* @return bool
*/
public function conditionMatches(Resolver $conditionResolver): bool
{
if (empty($this->condition)) {
return false;
}
return $conditionResolver->evaluate($this->condition);
}
/**
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @return bool
*/
public function isApplied(): bool
{
return $this->applied;
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
/**
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
interface RenderableVariantInterface
{
/**
* @return string
*/
public function getIdentifier(): string;
/**
* Apply the specified variant to this form element
* regardless of their conditions
*/
public function apply(): void;
/**
* @return bool
*/
public function isApplied(): bool;
/**
* @param Resolver $conditionResolver
* @return bool
*/
public function conditionMatches(Resolver $conditionResolver): bool;
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Base interface which all parts of a form must adhere to.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
interface RootRenderableInterface
{
/**
* Abstract "type" of this Renderable. Is used during the rendering process
* to determine the template file or the View PHP class being used to render
* the particular element.
*
* @return string
*/
public function getType(): string;
/**
* The identifier of this renderable
*
* @return string
*/
public function getIdentifier(): string;
/**
* Get the label which shall be displayed next to the form element
*
* @return string
*/
public function getLabel(): string;
/**
* Get the renderer class name to be used to display this form;
* must implement RendererInterface
*
* @return string the renderer class name
*/
public function getRendererClassName(): string;
/**
* Get all rendering options
*
* @return array associative array of rendering options
*/
public function getRenderingOptions(): array;
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Model\Renderable;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
*/
interface VariableRenderableInterface
{
/**
* Set multiple properties of this object at once.
* Every property which has a corresponding set* method can be set using
* the passed $options array.
*
* @param array $options
* @param bool $reset
* @internal
*/
public function setOptions(array $options, bool $reset = false);
/**
* Get all rendering variants
*
* @return RenderableVariantInterface[]
*/
public function getVariants(): array;
/**
* Adds the specified variant to this form element
*
* @param RenderableVariantInterface $variant
*/
public function addVariant(RenderableVariantInterface $variant);
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Renderer;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
/**
* Abstract renderer which can be used as base class for custom renderers.
*
* Scope: frontend
* **This class is meant to be sub classed by developers**.
*/
abstract class AbstractElementRenderer implements RendererInterface
{
/**
* The assigned controller context which might be needed by the renderer.
*
* @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
*/
protected $controllerContext;
/**
* @var \TYPO3\CMS\Form\Domain\Runtime\FormRuntime
*/
protected $formRuntime;
/**
* Set the controller context which should be used
*
* @param \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext
*/
public function setControllerContext(\TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext)
{
$this->controllerContext = $controllerContext;
}
/**
* @param FormRuntime $formRuntime
*/
public function setFormRuntime(FormRuntime $formRuntime)
{
$this->formRuntime = $formRuntime;
}
/**
* @return FormRuntime
*/
public function getFormRuntime(): FormRuntime
{
return $this->formRuntime;
}
}
@@ -0,0 +1,189 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Renderer;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Fluid\View\TemplateView;
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
/**
* A fluid RendererInterface implementation which used to render a *FormDefinition*.
*
* This renderer is called from {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime::render()}.
*
* Options
* =======
*
* The FluidFormRenderer uses some rendering options which are of particular
* importance, as they determine how the form field is resolved to a path
* in the file system.
*
* All rendering options are retrieved from the FormDefinition,
* using the {@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::getRenderingOptions()}
* method.
*
* templateRootPaths
* -----------------
*
* Used to define several paths for templates, which will be tried in reversed
* order (the paths are searched from bottom to top). The first folder where
* the desired layout is found, is used. If the array keys are numeric,
* they are first sorted and then tried in reversed order.
* Within this paths, fluid will search for a file which is named like the
* renderable *type*.
* For example:
* templateRootPaths.10 = EXT:form/Resources/Private/Frontend/Templates/
* $renderable->getType() = Form
* Expected template file: EXT:form/Resources/Private/Frontend/Templates/Form.html
* There is a setting available to set a custom template name. Please read
* the section 'templateName'.
*
* Only the root renderable (FormDefinition) has to be a template file.
* All child renderables are partials. By default, the root renderable
* is called 'Form'.
*
* layoutRootPaths
* ---------------
*
* Used to define several paths for layouts, which will be tried in reversed
* order (the paths are searched from bottom to top). The first folder where
* the desired layout is found, is used. If the array keys are numeric,
* they are first sorted and then tried in reversed order.
*
* partialRootPaths
* ----------------
*
* Used to define several paths for partials, which will be tried in reversed
* order. The first folder where the desired partial is found, is used.
* The keys of the array define the order.
*
* Within this paths, fluid will search for a file which is named like the
* renderable *type*.
* For example:
* templateRootPaths.10 = EXT:form/Resources/Private/Frontend/Partials/
* $renderable->getType() = Text
* Expected template file: EXT:form/Resources/Private/Frontend/Partials/Text.html
* There is a setting available to set a custom partial name. Please read
* the section 'templateName'.
*
* templateName
* -----------
* By default, the renderable type will be taken as the name for the
* template / partial.
* For example:
* partialRootPaths.10 = EXT:form/Resources/Private/Frontend/Partials/
* $renderable->getType() = Text
* Expected partial file: EXT:form/Resources/Private/Frontend/Partials/Text.html
*
* Set 'templateName' to define a custom name which should be used instead.
* For example:
* templateName = Foo
* $renderable->getType() = Text
* Expected partial file: EXT:form/Resources/Private/Frontend/Partials/Foo.html
*
* Rendering Child Renderables
* ===========================
*
* If a renderable wants to render child renderables, inside its template / partial,
* it can do that using the <code><formvh:renderRenderable></code> ViewHelper.
*
* A template example from Page shall demonstrate this:
*
* <pre>
* <formvh:renderRenderable renderable="{page}">
* <f:for each="{page.elements}" as="element">
* <formvh:renderRenderable renderable="{element}">
* <f:render partial="{element.templateName}" arguments="{element: element}" />
* </formvh:renderRenderable>
* </f:for>
* </formvh:renderRenderable>
* </pre>
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
class FluidFormRenderer extends AbstractElementRenderer
{
/**
* Renders the FormDefinition.
*
* This method is expected to call the 'beforeRendering' hook
* on each renderable.
* This method call the 'beforeRendering' hook initially.
* Each other hooks will be called from the
* renderRenderable viewHelper.
* {@link \TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper::renderStatic()}
*
* @return string the rendered $formRuntime
* @internal
*/
public function render(): string
{
$formElementType = $this->formRuntime->getType();
$renderingOptions = $this->formRuntime->getRenderingOptions();
$view = GeneralUtility::makeInstance(ObjectManager::class)
->get(TemplateView::class);
$view->setControllerContext($this->controllerContext);
if (!isset($renderingOptions['templateRootPaths'])) {
throw new RenderingException(
sprintf('The option templateRootPaths must be set for renderable "%s"', $formElementType),
1480293084
);
}
if (!isset($renderingOptions['layoutRootPaths'])) {
throw new RenderingException(
sprintf('The option layoutRootPaths must be set for renderable "%s"', $formElementType),
1480293085
);
}
if (!isset($renderingOptions['partialRootPaths'])) {
throw new RenderingException(
sprintf('The option partialRootPaths must be set for renderable "%s"', $formElementType),
1480293086
);
}
$view->assign('form', $this->formRuntime);
$view->getRenderingContext()
->getViewHelperVariableContainer()
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $this->formRuntime);
// Configure the fluid TemplatePaths with the rendering options
// from the renderable
$view->getTemplatePaths()->fillFromConfigurationArray($renderingOptions);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'beforeRendering')) {
$hookObj->beforeRendering(
$this->formRuntime,
$this->formRuntime->getFormDefinition()
);
}
}
return $view->render($this->formRuntime->getTemplateName());
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Renderer;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
/**
* Base interface for Renderers. A Renderer is used to render a form.
*
* Scope: frontend
* **This interface is meant to be implemented by developers, although often you
* will subclass AbstractElementRenderer** ({@link AbstractElementRenderer}).
*/
interface RendererInterface
{
/**
* Set the controller context which should be used
*
* @param ControllerContext $controllerContext
*/
public function setControllerContext(ControllerContext $controllerContext);
/**
* Note: This method is expected to call the 'beforeRendering' hook
* on each $renderable
*
* @return string the rendered $formRuntime
*/
public function render(): string;
/**
* @param FormRuntime $formRuntime
*/
public function setFormRuntime(FormRuntime $formRuntime);
/**
* @return FormRuntime
*/
public function getFormRuntime(): FormRuntime;
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Runtime\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Domain\Exception;
/**
* This Exception is thrown in the FormRuntime if the PropertyMapper throws
* a \TYPO3\CMS\Extbase\Property\Exception. It adds some more Information to
* better understand why the PropertyMapper failed to map the properties
*/
class PropertyMappingException extends Exception
{
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 12716 files, more files were not shown because too many files have changed in this diff. Show more