TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/vendor/
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use TYPO3\CMS\Form\Service\CleanupFormUploadsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI command to clean up old file upload folders created by the TYPO3 Form Framework.
|
||||||
|
*
|
||||||
|
* When users upload files via ext:form, the files are stored in `form_<hash>` sub-folders.
|
||||||
|
* Over time these folders accumulate — both from completed and incomplete form submissions.
|
||||||
|
* Since files are not moved upon submission, there is no way to distinguish between
|
||||||
|
* the two. This command removes form upload folders older than a configurable retention period.
|
||||||
|
*
|
||||||
|
* Usage examples:
|
||||||
|
* # Dry-run: list form upload folders older than 2 weeks (default)
|
||||||
|
* bin/typo3 form:cleanup:uploads 1:/user_upload/ --dry-run
|
||||||
|
*
|
||||||
|
* # Delete folders older than 48 hours in specific upload folders
|
||||||
|
* bin/typo3 form:cleanup:uploads 1:/user_upload/ 2:/custom_uploads/ --retention-period=48
|
||||||
|
*
|
||||||
|
* # Force deletion without confirmation (e.g. for scheduler)
|
||||||
|
* bin/typo3 form:cleanup:uploads 1:/user_upload/ --force
|
||||||
|
*/
|
||||||
|
#[AsCommand('form:cleanup:uploads', 'Remove old form file upload folders based on retention period.')]
|
||||||
|
class CleanupFormUploadsCommand extends Command
|
||||||
|
{
|
||||||
|
private const int DEFAULT_RETENTION_PERIOD_HOURS = 336;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly CleanupFormUploadsService $cleanupService,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setHelp(
|
||||||
|
'Removes old form upload folders (form_<hash>) that were created by file uploads '
|
||||||
|
. 'in ext:form.' . LF . LF
|
||||||
|
. 'Since uploaded files are not moved when a form is submitted, the command cannot ' . LF
|
||||||
|
. 'distinguish between folders from completed and abandoned submissions. It uses ' . LF
|
||||||
|
. 'the folder modification time and a configurable retention period to decide ' . LF
|
||||||
|
. 'which folders to remove.' . LF . LF
|
||||||
|
. 'You must specify at least one upload folder to scan. Each form element can configure ' . LF
|
||||||
|
. 'a different saveToFileMount; pass all relevant folders as arguments.' . LF . LF
|
||||||
|
. 'Use --verbose for detailed output about each folder found.'
|
||||||
|
)
|
||||||
|
->addArgument(
|
||||||
|
'upload-folder',
|
||||||
|
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
|
||||||
|
'Combined folder identifier(s) to scan (e.g. "1:/user_upload/").',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'retention-period',
|
||||||
|
'r',
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Minimum age in hours before a form upload folder is considered for removal.',
|
||||||
|
(string)self::DEFAULT_RETENTION_PERIOD_HOURS,
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'dry-run',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Only list expired folders without deleting them.',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'force',
|
||||||
|
'f',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Skip the confirmation question. Automatically set when using --no-interaction.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$retentionHours = (int)$input->getOption('retention-period');
|
||||||
|
if ($retentionHours < 1) {
|
||||||
|
$io->error('The retention period must be at least 1 hour.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maximumAgeSeconds = $retentionHours * 3600;
|
||||||
|
/** @var list<string> $uploadFolders */
|
||||||
|
$uploadFolders = $input->getArgument('upload-folder');
|
||||||
|
$isDryRun = (bool)$input->getOption('dry-run');
|
||||||
|
|
||||||
|
$io->section(sprintf(
|
||||||
|
'Scanning %s for form upload folders older than %d hour(s)',
|
||||||
|
'folders: ' . implode(', ', $uploadFolders),
|
||||||
|
$retentionHours,
|
||||||
|
));
|
||||||
|
|
||||||
|
$expiredFolders = $this->cleanupService->getExpiredFolders($maximumAgeSeconds, $uploadFolders);
|
||||||
|
|
||||||
|
if ($expiredFolders === []) {
|
||||||
|
$io->success('No expired form upload folders found. Nothing to do.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
foreach ($expiredFolders as $folder) {
|
||||||
|
$age = time() - $folder->getModificationTime();
|
||||||
|
$ageHours = round($age / 3600, 1);
|
||||||
|
$fileCount = $folder->getFileCount();
|
||||||
|
$io->writeln(sprintf(
|
||||||
|
' [FOLDER] %s (age: %s hours, files: %d)',
|
||||||
|
$folder->getCombinedIdentifier(),
|
||||||
|
$ageHours,
|
||||||
|
$fileCount,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalFiles = 0;
|
||||||
|
foreach ($expiredFolders as $folder) {
|
||||||
|
$totalFiles += $folder->getFileCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->writeln(sprintf(
|
||||||
|
'Found <options=bold>%d folder(s)</> containing <options=bold>%d file(s)</>.',
|
||||||
|
count($expiredFolders),
|
||||||
|
$totalFiles,
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$io->note('Dry-run mode: no folders were deleted.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not ask for confirmation when running the command in EXT:scheduler
|
||||||
|
if (!$input->isInteractive()) {
|
||||||
|
$input->setOption('force', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('force')) {
|
||||||
|
/** @var QuestionHelper $questionHelper */
|
||||||
|
$questionHelper = $this->getHelper('question');
|
||||||
|
$question = new ConfirmationQuestion(
|
||||||
|
sprintf(
|
||||||
|
'Are you sure you want to delete %d folder(s) with %d file(s)? [default: no] ',
|
||||||
|
count($expiredFolders),
|
||||||
|
$totalFiles,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!$questionHelper->ask($input, $output, $question)) {
|
||||||
|
$io->note('Aborted by user.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->cleanupService->deleteFolders($expiredFolders);
|
||||||
|
|
||||||
|
if ($result['deleted'] > 0) {
|
||||||
|
$io->success(sprintf('Successfully deleted %d form upload folder(s).', $result['deleted']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result['failed'] > 0) {
|
||||||
|
$io->warning(sprintf('Failed to delete %d folder(s).', $result['failed']));
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
foreach ($result['errors'] as $error) {
|
||||||
|
$io->writeln(sprintf(' [ERROR] %s: %s', $error['folder'], $error['message']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result['failed'] > 0 ? Command::FAILURE : Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||||
|
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
|
||||||
|
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||||
|
use TYPO3\CMS\Core\Http\ServerRequest;
|
||||||
|
use TYPO3\CMS\Form\Service\FormTransferService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI command to transfer form definitions between storage backends.
|
||||||
|
*
|
||||||
|
* Usage examples:
|
||||||
|
* # Transfer all forms from extension storage to database
|
||||||
|
* bin/typo3 form:definition:transfer --source=extension --target=database
|
||||||
|
*
|
||||||
|
* # Transfer a specific form
|
||||||
|
* bin/typo3 form:definition:transfer --source=extension --target=database --form-identifier=contact
|
||||||
|
*
|
||||||
|
* # Move forms (transfer + delete source)
|
||||||
|
* bin/typo3 form:definition:transfer --source=filemount --target=database --move
|
||||||
|
*
|
||||||
|
* # Dry-run: preview what would be transferred
|
||||||
|
* bin/typo3 form:definition:transfer --source=extension --target=database --dry-run
|
||||||
|
*
|
||||||
|
* # Transfer to a specific target location (PID for database)
|
||||||
|
* bin/typo3 form:definition:transfer --source=extension --target=database --target-location=42
|
||||||
|
*/
|
||||||
|
#[AsCommand('form:definition:transfer', 'Transfer form definitions between storage backends')]
|
||||||
|
class TransferFormDefinitionCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FormTransferService $formTransferService,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setHelp(
|
||||||
|
'Transfers form definitions from one storage backend to another.' . LF . LF
|
||||||
|
. 'Available storage types depend on the installed adapters. Core provides:' . LF
|
||||||
|
. ' - database: Database storage (default target)' . LF
|
||||||
|
. ' - extension: Extension paths (EXT:...)' . LF
|
||||||
|
. ' - filemount: File mount storage (deprecated since v14.2)' . LF . LF
|
||||||
|
. 'Target location (--target-location / -l) per storage type:' . LF
|
||||||
|
. ' - database: Always "0" (fixed; forms are stored at the root level).' . LF
|
||||||
|
. ' - extension: An EXT: path configured in "persistenceManager.allowedExtensionPaths",' . LF
|
||||||
|
. ' with "persistenceManager.allowSaveToExtensionPaths: true" set.' . LF
|
||||||
|
. ' e.g. --target-location="EXT:my_extension/Resources/Private/Forms/"' . LF . LF
|
||||||
|
. 'Use --dry-run to preview which forms would be transferred.' . LF
|
||||||
|
. 'Use --move to delete the source form after successful transfer.' . LF . LF
|
||||||
|
. 'After a successful transfer, content element references in "tt_content" are automatically' . LF
|
||||||
|
. 'updated to point to the new storage location. No other tables are updated.'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'source',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Source storage type identifier (e.g., "extension", "filemount", "database").',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'target',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Target storage type identifier (e.g., "database", "extension").',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'target-location',
|
||||||
|
'l',
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Target storage location. For "database": always "0". For "extension": EXT: path from allowedExtensionPaths.',
|
||||||
|
'0',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'form-identifier',
|
||||||
|
'f',
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Transfer only the form with this identifier. If omitted, all forms from the source are transferred.',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'move',
|
||||||
|
'm',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Delete the source form after successful transfer (move operation).',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'dry-run',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Only list forms that would be transferred without making changes.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
Bootstrap::initializeBackendAuthentication();
|
||||||
|
|
||||||
|
// @todo: ConfigurationManager triggered by PersistenceConfigurationService needs a Request
|
||||||
|
$request = (new ServerRequest('https://localhost/', 'GET'));
|
||||||
|
$request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
|
||||||
|
->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
|
||||||
|
$GLOBALS['TYPO3_REQUEST'] = $request;
|
||||||
|
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$sourceType = $input->getOption('source');
|
||||||
|
$targetType = $input->getOption('target');
|
||||||
|
$targetLocation = $input->getOption('target-location');
|
||||||
|
$formIdentifier = $input->getOption('form-identifier');
|
||||||
|
$isMove = (bool)$input->getOption('move');
|
||||||
|
$isDryRun = (bool)$input->getOption('dry-run');
|
||||||
|
|
||||||
|
if ($sourceType === null || $targetType === null) {
|
||||||
|
$io->error('Both --source and --target options are required.');
|
||||||
|
$io->note(sprintf(
|
||||||
|
'Available storage types: %s',
|
||||||
|
implode(', ', $this->formTransferService->getAvailableStorageTypes()),
|
||||||
|
));
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->formTransferService->hasStorageType($sourceType)) {
|
||||||
|
$io->error(sprintf(
|
||||||
|
'Unknown source storage type "%s". Available types: %s',
|
||||||
|
$sourceType,
|
||||||
|
implode(', ', $this->formTransferService->getAvailableStorageTypes()),
|
||||||
|
));
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->formTransferService->hasStorageType($targetType)) {
|
||||||
|
$io->error(sprintf(
|
||||||
|
'Unknown target storage type "%s". Available types: %s',
|
||||||
|
$targetType,
|
||||||
|
implode(', ', $this->formTransferService->getAvailableStorageTypes()),
|
||||||
|
));
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceType === $targetType && $formIdentifier === null) {
|
||||||
|
$io->error('Source and target storage types are identical. Use --form-identifier to transfer a specific form, or choose different storage types.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetAdapter = $this->formTransferService->getAdapter($targetType);
|
||||||
|
if (!$targetAdapter->isAllowedStorageLocation($targetLocation)) {
|
||||||
|
$hint = match ($targetType) {
|
||||||
|
'database' => 'For database storage the only valid location is "0" (default). The option can be omitted.',
|
||||||
|
'extension' => 'For extension storage, provide an EXT: path that is registered in "persistenceManager.allowedExtensionPaths"' . LF
|
||||||
|
. 'and ensure "persistenceManager.allowSaveToExtensionPaths: true" is set in your form YAML setup.' . LF
|
||||||
|
. 'Example: --target-location="EXT:my_extension/Resources/Private/Forms/"',
|
||||||
|
default => 'Check the storage adapter documentation for valid location formats.',
|
||||||
|
};
|
||||||
|
$io->error(sprintf(
|
||||||
|
'The target location "%s" is not valid for the "%s" storage adapter.' . LF . '%s',
|
||||||
|
$targetLocation,
|
||||||
|
$targetType,
|
||||||
|
$hint,
|
||||||
|
));
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceForms = $this->formTransferService->listSourceForms($sourceType, $formIdentifier);
|
||||||
|
|
||||||
|
if ($sourceForms === []) {
|
||||||
|
$message = $formIdentifier !== null
|
||||||
|
? sprintf('No form with identifier "%s" found in "%s" storage.', $formIdentifier, $sourceType)
|
||||||
|
: sprintf('No forms found in "%s" storage.', $sourceType);
|
||||||
|
$io->warning($message);
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operation = $isMove ? 'move' : 'transfer';
|
||||||
|
$io->section(sprintf(
|
||||||
|
'Found %d form(s) to %s from "%s" to "%s"',
|
||||||
|
count($sourceForms),
|
||||||
|
$operation,
|
||||||
|
$sourceType,
|
||||||
|
$targetType,
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$rows = [];
|
||||||
|
foreach ($sourceForms as $form) {
|
||||||
|
$rows[] = [
|
||||||
|
$form->identifier,
|
||||||
|
$form->name,
|
||||||
|
$form->persistenceIdentifier ?? '-',
|
||||||
|
'<comment>would ' . $operation . '</comment>',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$io->table(['Identifier', 'Name', 'Source', 'Status'], $rows);
|
||||||
|
$io->note('Dry-run mode: no forms were transferred.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transferred = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$results = [];
|
||||||
|
$migrationMap = [];
|
||||||
|
|
||||||
|
foreach ($sourceForms as $form) {
|
||||||
|
try {
|
||||||
|
$result = $this->formTransferService->transferForm(
|
||||||
|
$form,
|
||||||
|
$sourceType,
|
||||||
|
$targetType,
|
||||||
|
$targetLocation,
|
||||||
|
$isMove,
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = '<info>success</info>';
|
||||||
|
if ($isMove && $result->sourceDeleted) {
|
||||||
|
$status = '<info>moved</info>';
|
||||||
|
} elseif ($isMove && $result->deletionError !== null) {
|
||||||
|
$status = '<info>transferred</info>, <comment>source deletion failed: ' . $result->deletionError . '</comment>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
$result->formIdentifier,
|
||||||
|
$result->formName,
|
||||||
|
$result->sourceIdentifier,
|
||||||
|
$result->targetIdentifier,
|
||||||
|
$status,
|
||||||
|
];
|
||||||
|
$migrationMap[$result->sourceIdentifier] = $result->targetIdentifier;
|
||||||
|
$transferred++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$results[] = [
|
||||||
|
$form->identifier,
|
||||||
|
$form->name,
|
||||||
|
$form->persistenceIdentifier ?? '-',
|
||||||
|
'-',
|
||||||
|
'<error>' . $e->getMessage() . '</error>',
|
||||||
|
];
|
||||||
|
$failed++;
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
$io->error(sprintf('Failed to transfer "%s": %s', $form->identifier, $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->table(['Identifier', 'Name', 'Source', 'Target', 'Status'], $results);
|
||||||
|
|
||||||
|
if ($migrationMap !== []) {
|
||||||
|
$referencesUpdated = $this->formTransferService->updateContentElementReferences($migrationMap);
|
||||||
|
if ($referencesUpdated > 0) {
|
||||||
|
$io->note(sprintf('Updated %d content element reference(s).', $referencesUpdated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($transferred > 0) {
|
||||||
|
$verb = $isMove ? 'moved' : 'transferred';
|
||||||
|
$io->success(sprintf('Successfully %s %d form(s) from "%s" to "%s".', $verb, $transferred, $sourceType, $targetType));
|
||||||
|
}
|
||||||
|
if ($failed > 0) {
|
||||||
|
$io->warning(sprintf('Failed to transfer %d form(s).', $failed));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $failed > 0 ? Command::FAILURE : Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\ConfigurationModuleProvider;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\ProviderInterface;
|
||||||
|
|
||||||
|
class FormYamlProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
protected string $identifier;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||||
|
protected readonly ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(array $attributes): self
|
||||||
|
{
|
||||||
|
$this->identifier = $attributes['identifier'];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return $this->getLanguageService()->sL(
|
||||||
|
'LLL:EXT:form/Resources/Private/Language/locallang.xlf:form.configuration.module.provider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfiguration(): array
|
||||||
|
{
|
||||||
|
// Another hidden dependency to $GLOBALS['TYPO3_REQUEST'] made explicit here.
|
||||||
|
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||||
|
$extbaseConfigurationManager = $this->extbaseConfigurationManager;
|
||||||
|
$extbaseConfigurationManager->setRequest($request);
|
||||||
|
$typoScriptSettings = $extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||||
|
$configuration = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, false);
|
||||||
|
ArrayUtility::naturalKeySortRecursive($configuration);
|
||||||
|
return $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLanguageService(): LanguageService
|
||||||
|
{
|
||||||
|
return $GLOBALS['LANG'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
|
||||||
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||||
|
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||||
|
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||||
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||||
|
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||||
|
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
|
||||||
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||||
|
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||||
|
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||||
|
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||||
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
|
||||||
|
use TYPO3\CMS\Form\Domain\DTO\PersistenceManagerConfiguration;
|
||||||
|
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
|
||||||
|
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
|
||||||
|
use TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent;
|
||||||
|
use TYPO3\CMS\Form\Exception;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Service\DatabaseService;
|
||||||
|
use TYPO3\CMS\Form\Service\FormEditorEnrichmentService;
|
||||||
|
use TYPO3\CMS\Form\Service\TranslationService;
|
||||||
|
use TYPO3\CMS\Form\Type\FormDefinitionArray;
|
||||||
|
use TYPO3\CMS\Form\Utility\DateRangeValidatorPatterns;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form editor controller
|
||||||
|
*
|
||||||
|
* Scope: backend
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FormEditorController extends ActionController
|
||||||
|
{
|
||||||
|
use AllowedMethodsTrait;
|
||||||
|
|
||||||
|
protected const JS_MODULE_NAMES = ['app', 'mediator', 'viewModel'];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||||
|
protected readonly PageRenderer $pageRenderer,
|
||||||
|
protected readonly IconFactory $iconFactory,
|
||||||
|
protected readonly FormDefinitionConversionService $formDefinitionConversionService,
|
||||||
|
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||||
|
protected readonly ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||||
|
protected readonly TranslationService $translationService,
|
||||||
|
protected readonly ConfigurationService $configurationService,
|
||||||
|
protected readonly UriBuilder $coreUriBuilder,
|
||||||
|
protected readonly ArrayFormFactory $arrayFormFactory,
|
||||||
|
protected readonly ViewFactoryInterface $viewFactory,
|
||||||
|
protected readonly DatabaseService $databaseService,
|
||||||
|
protected readonly CacheManager $cacheManager,
|
||||||
|
protected readonly ComponentFactory $componentFactory,
|
||||||
|
protected readonly FormEditorEnrichmentService $formEditorEnrichmentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the form editor.
|
||||||
|
*
|
||||||
|
* @throws PersistenceManagerException
|
||||||
|
*/
|
||||||
|
protected function indexAction(string $formPersistenceIdentifier = '', ?string $prototypeName = null, string $returnUrl = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($formPersistenceIdentifier === '') {
|
||||||
|
return new RedirectResponse((string)$this->coreUriBuilder->buildUriFromRoute('form_manager'));
|
||||||
|
}
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Read "%s" is not allowed', $formPersistenceIdentifier), 1614500662);
|
||||||
|
}
|
||||||
|
if (PathUtility::isExtensionPath($formPersistenceIdentifier)
|
||||||
|
&& !PersistenceManagerConfiguration::fromArray($formSettings['persistenceManager'] ?? [])->allowSaveToExtensionPaths
|
||||||
|
) {
|
||||||
|
throw new PersistenceManagerException('Edit an extension formDefinition is not allowed.', 1478265661);
|
||||||
|
}
|
||||||
|
$formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
|
||||||
|
if ($prototypeName === null) {
|
||||||
|
$prototypeName = $formDefinition['prototypeName'] ?? 'standard';
|
||||||
|
} else {
|
||||||
|
// Loading a form definition with another prototype is currently not implemented but is planned in the future.
|
||||||
|
// This safety check is a preventive measure.
|
||||||
|
$selectablePrototypeNames = $this->configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
|
||||||
|
if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
|
||||||
|
throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$formDefinition['prototypeName'] = $prototypeName;
|
||||||
|
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($prototypeName);
|
||||||
|
$formDefinition = $this->transformFormDefinitionForFormEditor($prototypeConfiguration, $formDefinition, $formPersistenceIdentifier);
|
||||||
|
$formEditorDefinitions = $this->getFormEditorDefinitions($prototypeConfiguration);
|
||||||
|
$additionalViewModelJavaScriptModules = array_map(
|
||||||
|
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||||
|
$prototypeConfiguration['formEditor']['dynamicJavaScriptModules']['additionalViewModelModules'] ?? []
|
||||||
|
);
|
||||||
|
array_map($this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $additionalViewModelJavaScriptModules);
|
||||||
|
$formEditorAppInitialData = [
|
||||||
|
'formEditorDefinitions' => $formEditorDefinitions,
|
||||||
|
'formDefinition' => $formDefinition,
|
||||||
|
'formPersistenceIdentifier' => $formPersistenceIdentifier,
|
||||||
|
'prototypeName' => $prototypeName,
|
||||||
|
'endpoints' => [
|
||||||
|
'formPageRenderer' => $this->uriBuilder->uriFor('renderFormPage'),
|
||||||
|
'saveForm' => $this->uriBuilder->uriFor('saveForm'),
|
||||||
|
],
|
||||||
|
'additionalViewModelModules' => $additionalViewModelJavaScriptModules,
|
||||||
|
'maximumUndoSteps' => $prototypeConfiguration['formEditor']['maximumUndoSteps'],
|
||||||
|
];
|
||||||
|
$moduleTemplate = $this->initializeModuleTemplate($this->request, $returnUrl);
|
||||||
|
$moduleTemplate->assign('formEditorTemplates', $this->renderFormEditorTemplates($prototypeConfiguration, $formEditorDefinitions));
|
||||||
|
$moduleTemplate->getDocHeaderComponent()->addBreadcrumbSuffixNode(new BreadcrumbNode(
|
||||||
|
identifier: $formPersistenceIdentifier,
|
||||||
|
label: $formDefinition['label'],
|
||||||
|
icon: 'content-form',
|
||||||
|
));
|
||||||
|
$addInlineSettings = [
|
||||||
|
'FormEditor' => [
|
||||||
|
'typo3WinBrowserUrl' => (string)$this->coreUriBuilder->buildUriFromRoute('wizard_element_browser'),
|
||||||
|
'dateEditor' => [
|
||||||
|
'absolutePattern' => DateRangeValidatorPatterns::RFC3339_FULL_DATE,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$addInlineSettings = array_replace_recursive(
|
||||||
|
$addInlineSettings,
|
||||||
|
$prototypeConfiguration['formEditor']['addInlineSettings']
|
||||||
|
);
|
||||||
|
if (json_encode($formEditorAppInitialData) === false) {
|
||||||
|
throw new Exception('The form editor app data could not be encoded', 1628677079);
|
||||||
|
}
|
||||||
|
$javaScriptModules = array_map(
|
||||||
|
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||||
|
array_filter(
|
||||||
|
$prototypeConfiguration['formEditor']['dynamicJavaScriptModules'] ?? [],
|
||||||
|
fn(string $name) => in_array($name, self::JS_MODULE_NAMES, true),
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$pageRenderer = $this->pageRenderer;
|
||||||
|
$pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||||
|
JavaScriptModuleInstruction::create('@typo3/form/backend/helper.js', 'Helper')
|
||||||
|
->invoke('dispatchFormEditor', $javaScriptModules, $formEditorAppInitialData)
|
||||||
|
);
|
||||||
|
array_map($pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $javaScriptModules);
|
||||||
|
$pageRenderer->addInlineSettingArray('', $addInlineSettings);
|
||||||
|
$stylesheets = $prototypeConfiguration['formEditor']['stylesheets'];
|
||||||
|
foreach ($stylesheets as $stylesheet) {
|
||||||
|
$pageRenderer->addCssFile($stylesheet);
|
||||||
|
}
|
||||||
|
$moduleTemplate->setModuleClass($this->request->getPluginName() . '_' . $this->request->getControllerName());
|
||||||
|
$moduleTemplate->setFlashMessageQueue($this->getFlashMessageQueue());
|
||||||
|
$moduleTemplate->setTitle(
|
||||||
|
$this->getLanguageService()->translate('title', 'form.module'),
|
||||||
|
$formDefinition['label']
|
||||||
|
);
|
||||||
|
return $moduleTemplate->renderResponse('Backend/FormEditor/Index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the save action.
|
||||||
|
* This action uses the Fluid JsonView::class as view.
|
||||||
|
*/
|
||||||
|
protected function initializeSaveFormAction(): void
|
||||||
|
{
|
||||||
|
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||||
|
$this->defaultViewObjectName = JsonView::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a formDefinition which was build by the form editor.
|
||||||
|
*/
|
||||||
|
protected function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition): ResponseInterface
|
||||||
|
{
|
||||||
|
$formDefinition = $formDefinition->getArrayCopy();
|
||||||
|
$event = $this->eventDispatcher->dispatch(
|
||||||
|
new BeforeFormIsSavedEvent($formPersistenceIdentifier, $formDefinition, $this->request),
|
||||||
|
);
|
||||||
|
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||||
|
$formDefinition = $event->form;
|
||||||
|
$response = [
|
||||||
|
'status' => 'success',
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Save "%s" is not allowed', $formPersistenceIdentifier), 1614500663);
|
||||||
|
}
|
||||||
|
$this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition, []);
|
||||||
|
$this->flushPageCache($formPersistenceIdentifier);
|
||||||
|
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
|
||||||
|
$formDefinition = $this->transformFormDefinitionForFormEditor($prototypeConfiguration, $formDefinition, $formPersistenceIdentifier);
|
||||||
|
$response['formDefinition'] = $formDefinition;
|
||||||
|
} catch (PersistenceManagerException $e) {
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// saveFormAction uses the extbase JsonView::class.
|
||||||
|
// That's why we have to set the view variables in this way.
|
||||||
|
/** @var JsonView $view */
|
||||||
|
$view = $this->view;
|
||||||
|
$view->assign('response', $response);
|
||||||
|
$view->setVariablesToRender([
|
||||||
|
'response',
|
||||||
|
]);
|
||||||
|
return $this->jsonResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a page from the formDefinition which was build by the form editor.
|
||||||
|
* Use the frontend rendering and set the form framework to preview mode.
|
||||||
|
*/
|
||||||
|
protected function renderFormPageAction(
|
||||||
|
FormDefinitionArray $formDefinition,
|
||||||
|
int $pageIndex,
|
||||||
|
?string $prototypeName = null,
|
||||||
|
?string $formPersistenceIdentifier = null
|
||||||
|
): ResponseInterface {
|
||||||
|
$prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
|
||||||
|
$formDefinition = $formDefinition->getArrayCopy();
|
||||||
|
$formDefinition['renderingOptions']['previewMode'] = true;
|
||||||
|
$formDefinition = $this->arrayFormFactory->build($formDefinition, $prototypeName, $this->request);
|
||||||
|
|
||||||
|
if ($formPersistenceIdentifier !== null) {
|
||||||
|
$formDefinition->setRenderingOption('formPersistenceIdentifier', $formPersistenceIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $formDefinition->bind($this->request);
|
||||||
|
$form->setCurrentSiteLanguage($this->buildFakeSiteLanguage(0, 0));
|
||||||
|
$form->overrideCurrentPage($pageIndex);
|
||||||
|
return $this->htmlResponse($form->render());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormSettings(): array
|
||||||
|
{
|
||||||
|
$typoScriptSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||||
|
$formSettings = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, false);
|
||||||
|
if (!isset($formSettings['formManager'])) {
|
||||||
|
// Config sub array formManager is crucial and should always exist. If it does
|
||||||
|
// not, this indicates an issue in config loading logic. Except in this case.
|
||||||
|
throw new \LogicException('Configuration could not be loaded', 1681549038);
|
||||||
|
}
|
||||||
|
return $formSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a SiteLanguage object to render the form preview with a
|
||||||
|
* specific language.
|
||||||
|
*/
|
||||||
|
protected function buildFakeSiteLanguage(int $pageId, int $languageId): SiteLanguage
|
||||||
|
{
|
||||||
|
$fakeSiteConfiguration = [
|
||||||
|
'languages' => [
|
||||||
|
[
|
||||||
|
'languageId' => $languageId,
|
||||||
|
'title' => 'Dummy',
|
||||||
|
'navigationTitle' => '',
|
||||||
|
'flag' => '',
|
||||||
|
'locale' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)->getLanguageById($languageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the formElements.*.formEditor section from the YAML settings.
|
||||||
|
* Sort all formElements into groups and add additional data.
|
||||||
|
*/
|
||||||
|
protected function getInsertRenderablesPanelConfiguration(array $prototypeConfiguration, array $formElementsDefinition, bool $isInsertPages = false): array
|
||||||
|
{
|
||||||
|
/** @var array<string, list<array<string, array{key: string, cssKey: string, label: string, description: string, sorting: int, iconIdentifier: string}>>> $formElementsByGroup */
|
||||||
|
$formElementsByGroup = [];
|
||||||
|
foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
|
||||||
|
if (!isset($formElementConfiguration['group']) || ($isInsertPages && $formElementConfiguration['group'] !== 'page') || (!$isInsertPages && $formElementConfiguration['group'] === 'page')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
|
||||||
|
$formElementsByGroup[$formElementConfiguration['group']] = [];
|
||||||
|
}
|
||||||
|
$formElementConfiguration = $this->translationService->translateValuesRecursive(
|
||||||
|
$formElementConfiguration,
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
$formElementsByGroup[$formElementConfiguration['group']][] = [
|
||||||
|
'identifier' => $formElementName,
|
||||||
|
'label' => $formElementConfiguration['label'],
|
||||||
|
'description' => $formElementConfiguration['description'] ?? '',
|
||||||
|
'requestType' => 'event',
|
||||||
|
'event' => 'typo3:form:insert-element-click',
|
||||||
|
'sorting' => $formElementConfiguration['groupSorting'],
|
||||||
|
'icon' => $formElementConfiguration['iconIdentifier'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$formGroups = [];
|
||||||
|
foreach ($prototypeConfiguration['formEditor']['formElementGroups'] ?? [] as $groupName => $groupConfiguration) {
|
||||||
|
if (!isset($formElementsByGroup[$groupName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
usort($formElementsByGroup[$groupName], static function ($a, $b) {
|
||||||
|
return $a['sorting'] - $b['sorting'];
|
||||||
|
});
|
||||||
|
$groupConfiguration = $this->translationService->translateValuesRecursive(
|
||||||
|
$groupConfiguration,
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
$formGroups[$groupName] = [
|
||||||
|
'identifier' => $groupName,
|
||||||
|
'items' => $formElementsByGroup[$groupName],
|
||||||
|
'label' => $groupConfiguration['label'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $formGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce the YAML settings by the 'formEditor' keyword.
|
||||||
|
*/
|
||||||
|
protected function getFormEditorDefinitions(array $prototypeConfiguration): array
|
||||||
|
{
|
||||||
|
$formEditorDefinitions = [];
|
||||||
|
foreach ([$prototypeConfiguration, $prototypeConfiguration['formEditor']] as $configuration) {
|
||||||
|
foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
|
||||||
|
if (!str_ends_with($firstLevelItemKey, 'Definition')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$reducedKey = substr($firstLevelItemKey, 0, -10);
|
||||||
|
foreach ($firstLevelItemValue as $formEditorDefinitionKey => $formEditorDefinitionValue) {
|
||||||
|
if (isset($formEditorDefinitionValue['formEditor'])) {
|
||||||
|
$formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
|
||||||
|
$formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
|
||||||
|
} else {
|
||||||
|
$formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
|
||||||
|
$formEditorDefinitions = $this->formEditorEnrichmentService->enrichFormEditorDefinitions($formEditorDefinitions);
|
||||||
|
return $this->translationService->translateValuesRecursive(
|
||||||
|
$formEditorDefinitions,
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize ModuleTemplate and register docheader icons.
|
||||||
|
*/
|
||||||
|
protected function initializeModuleTemplate(RequestInterface $request, string $returnUrl = ''): ModuleTemplate
|
||||||
|
{
|
||||||
|
$moduleTemplate = $this->moduleTemplateFactory->create($request);
|
||||||
|
$getVars = $request->getArguments();
|
||||||
|
if (isset($getVars['action']) && $getVars['action'] === 'index') {
|
||||||
|
$closeUrl = $returnUrl !== '' ? $returnUrl : (string)$this->coreUriBuilder->buildUriFromRoute('web_FormFormbuilder');
|
||||||
|
$closeButton = $this->componentFactory->createCloseButton($closeUrl)
|
||||||
|
->setDataAttributes(['identifier' => 'closeButton'])
|
||||||
|
->setClasses('formeditor-element-close-form-button hidden');
|
||||||
|
$moduleTemplate->addButtonToButtonBar($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||||
|
$saveButton = $this->componentFactory->createInputButton()
|
||||||
|
->setDataAttributes(['identifier' => 'saveButton'])
|
||||||
|
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
|
||||||
|
->setName('formeditor-save-form')
|
||||||
|
->setValue('save')
|
||||||
|
->setClasses('formeditor-element-save-form-button hidden')
|
||||||
|
->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||||
|
->setShowLabelText(true);
|
||||||
|
$moduleTemplate->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
|
||||||
|
$undoButton = $this->componentFactory->createInputButton()
|
||||||
|
->setDataAttributes(['identifier' => 'undoButton'])
|
||||||
|
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
|
||||||
|
->setName('formeditor-undo-form')
|
||||||
|
->setValue('undo')
|
||||||
|
->setClasses('formeditor-element-undo-form-button hidden disabled')
|
||||||
|
->setIcon($this->iconFactory->getIcon('actions-edit-undo', IconSize::SMALL));
|
||||||
|
$moduleTemplate->addButtonToButtonBar($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
|
||||||
|
$redoButton = $this->componentFactory->createInputButton()
|
||||||
|
->setDataAttributes(['identifier' => 'redoButton'])
|
||||||
|
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
|
||||||
|
->setName('formeditor-redo-form')
|
||||||
|
->setValue('redo')
|
||||||
|
->setClasses('formeditor-element-redo-form-button hidden disabled')
|
||||||
|
->setIcon($this->iconFactory->getIcon('actions-edit-redo', IconSize::SMALL));
|
||||||
|
$moduleTemplate->addButtonToButtonBar($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
|
||||||
|
}
|
||||||
|
return $moduleTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the form editor templates.
|
||||||
|
*/
|
||||||
|
protected function renderFormEditorTemplates(array $prototypeConfiguration, array $formEditorDefinitions): string
|
||||||
|
{
|
||||||
|
$fluidConfiguration = $prototypeConfiguration['formEditor']['formEditorFluidConfiguration'] ?? null;
|
||||||
|
$formEditorPartials = $prototypeConfiguration['formEditor']['formEditorPartials'] ?? null;
|
||||||
|
if (!isset($fluidConfiguration['templatePathAndFilename'])) {
|
||||||
|
throw new RenderingException('The option templatePathAndFilename must be set.', 1485636499);
|
||||||
|
}
|
||||||
|
if (!isset($fluidConfiguration['layoutRootPaths']) || !is_array($fluidConfiguration['layoutRootPaths'])) {
|
||||||
|
throw new RenderingException('The option layoutRootPaths must be set.', 1480294721);
|
||||||
|
}
|
||||||
|
if (!isset($fluidConfiguration['partialRootPaths']) || !is_array($fluidConfiguration['partialRootPaths'])) {
|
||||||
|
throw new RenderingException('The option partialRootPaths must be set.', 1480294722);
|
||||||
|
}
|
||||||
|
|
||||||
|
$elementsCategories = $this->getInsertRenderablesPanelConfiguration($prototypeConfiguration, $formEditorDefinitions['formElements']);
|
||||||
|
$pagesCategories = $this->getInsertRenderablesPanelConfiguration($prototypeConfiguration, $formEditorDefinitions['formElements'], true);
|
||||||
|
$viewFactoryData = new ViewFactoryData(
|
||||||
|
templatePathAndFilename: $fluidConfiguration['templatePathAndFilename'],
|
||||||
|
partialRootPaths: $fluidConfiguration['partialRootPaths'],
|
||||||
|
layoutRootPaths: $fluidConfiguration['layoutRootPaths'],
|
||||||
|
request: $this->request,
|
||||||
|
);
|
||||||
|
$view = $this->viewFactory->create($viewFactoryData);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'elementsCategoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($elementsCategories, false),
|
||||||
|
'pagesCategoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($pagesCategories, false),
|
||||||
|
'formEditorPartials' => $formEditorPartials,
|
||||||
|
]);
|
||||||
|
return $view->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo move this to FormDefinitionConversionService
|
||||||
|
*/
|
||||||
|
protected function transformFormDefinitionForFormEditor(array $prototypeConfiguration, array $formDefinition, string $formPersistenceIdentifier): array
|
||||||
|
{
|
||||||
|
/** @var array<string, list<string>> $multiValueFormElementProperties */
|
||||||
|
$multiValueFormElementProperties = [];
|
||||||
|
/** @var array<string, list<string>> $multiValueFinisherProperties */
|
||||||
|
$multiValueFinisherProperties = [];
|
||||||
|
foreach ($prototypeConfiguration['formElementsDefinition'] as $type => $configuration) {
|
||||||
|
if (!isset($configuration['formEditor']['editors'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($configuration['formEditor']['editors'] as $editorConfiguration) {
|
||||||
|
if (($editorConfiguration['templateName'] ?? '') === 'Inspector-PropertyGridEditor') {
|
||||||
|
$multiValueFormElementProperties[$type][] = $editorConfiguration['propertyPath'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($prototypeConfiguration['formElementsDefinition']['Form']['formEditor']['propertyCollections']['finishers'] ?? [] as $configuration) {
|
||||||
|
if (!isset($configuration['editors'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($configuration['editors'] as $editorConfiguration) {
|
||||||
|
if (($editorConfiguration['templateName'] ?? '') === 'Inspector-PropertyGridEditor') {
|
||||||
|
$multiValueFinisherProperties[$configuration['identifier']][] = $editorConfiguration['propertyPath'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$formDefinition = $this->filterEmptyArrays($formDefinition);
|
||||||
|
$formDefinition = $this->migrateEmailFinisherRecipients($formDefinition);
|
||||||
|
|
||||||
|
$formDefinition = $this->transformMultiValuePropertiesForFormEditor(
|
||||||
|
$formDefinition,
|
||||||
|
'type',
|
||||||
|
$multiValueFormElementProperties
|
||||||
|
);
|
||||||
|
$formDefinition = $this->transformMultiValuePropertiesForFormEditor(
|
||||||
|
$formDefinition,
|
||||||
|
'identifier',
|
||||||
|
$multiValueFinisherProperties
|
||||||
|
);
|
||||||
|
|
||||||
|
$rtePropertyPaths = $this->formDefinitionConversionService->extractRtePropertyPaths($prototypeConfiguration);
|
||||||
|
if ($rtePropertyPaths !== []) {
|
||||||
|
$formDefinition = $this->formDefinitionConversionService->transformRteContentForRichTextEditor(
|
||||||
|
$formDefinition,
|
||||||
|
$rtePropertyPaths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formDefinition = $this->formDefinitionConversionService->sanitizeHtml($formDefinition, $rtePropertyPaths);
|
||||||
|
$formDefinition = $this->formDefinitionConversionService->addHmacData($formDefinition, $formPersistenceIdentifier);
|
||||||
|
return $this->formDefinitionConversionService->migrateFinisherConfiguration($formDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some data needs a transformation before it can be used by the
|
||||||
|
* form editor. This rules for multivalue elements like select
|
||||||
|
* elements. To ensure the right sorting if the data goes into
|
||||||
|
* javascript, we need to do transformations:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* '5' => '5',
|
||||||
|
* '4' => '4',
|
||||||
|
* '3' => '3'
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This method transform this into:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* [
|
||||||
|
* _label => '5'
|
||||||
|
* _value => 5
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* _label => '4'
|
||||||
|
* _value => 4
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* _label => '3'
|
||||||
|
* _value => 3
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @param array<string, list<string>> $multiValueProperties
|
||||||
|
*/
|
||||||
|
protected function transformMultiValuePropertiesForFormEditor(
|
||||||
|
array $formDefinition,
|
||||||
|
string $identifierProperty,
|
||||||
|
array $multiValueProperties
|
||||||
|
): array {
|
||||||
|
$output = $formDefinition;
|
||||||
|
foreach ($formDefinition as $key => $value) {
|
||||||
|
$identifier = $value[$identifierProperty] ?? null;
|
||||||
|
if (is_string($identifier) && array_key_exists($identifier, $multiValueProperties)) {
|
||||||
|
$multiValuePropertiesForIdentifier = $multiValueProperties[$identifier];
|
||||||
|
foreach ($multiValuePropertiesForIdentifier as $multiValueProperty) {
|
||||||
|
if (!ArrayUtility::isValidPath($value, $multiValueProperty, '.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$multiValuePropertyData = ArrayUtility::getValueByPath($value, $multiValueProperty, '.');
|
||||||
|
if (!is_array($multiValuePropertyData)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$newMultiValuePropertyData = [];
|
||||||
|
foreach ($multiValuePropertyData as $k => $v) {
|
||||||
|
$newMultiValuePropertyData[] = [
|
||||||
|
'_label' => $v,
|
||||||
|
'_value' => $k,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$value = ArrayUtility::setValueByPath($value, $multiValueProperty, $newMultiValuePropertyData, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output[$key] = $value;
|
||||||
|
if (is_array($value)) {
|
||||||
|
$output[$key] = $this->transformMultiValuePropertiesForFormEditor(
|
||||||
|
$value,
|
||||||
|
$identifierProperty,
|
||||||
|
$multiValueProperties
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove keys from an array if the key value is an empty array
|
||||||
|
*/
|
||||||
|
protected function filterEmptyArrays(array $array): array
|
||||||
|
{
|
||||||
|
foreach ($array as $key => $value) {
|
||||||
|
if (!is_array($value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($value)) {
|
||||||
|
unset($array[$key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$array[$key] = $this->filterEmptyArrays($value);
|
||||||
|
if (empty($array[$key])) {
|
||||||
|
unset($array[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate single recipient options to their list successors
|
||||||
|
*/
|
||||||
|
protected function migrateEmailFinisherRecipients(array $formDefinition): array
|
||||||
|
{
|
||||||
|
foreach ($formDefinition['finishers'] ?? [] as $i => $finisherConfiguration) {
|
||||||
|
if (!in_array($finisherConfiguration['identifier'], ['EmailToSender', 'EmailToReceiver'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$recipientAddress = $finisherConfiguration['options']['recipientAddress'] ?? '';
|
||||||
|
$recipientName = $finisherConfiguration['options']['recipientName'] ?? '';
|
||||||
|
$carbonCopyAddress = $finisherConfiguration['options']['carbonCopyAddress'] ?? '';
|
||||||
|
$blindCarbonCopyAddress = $finisherConfiguration['options']['blindCarbonCopyAddress'] ?? '';
|
||||||
|
$replyToAddress = $finisherConfiguration['options']['replyToAddress'] ?? '';
|
||||||
|
if (!empty($recipientAddress)) {
|
||||||
|
$finisherConfiguration['options']['recipients'][$recipientAddress] = $recipientName;
|
||||||
|
}
|
||||||
|
if (!empty($carbonCopyAddress)) {
|
||||||
|
$finisherConfiguration['options']['carbonCopyRecipients'][$carbonCopyAddress] = '';
|
||||||
|
}
|
||||||
|
if (!empty($blindCarbonCopyAddress)) {
|
||||||
|
$finisherConfiguration['options']['blindCarbonCopyRecipients'][$blindCarbonCopyAddress] = '';
|
||||||
|
}
|
||||||
|
if (!empty($replyToAddress)) {
|
||||||
|
$finisherConfiguration['options']['replyToRecipients'][$replyToAddress] = '';
|
||||||
|
}
|
||||||
|
unset(
|
||||||
|
$finisherConfiguration['options']['recipientAddress'],
|
||||||
|
$finisherConfiguration['options']['recipientName'],
|
||||||
|
$finisherConfiguration['options']['carbonCopyAddress'],
|
||||||
|
$finisherConfiguration['options']['blindCarbonCopyAddress'],
|
||||||
|
$finisherConfiguration['options']['replyToAddress']
|
||||||
|
);
|
||||||
|
$formDefinition['finishers'][$i] = $finisherConfiguration;
|
||||||
|
}
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function flushPageCache(string $formPersistenceIdentifier): void
|
||||||
|
{
|
||||||
|
$pageIdList = [];
|
||||||
|
$referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($formPersistenceIdentifier);
|
||||||
|
foreach ($referenceRows as $referenceRow) {
|
||||||
|
$record = BackendUtility::getRecord($referenceRow['tablename'], $referenceRow['recuid']);
|
||||||
|
if (!$record) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pageIdList[] = $record['pid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_unique($pageIdList) as $pageId) {
|
||||||
|
$this->cacheManager->flushCachesInGroupByTag('pages', 'pageId_' . $pageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLanguageService(): LanguageService
|
||||||
|
{
|
||||||
|
return $GLOBALS['LANG'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\FinisherOptionsFlexFormOverridesConverter;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\FlexFormFinisherOverridesConverterDto;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frontend controller
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FormFrontendController extends ActionController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ConfigurationService $configurationService,
|
||||||
|
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||||
|
protected readonly FlexFormTools $flexFormTools,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the form which should be rendered from the plugin settings
|
||||||
|
* and overlay the formDefinition with additional data from
|
||||||
|
* flexform and typoscript settings.
|
||||||
|
* This method is used directly to display the first page from the
|
||||||
|
* formDefinition because its cached.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function renderAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$formDefinition = [];
|
||||||
|
if (!empty($this->settings['persistenceIdentifier'])) {
|
||||||
|
$typoScriptSettings = $this->configurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||||
|
$formDefinition = $this->formPersistenceManager->load($this->settings['persistenceIdentifier'], $typoScriptSettings, $this->request);
|
||||||
|
$formDefinition['persistenceIdentifier'] = $this->settings['persistenceIdentifier'];
|
||||||
|
$formDefinition = $this->overrideByFlexFormSettings($formDefinition);
|
||||||
|
$formDefinition = ArrayUtility::setValueByPath($formDefinition, 'renderingOptions._originalIdentifier', $formDefinition['identifier'], '.');
|
||||||
|
$formDefinition['identifier'] .= '-' . ($this->request->getAttribute('currentContentObject')?->data['uid'] ?? '');
|
||||||
|
}
|
||||||
|
$this->view->assign('formConfiguration', $formDefinition);
|
||||||
|
return $this->htmlResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to display all pages / finishers except the
|
||||||
|
* first page because its non cached.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function performAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return new ForwardResponse('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the formDefinition with additional data from the Flexform
|
||||||
|
* settings. For now, only finisher settings are overridable.
|
||||||
|
*/
|
||||||
|
protected function overrideByFlexFormSettings(array $formDefinition): array
|
||||||
|
{
|
||||||
|
$flexFormData = $this->request->getAttribute('currentContentObject')?->data['pi_flexform'] ?? [];
|
||||||
|
if (is_string($flexFormData) && $flexFormData !== '') {
|
||||||
|
$flexFormData = GeneralUtility::xml2array($flexFormData);
|
||||||
|
}
|
||||||
|
if (!is_array($flexFormData) || $flexFormData === []) {
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
if (isset($formDefinition['finishers'])) {
|
||||||
|
$prototypeName = $formDefinition['prototypeName'] ?? 'standard';
|
||||||
|
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($prototypeName);
|
||||||
|
foreach ($formDefinition['finishers'] as $index => $formFinisherDefinition) {
|
||||||
|
$finisherIdentifier = $formFinisherDefinition['identifier'];
|
||||||
|
$sheetIdentifier = $this->getFlexformSheetIdentifier($formDefinition, $prototypeName, $finisherIdentifier);
|
||||||
|
$flexFormSheetSettings = $this->getFlexFormSettingsFromSheet($flexFormData, $sheetIdentifier);
|
||||||
|
if (($this->settings['overrideFinishers'] ?? false) && isset($flexFormSheetSettings['finishers'][$finisherIdentifier])) {
|
||||||
|
$prototypeFinisherDefinition = $prototypeConfiguration['finishersDefinition'][$finisherIdentifier] ?? [];
|
||||||
|
$converterDto = GeneralUtility::makeInstance(
|
||||||
|
FlexFormFinisherOverridesConverterDto::class,
|
||||||
|
$prototypeFinisherDefinition,
|
||||||
|
$formFinisherDefinition,
|
||||||
|
$finisherIdentifier,
|
||||||
|
$flexFormSheetSettings
|
||||||
|
);
|
||||||
|
// Iterate over all `prototypes.<prototypeName>.finishersDefinition.<finisherIdentifier>.FormEngine.elements` values
|
||||||
|
GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeFinisherDefinition['FormEngine']['elements'])->forEach(
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'modifyFinisherOptionsFromFlexFormOverrides',
|
||||||
|
'^(.*)(?:\.config\.type|\.section)$',
|
||||||
|
GeneralUtility::makeInstance(FinisherOptionsFlexFormOverridesConverter::class, $converterDto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$formDefinition['finishers'][$index] = $converterDto->getFinisherDefinition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFlexformSheetIdentifier(array $formDefinition, string $prototypeName, string $finisherIdentifier): string
|
||||||
|
{
|
||||||
|
return md5(
|
||||||
|
implode('', [
|
||||||
|
$formDefinition['persistenceIdentifier'],
|
||||||
|
$prototypeName,
|
||||||
|
$formDefinition['identifier'],
|
||||||
|
$finisherIdentifier,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFlexFormSettingsFromSheet(array $flexForm, string $sheetIdentifier): array
|
||||||
|
{
|
||||||
|
$sheetData = [];
|
||||||
|
$sheetData['data'] = array_filter(
|
||||||
|
$flexForm['data'] ?? [],
|
||||||
|
static function ($key) use ($sheetIdentifier) {
|
||||||
|
return $key === $sheetIdentifier;
|
||||||
|
},
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
);
|
||||||
|
if (empty($sheetData['data'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$sheetDataXml = $this->flexFormTools->flexArray2Xml($sheetData);
|
||||||
|
return $this->flexFormTools->convertFlexFormContentToArray($sheetDataXml)['settings'] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
|
||||||
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||||
|
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||||
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||||
|
use TYPO3\CMS\Core\Charset\CharsetConverter;
|
||||||
|
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||||
|
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||||
|
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||||
|
use TYPO3\CMS\Core\Pagination\ArrayPaginator;
|
||||||
|
use TYPO3\CMS\Core\Pagination\SimplePagination;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||||
|
use TYPO3\CMS\Form\Domain\DTO\SearchCriteria;
|
||||||
|
use TYPO3\CMS\Form\Domain\Repository\FormDefinitionRepository;
|
||||||
|
use TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent;
|
||||||
|
use TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent;
|
||||||
|
use TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent;
|
||||||
|
use TYPO3\CMS\Form\Exception as FormException;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\YamlSource;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Service\DatabaseService;
|
||||||
|
use TYPO3\CMS\Form\Service\TranslationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form manager controller
|
||||||
|
*
|
||||||
|
* Scope: backend
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FormManagerController extends ActionController
|
||||||
|
{
|
||||||
|
use AllowedMethodsTrait;
|
||||||
|
|
||||||
|
protected const JS_MODULE_NAMES = ['app', 'viewModel'];
|
||||||
|
protected const PAGINATION_MAX = 20;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||||
|
protected readonly PageRenderer $pageRenderer,
|
||||||
|
protected readonly IconFactory $iconFactory,
|
||||||
|
protected readonly DatabaseService $databaseService,
|
||||||
|
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||||
|
protected readonly ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||||
|
protected readonly TranslationService $translationService,
|
||||||
|
protected readonly CharsetConverter $charsetConverter,
|
||||||
|
protected readonly UriBuilder $coreUriBuilder,
|
||||||
|
protected readonly YamlSource $yamlSource,
|
||||||
|
protected readonly ComponentFactory $componentFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the Form Manager. The main showing available forms.
|
||||||
|
*/
|
||||||
|
protected function indexAction(int $page = 1, string $searchTerm = '', string $orderField = '', ?string $orderDirection = null): ResponseInterface
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
$hasForms = $this->formPersistenceManager->hasForms([]);
|
||||||
|
$searchCriteria = new SearchCriteria(searchTerm: trim($searchTerm), orderField: $orderField, orderDirection: $orderDirection);
|
||||||
|
$returnUrl = $this->request->getAttribute('normalizedParams')->getRequestUri();
|
||||||
|
$forms = $hasForms ? $this->getAvailableFormDefinitions($formSettings, $searchCriteria, $returnUrl) : [];
|
||||||
|
$arrayPaginator = new ArrayPaginator($forms, $page, self::PAGINATION_MAX);
|
||||||
|
$pagination = new SimplePagination($arrayPaginator);
|
||||||
|
$moduleTemplate = $this->initializeModuleTemplate($this->request, $page, $searchTerm);
|
||||||
|
$moduleTemplate->assignMultiple([
|
||||||
|
'paginator' => $arrayPaginator,
|
||||||
|
'pagination' => $pagination,
|
||||||
|
'searchTerm' => $searchTerm,
|
||||||
|
'orderField' => $searchCriteria->orderField,
|
||||||
|
'orderDirection' => $searchCriteria->orderDirection,
|
||||||
|
'hasForms' => $hasForms,
|
||||||
|
'stylesheets' => $formSettings['formManager']['stylesheets'],
|
||||||
|
'formManagerAppInitialData' => json_encode($this->getFormManagerAppInitialData($formSettings)),
|
||||||
|
]);
|
||||||
|
$javaScriptModules = array_map(
|
||||||
|
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||||
|
array_filter(
|
||||||
|
$formSettings['formManager']['dynamicJavaScriptModules'] ?? [],
|
||||||
|
fn(string $name) => in_array($name, self::JS_MODULE_NAMES, true),
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||||
|
JavaScriptModuleInstruction::create('@typo3/form/backend/helper.js', 'Helper')
|
||||||
|
->invoke('dispatchFormManager', $javaScriptModules, $this->getFormManagerAppInitialData($formSettings))
|
||||||
|
);
|
||||||
|
array_map($this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $javaScriptModules);
|
||||||
|
$moduleTemplate->setModuleClass($this->request->getPluginName() . '_' . $this->request->getControllerName());
|
||||||
|
$moduleTemplate->setFlashMessageQueue($this->getFlashMessageQueue());
|
||||||
|
$moduleTemplate->setTitle(
|
||||||
|
$this->getLanguageService()->translate('title', 'form.module')
|
||||||
|
);
|
||||||
|
return $moduleTemplate->renderResponse('Backend/FormManager/Index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the "create" action.
|
||||||
|
* This action uses the Fluid JsonView::class as view.
|
||||||
|
*/
|
||||||
|
protected function initializeCreateAction(): void
|
||||||
|
{
|
||||||
|
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||||
|
$this->defaultViewObjectName = JsonView::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Form and redirects to the Form Editor
|
||||||
|
*
|
||||||
|
* @throws FormException
|
||||||
|
* @throws PersistenceManagerException
|
||||||
|
*/
|
||||||
|
protected function createAction(string $formName, string $templatePath, string $prototypeName, string $storage, string $storageLocation): ResponseInterface
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
if (!$this->formPersistenceManager->isAllowedStorageLocation($storageLocation)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Save to storage location "%s" is not allowed', $storageLocation), 1614500657);
|
||||||
|
}
|
||||||
|
if (!$this->isValidTemplatePath($formSettings, $prototypeName, $templatePath)) {
|
||||||
|
throw new FormException(sprintf('The template path "%s" is not allowed', $templatePath), 1329233410);
|
||||||
|
}
|
||||||
|
if (empty($formName)) {
|
||||||
|
throw new FormException('No form name', 1472312204);
|
||||||
|
}
|
||||||
|
$templatePath = GeneralUtility::getFileAbsFileName($templatePath);
|
||||||
|
$form = $this->yamlSource->load([$templatePath]);
|
||||||
|
$form['label'] = $formName;
|
||||||
|
$form['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
|
||||||
|
$form['prototypeName'] = $prototypeName;
|
||||||
|
$formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($storage, $form['identifier'], $storageLocation);
|
||||||
|
$event = $this->eventDispatcher->dispatch(
|
||||||
|
new BeforeFormIsCreatedEvent($formPersistenceIdentifier, $form, $this->request)
|
||||||
|
);
|
||||||
|
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||||
|
$form = $event->form;
|
||||||
|
$form = ArrayUtility::stripTagsFromValuesRecursive($form);
|
||||||
|
try {
|
||||||
|
$formIdentifier = $this->formPersistenceManager->save($formPersistenceIdentifier, $form, [], $storageLocation);
|
||||||
|
$response = [
|
||||||
|
'status' => 'success',
|
||||||
|
'url' => (string)$this->coreUriBuilder->buildUriFromRoute('form_editor', ['formPersistenceIdentifier' => $formIdentifier->identifier]),
|
||||||
|
];
|
||||||
|
} catch (PersistenceManagerException $e) {
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// createAction uses the Extbase JsonView::class.
|
||||||
|
// That's why we have to set the view variables in this way.
|
||||||
|
/** @var JsonView $view */
|
||||||
|
$view = $this->view;
|
||||||
|
$view->assign('response', $response);
|
||||||
|
$view->setVariablesToRender([
|
||||||
|
'response',
|
||||||
|
]);
|
||||||
|
return $this->jsonResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the duplicate action.
|
||||||
|
* This action uses the Fluid JsonView::class as view.
|
||||||
|
*/
|
||||||
|
protected function initializeDuplicateAction(): void
|
||||||
|
{
|
||||||
|
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||||
|
$this->defaultViewObjectName = JsonView::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates a given formDefinition and redirects to the Form Editor
|
||||||
|
*
|
||||||
|
* @throws PersistenceManagerException
|
||||||
|
*/
|
||||||
|
protected function duplicateAction(string $formName, string $formPersistenceIdentifier, string $storage, string $storageLocation): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$this->formPersistenceManager->isAllowedStorageLocation($storageLocation)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Save to storage location "%s" is not allowed', $storageLocation), 1614500658);
|
||||||
|
}
|
||||||
|
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Read of "%s" is not allowed', $formPersistenceIdentifier), 1614500659);
|
||||||
|
}
|
||||||
|
$formToDuplicate = $this->formPersistenceManager->load($formPersistenceIdentifier);
|
||||||
|
$formToDuplicate['label'] = $formName;
|
||||||
|
$formToDuplicate['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
|
||||||
|
$formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($storage, $formToDuplicate['identifier'], $storageLocation);
|
||||||
|
$event = $this->eventDispatcher->dispatch(
|
||||||
|
new BeforeFormIsDuplicatedEvent($formPersistenceIdentifier, $formToDuplicate, $this->request)
|
||||||
|
);
|
||||||
|
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||||
|
$formToDuplicate = $event->form;
|
||||||
|
$formToDuplicate = ArrayUtility::stripTagsFromValuesRecursive($formToDuplicate);
|
||||||
|
try {
|
||||||
|
$formIdentifier = $this->formPersistenceManager->save($formPersistenceIdentifier, $formToDuplicate, [], $storageLocation);
|
||||||
|
$response = [
|
||||||
|
'status' => 'success',
|
||||||
|
'url' => (string)$this->coreUriBuilder->buildUriFromRoute('form_editor', ['formPersistenceIdentifier' => $formIdentifier->identifier]),
|
||||||
|
];
|
||||||
|
} catch (PersistenceManagerException $e) {
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// createAction uses the Extbase JsonView::class.
|
||||||
|
// That's why we have to set the view variables in this way.
|
||||||
|
/** @var JsonView $view */
|
||||||
|
$view = $this->view;
|
||||||
|
$view->assign('response', $response);
|
||||||
|
$view->setVariablesToRender([
|
||||||
|
'response',
|
||||||
|
]);
|
||||||
|
return $this->jsonResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the references action.
|
||||||
|
* This action uses the Fluid JsonView::class as view.
|
||||||
|
*/
|
||||||
|
protected function initializeReferencesAction(): void
|
||||||
|
{
|
||||||
|
$this->defaultViewObjectName = JsonView::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show references to this persistence identifier
|
||||||
|
*
|
||||||
|
* @throws PersistenceManagerException
|
||||||
|
*/
|
||||||
|
protected function referencesAction(string $formPersistenceIdentifier): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Access to "%s" is not allowed', $formPersistenceIdentifier), 1614500661);
|
||||||
|
}
|
||||||
|
// referencesAction uses the extbase JsonView::class.
|
||||||
|
// That's why we have to set the view variables in this way.
|
||||||
|
/** @var JsonView $view */
|
||||||
|
$view = $this->view;
|
||||||
|
$view->assign('references', $this->getProcessedReferencesRows($formPersistenceIdentifier));
|
||||||
|
$view->assign('formPersistenceIdentifier', $formPersistenceIdentifier);
|
||||||
|
$view->setVariablesToRender([
|
||||||
|
'references',
|
||||||
|
'formPersistenceIdentifier',
|
||||||
|
]);
|
||||||
|
return $this->jsonResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initializeDeleteAction(): void
|
||||||
|
{
|
||||||
|
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||||
|
$this->defaultViewObjectName = JsonView::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a formDefinition identified by the $formPersistenceIdentifier.
|
||||||
|
*
|
||||||
|
* @throws PersistenceManagerException
|
||||||
|
*/
|
||||||
|
protected function deleteAction(string $formPersistenceIdentifier): ResponseInterface
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||||
|
throw new PersistenceManagerException(sprintf('Delete "%s" is not allowed', $formPersistenceIdentifier), 1768562524);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasReferences = !empty($this->databaseService->getReferencesByPersistenceIdentifier($formPersistenceIdentifier));
|
||||||
|
|
||||||
|
if ($hasReferences) {
|
||||||
|
$response = $this->getErrorResponseForDeleteAction($formSettings, $formPersistenceIdentifier);
|
||||||
|
} else {
|
||||||
|
$event = $this->eventDispatcher->dispatch(
|
||||||
|
new BeforeFormIsDeletedEvent($formPersistenceIdentifier, $this->request)
|
||||||
|
);
|
||||||
|
if ($event->preventDeletion) {
|
||||||
|
$response = $this->getErrorResponseForDeleteAction($formSettings, $formPersistenceIdentifier);
|
||||||
|
} else {
|
||||||
|
$this->formPersistenceManager->delete($formPersistenceIdentifier, []);
|
||||||
|
$response = [
|
||||||
|
'status' => 'success',
|
||||||
|
'url' => $this->uriBuilder->uriFor('index', [], 'FormManager'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAction uses the extbase JsonView::class.
|
||||||
|
// That's why we have to set the view variables in this way.
|
||||||
|
/** @var JsonView $view */
|
||||||
|
$view = $this->view;
|
||||||
|
$view->assign('response', $response);
|
||||||
|
$view->setVariablesToRender([
|
||||||
|
'response',
|
||||||
|
]);
|
||||||
|
return $this->jsonResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getErrorResponseForDeleteAction(array $formSettings, string $formPersistenceIdentifier): array
|
||||||
|
{
|
||||||
|
$controllerConfiguration = $this->translationService->translateValuesRecursive(
|
||||||
|
$formSettings['formManager']['controller'],
|
||||||
|
$formSettings['formManager']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'title' => $controllerConfiguration['deleteAction']['errorTitle'],
|
||||||
|
'message' => sprintf($controllerConfiguration['deleteAction']['errorMessage'], $formPersistenceIdentifier),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormSettings(): array
|
||||||
|
{
|
||||||
|
$typoScriptSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||||
|
$formSettings = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, false);
|
||||||
|
if (!isset($formSettings['formManager'])) {
|
||||||
|
// Config sub array formManager is crucial and should always exist. If it does
|
||||||
|
// not, this indicates an issue in config loading logic. Except in this case.
|
||||||
|
throw new \LogicException('Configuration could not be loaded', 1723717461);
|
||||||
|
}
|
||||||
|
return $formSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the json encoded data which is used by the form editor
|
||||||
|
* JavaScript app.
|
||||||
|
*/
|
||||||
|
protected function getFormManagerAppInitialData(array $formSettings): array
|
||||||
|
{
|
||||||
|
$formManagerAppInitialData = [
|
||||||
|
'selectablePrototypesConfiguration' => $formSettings['formManager']['selectablePrototypesConfiguration'],
|
||||||
|
'endpoints' => [
|
||||||
|
'create' => $this->uriBuilder->uriFor('create'),
|
||||||
|
'duplicate' => $this->uriBuilder->uriFor('duplicate'),
|
||||||
|
'delete' => $this->uriBuilder->uriFor('delete'),
|
||||||
|
'references' => $this->uriBuilder->uriFor('references'),
|
||||||
|
],
|
||||||
|
'accessibleStorageAdapters' => $this->formPersistenceManager->getAccessibleStorageAdapters(),
|
||||||
|
];
|
||||||
|
$formManagerAppInitialData = ArrayUtility::reIndexNumericArrayKeysRecursive($formManagerAppInitialData);
|
||||||
|
return $this->translationService->translateValuesRecursive(
|
||||||
|
$formManagerAppInitialData,
|
||||||
|
$formSettings['formManager']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all formDefinitions which can be loaded through form persistence
|
||||||
|
* manager. Enrich this data by a reference counter.
|
||||||
|
*/
|
||||||
|
protected function getAvailableFormDefinitions(array $formSettings, SearchCriteria $searchCriteria, string $returnUrl = ''): array
|
||||||
|
{
|
||||||
|
$availableFormDefinitions = [];
|
||||||
|
|
||||||
|
foreach ($this->formPersistenceManager->listForms($formSettings, $searchCriteria) as $formMetadata) {
|
||||||
|
|
||||||
|
if ($formMetadata->persistenceIdentifier && !$formMetadata->invalid && !$formMetadata->readOnly) {
|
||||||
|
$editUrl = (string)$this->coreUriBuilder->buildUriFromRoute(
|
||||||
|
'form_editor',
|
||||||
|
array_filter([
|
||||||
|
'formPersistenceIdentifier' => $formMetadata->persistenceIdentifier,
|
||||||
|
'returnUrl' => $returnUrl,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
$formMetadata = $formMetadata->withEditUrl($editUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions = $this->getRecordActions($formMetadata->persistenceIdentifier);
|
||||||
|
$formMetadata = $formMetadata->withActions($actions);
|
||||||
|
|
||||||
|
if ($searchCriteria->searchTerm === ''
|
||||||
|
|| $this->valueContainsSearchTerm($formMetadata->name, $searchCriteria->searchTerm)
|
||||||
|
|| ($formMetadata->persistenceIdentifier && $this->valueContainsSearchTerm($formMetadata->persistenceIdentifier, $searchCriteria->searchTerm))
|
||||||
|
) {
|
||||||
|
$availableFormDefinitions[] = $formMetadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $availableFormDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function valueContainsSearchTerm(string $value, string $searchTerm): bool
|
||||||
|
{
|
||||||
|
return str_contains(strtolower($value), strtolower($searchTerm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array with information about the references for a
|
||||||
|
* formDefinition identified by $persistenceIdentifier.
|
||||||
|
*/
|
||||||
|
protected function getProcessedReferencesRows(string $persistenceIdentifier): array
|
||||||
|
{
|
||||||
|
if (empty($persistenceIdentifier)) {
|
||||||
|
throw new \InvalidArgumentException('$persistenceIdentifier must not be empty.', 1477071939);
|
||||||
|
}
|
||||||
|
$references = [];
|
||||||
|
$referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($persistenceIdentifier);
|
||||||
|
foreach ($referenceRows as $referenceRow) {
|
||||||
|
$record = $this->getRecord($referenceRow['tablename'], $referenceRow['recuid']);
|
||||||
|
if (!$record) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pageRecord = $this->getRecord('pages', $record['pid']);
|
||||||
|
$urlParameters = [
|
||||||
|
'edit' => [
|
||||||
|
$referenceRow['tablename'] => [
|
||||||
|
$referenceRow['recuid'] => 'edit',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'module' => 'web_FormFormbuilder',
|
||||||
|
'returnUrl' => $this->getModuleUrl('web_FormFormbuilder'),
|
||||||
|
];
|
||||||
|
$references[] = [
|
||||||
|
'recordPageTitle' => is_array($pageRecord) ? BackendUtility::getRecordTitle('pages', $pageRecord) : '',
|
||||||
|
'recordTitle' => BackendUtility::getRecordTitle($referenceRow['tablename'], $record),
|
||||||
|
'recordIcon' => $this->iconFactory->getIconForRecord($referenceRow['tablename'], $record, IconSize::SMALL)->render(),
|
||||||
|
'recordUid' => $referenceRow['recuid'],
|
||||||
|
'recordEditUrl' => $this->getModuleUrl('record_edit', $urlParameters),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $references;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given $templatePath for a given $prototypeName is valid
|
||||||
|
* and accessible.
|
||||||
|
*
|
||||||
|
* Valid template paths has to be configured within
|
||||||
|
* formManager.selectablePrototypesConfiguration.[('identifier': $prototypeName)].newFormTemplates.[('templatePath': $templatePath)]
|
||||||
|
*/
|
||||||
|
protected function isValidTemplatePath(array $formSettings, string $prototypeName, string $templatePath): bool
|
||||||
|
{
|
||||||
|
$isValid = false;
|
||||||
|
foreach ($formSettings['formManager']['selectablePrototypesConfiguration'] as $prototypesConfiguration) {
|
||||||
|
if ($prototypesConfiguration['identifier'] !== $prototypeName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($prototypesConfiguration['newFormTemplates'] as $templatesConfiguration) {
|
||||||
|
if ($templatesConfiguration['templatePath'] !== $templatePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$isValid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$templatePath = GeneralUtility::getFileAbsFileName($templatePath);
|
||||||
|
if (!is_file($templatePath)) {
|
||||||
|
$isValid = false;
|
||||||
|
}
|
||||||
|
return $isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the record actions
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* @throws RouteNotFoundException
|
||||||
|
*/
|
||||||
|
protected function getRecordActions(string $persistenceIdentifier): array
|
||||||
|
{
|
||||||
|
if (!MathUtility::canBeInterpretedAsInteger($persistenceIdentifier)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions = [];
|
||||||
|
|
||||||
|
// History button
|
||||||
|
$urlParameters = [
|
||||||
|
'element' => FormDefinitionRepository::TABLE_NAME . ':' . $persistenceIdentifier,
|
||||||
|
'returnUrl' => $this->request->getAttribute('normalizedParams')->getRequestUri(),
|
||||||
|
];
|
||||||
|
$actions['recordHistoryUrl'] = (string)$this->coreUriBuilder->buildUriFromRoute('record_history', $urlParameters);
|
||||||
|
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init ModuleTemplate and register document header buttons
|
||||||
|
*/
|
||||||
|
protected function initializeModuleTemplate(ServerRequestInterface $request, int $page, string $searchTerm): ModuleTemplate
|
||||||
|
{
|
||||||
|
$moduleTemplate = $this->moduleTemplateFactory->create($request);
|
||||||
|
// Create new
|
||||||
|
$addFormButton = $this->componentFactory->createLinkButton()
|
||||||
|
->setDataAttributes(['identifier' => 'newForm'])
|
||||||
|
->setHref('#')
|
||||||
|
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.create_new_form'))
|
||||||
|
->setShowLabelText(true)
|
||||||
|
->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL));
|
||||||
|
$moduleTemplate->addButtonToButtonBar($addFormButton);
|
||||||
|
// Shortcut
|
||||||
|
$arguments = [];
|
||||||
|
if ($searchTerm) {
|
||||||
|
$arguments['tx_form_web_formformbuilder']['searchTerm'] = $searchTerm;
|
||||||
|
$arguments['tx_form_web_formformbuilder']['controller'] = 'FormManager';
|
||||||
|
}
|
||||||
|
if ($page > 1) {
|
||||||
|
$arguments['tx_form_web_formformbuilder']['page'] = $page;
|
||||||
|
$arguments['tx_form_web_formformbuilder']['controller'] = 'FormManager';
|
||||||
|
}
|
||||||
|
$moduleTemplate->getDocHeaderComponent()->setShortcutContext(
|
||||||
|
'web_FormFormbuilder',
|
||||||
|
$this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:module.shortcut_name'),
|
||||||
|
$arguments
|
||||||
|
);
|
||||||
|
return $moduleTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a form identifier which is the lower cased form name.
|
||||||
|
*/
|
||||||
|
protected function convertFormNameToIdentifier(string $formName): string
|
||||||
|
{
|
||||||
|
$formName = \Normalizer::normalize($formName) ?: $formName;
|
||||||
|
$formIdentifier = $this->charsetConverter->utf8_char_mapping($formName);
|
||||||
|
$formIdentifier = (string)preg_replace('/[^a-zA-Z0-9-_]/', '', $formIdentifier);
|
||||||
|
return lcfirst($formIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper used for unit testing.
|
||||||
|
*/
|
||||||
|
protected function getRecord(string $table, int $uid): ?array
|
||||||
|
{
|
||||||
|
return BackendUtility::getRecord($table, $uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper used for unit testing.
|
||||||
|
*/
|
||||||
|
protected function getModuleUrl(string $moduleName, array $urlParameters = []): string
|
||||||
|
{
|
||||||
|
return (string)$this->coreUriBuilder->buildUriFromRoute($moduleName, $urlParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLanguageService(): LanguageService
|
||||||
|
{
|
||||||
|
return $GLOBALS['LANG'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\DependencyInjection;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
use Symfony\Component\Yaml\Exception\ParseException;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
use TYPO3\CMS\Core\Package\PackageManager;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\FormYamlCollector;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\FormYamlConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symfony service configurator for {@see FormYamlCollector}.
|
||||||
|
*
|
||||||
|
* Iterates over all active TYPO3 packages and registers every form YAML set
|
||||||
|
* found under {@code Configuration/Form/<SetName>/} with the collector.
|
||||||
|
*
|
||||||
|
* Each set directory must contain a {@code config.yaml}
|
||||||
|
* with the actual form configuration (loaded in both frontend and backend).
|
||||||
|
*
|
||||||
|
* Sets whose declared {@code name} in {@code config.yaml} appears in
|
||||||
|
* {@code $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets']}
|
||||||
|
* are skipped.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class FormYamlCollectorConfigurator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PackageManager $packageManager,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the given {@see FormYamlCollector} with all auto-discovered
|
||||||
|
* form YAML configurations across all active extensions.
|
||||||
|
*
|
||||||
|
* Note: {@see $GLOBALS['TYPO3_CONF_VARS']} is read at service-instantiation
|
||||||
|
* time (not at DI-compile time), so ext_localconf.php values are available.
|
||||||
|
*/
|
||||||
|
public function configure(FormYamlCollector $collector): void
|
||||||
|
{
|
||||||
|
// Sets listed here (by their config.yaml "name" field) are excluded from loading.
|
||||||
|
// Example in ext_localconf.php:
|
||||||
|
// $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets'][] = 'vendor/set-name';
|
||||||
|
$disabledSets = (array)($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets'] ?? []);
|
||||||
|
|
||||||
|
foreach ($this->packageManager->getActivePackages() as $package) {
|
||||||
|
$formConfigPath = $package->getPackagePath() . 'Configuration/Form';
|
||||||
|
|
||||||
|
if (!is_dir($formConfigPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extensionKey = $package->getPackageKey();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$finder = Finder::create()
|
||||||
|
->files()
|
||||||
|
->depth(1)
|
||||||
|
->sortByName()
|
||||||
|
->name('config.yaml')
|
||||||
|
->in($formConfigPath);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
// Directory exists but is not traversable
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($finder as $fileInfo) {
|
||||||
|
$setDirectory = dirname($fileInfo->getPathname());
|
||||||
|
$setDirectoryName = basename($setDirectory);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = Yaml::parseFile($fileInfo->getPathname()) ?? [];
|
||||||
|
} catch (ParseException $e) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'EXT:form skipped form set: could not parse config.yaml.',
|
||||||
|
[
|
||||||
|
'file' => $fileInfo->getPathname(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($config)) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'EXT:form skipped form set: config.yaml did not return an array.',
|
||||||
|
['file' => $fileInfo->getPathname()]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip disabled sets. Matching is done against the declared "name" in config.yaml
|
||||||
|
// (e.g. "my-vendor/my-set"), NOT against the directory name, so that renaming
|
||||||
|
// a set directory does not break the disable list.
|
||||||
|
$declaredName = (string)($config['name'] ?? '');
|
||||||
|
if ($declaredName !== '' && in_array($declaredName, $disabledSets, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$priority = (int)($config['priority'] ?? 100);
|
||||||
|
$virtualBase = 'EXT:' . $extensionKey . '/Configuration/Form/' . $setDirectoryName . '/';
|
||||||
|
|
||||||
|
$collector->add(new FormYamlConfiguration(
|
||||||
|
path: $virtualBase . 'config.yaml',
|
||||||
|
priority: $priority,
|
||||||
|
setName: $declaredName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Condition;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\ExpressionLanguage\AbstractProvider;
|
||||||
|
use TYPO3\CMS\Form\Domain\Condition\Functions\FormConditionFunctionsProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: frontend
|
||||||
|
* **This class is NOT meant to be sub classed by developers.**
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ConditionProvider extends AbstractProvider
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->expressionLanguageProviders = [
|
||||||
|
FormConditionFunctionsProvider::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Condition\Functions;
|
||||||
|
|
||||||
|
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
|
||||||
|
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FormConditionFunctionsProvider implements ExpressionFunctionProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return ExpressionFunction[] An array of Function instances
|
||||||
|
*/
|
||||||
|
public function getFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->getFormValueFunction(),
|
||||||
|
$this->getRootFormPropertyFunction(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut function to access field values
|
||||||
|
*/
|
||||||
|
protected function getFormValueFunction(): ExpressionFunction
|
||||||
|
{
|
||||||
|
return new ExpressionFunction(
|
||||||
|
'getFormValue',
|
||||||
|
static fn() => null, // Not implemented, we only use the evaluator
|
||||||
|
static function ($arguments, $field, $default = null) {
|
||||||
|
return $arguments['formValues'][$field] ?? $default;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRootFormPropertyFunction(): ExpressionFunction
|
||||||
|
{
|
||||||
|
return new ExpressionFunction(
|
||||||
|
'getRootFormProperty',
|
||||||
|
static fn() => null, // Not implemented, we only use the evaluator
|
||||||
|
static function ($arguments, $property) {
|
||||||
|
$formDefinition = $arguments['formRuntime']->getFormDefinition();
|
||||||
|
try {
|
||||||
|
$value = ObjectAccess::getPropertyPath($formDefinition, $property);
|
||||||
|
} catch (\Exception) {
|
||||||
|
$value = null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for array processing
|
||||||
|
*
|
||||||
|
* Scope: frontend / backend
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ArrayProcessing
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $expression;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
protected $processor;
|
||||||
|
|
||||||
|
public function __construct(string $identifier, string $expression, callable $processor)
|
||||||
|
{
|
||||||
|
$this->identifier = $identifier;
|
||||||
|
$this->expression = $expression;
|
||||||
|
$this->processor = $processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpression(): string
|
||||||
|
{
|
||||||
|
return $this->expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProcessor(): callable
|
||||||
|
{
|
||||||
|
return $this->processor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
$this->data = ArrayUtility::flattenPlain($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ArrayProcessing[] $processings
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,676 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration;
|
||||||
|
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||||
|
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||||
|
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\Event\AfterFormDefinitionValidationConfigurationIsBuiltEvent;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Service\TranslationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for configuration settings
|
||||||
|
* Scope: frontend / backend
|
||||||
|
*
|
||||||
|
* @todo: Get rid of ConfigurationManagerInterface by handing over $formSettings to
|
||||||
|
* methods instead to make the indirect dependency to Request explicit in consuming classes.
|
||||||
|
* @todo: Declare readonly when ConfigurationService is no longer injected lazy, or wait
|
||||||
|
* for PHP 8.3 minimum for symfony to allow both lazy and readonly dependencies.
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true)]
|
||||||
|
class ConfigurationService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
|
||||||
|
protected ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||||
|
protected TranslationService $translationService,
|
||||||
|
#[Autowire(service: 'cache.assets')]
|
||||||
|
protected FrontendInterface $assetsCache,
|
||||||
|
#[Autowire(service: 'cache.runtime')]
|
||||||
|
protected FrontendInterface $runtimeCache,
|
||||||
|
protected EventDispatcherInterface $eventDispatcher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
if (!isset($formSettings['prototypes'][$prototypeName])) {
|
||||||
|
throw new PrototypeNotFoundException(sprintf('The Prototype "%s" was not found.', $prototypeName), 1475924277);
|
||||||
|
}
|
||||||
|
return $formSettings['prototypes'][$prototypeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
$returnValue = GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessor::class,
|
||||||
|
$formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
|
||||||
|
)->forEach(
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'selectablePrototypeNames',
|
||||||
|
'^([\d]+)\.identifier$',
|
||||||
|
static function ($_, $value) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return array_values($returnValue['selectablePrototypeNames'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a form element property is defined in the form setup.
|
||||||
|
* If a form element property is defined in the form setup then it
|
||||||
|
* means that the form element property can be written by the form editor.
|
||||||
|
* A form element property can be written if the property path is defined within
|
||||||
|
* the following form editor properties:
|
||||||
|
* * formElementsDefinition.<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 via the
|
||||||
|
* AfterFormDefinitionValidationConfigurationIsBuiltEvent PSR-14 event.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? [];
|
||||||
|
return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a form elements finisher|validator property is defined in the form setup.
|
||||||
|
* If a form elements finisher|validator property is defined in the form setup then it
|
||||||
|
* means that the form elements finisher|validator property can be written by the form editor.
|
||||||
|
* A form elements finisher|validator property can be written if the property path is defined within
|
||||||
|
* the following form editor properties:
|
||||||
|
* * formElementsDefinition.<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 elements finisher|validator editor implementation which does not define the writable
|
||||||
|
* property paths by one of the above described inspector editor properties (e.g "propertyPath") within
|
||||||
|
* the form setup, you must provide the writable property paths via the
|
||||||
|
* AfterFormDefinitionValidationConfigurationIsBuiltEvent PSR-14 event.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? [];
|
||||||
|
return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a form element editor has a property called "selectOptions"
|
||||||
|
* (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
|
||||||
|
* then only the defined values within the selectOptions are allowed to be written
|
||||||
|
* by the form editor.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
|
||||||
|
ValidationDto $dto
|
||||||
|
): bool {
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
|
||||||
|
return isset(
|
||||||
|
$formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['selectOptions'][$propertyPath]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "selectOptions" value for a form element property from the form setup.
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getAllowedValuesForFormElementPropertyFromFormEditorSetup(
|
||||||
|
ValidationDto $dto,
|
||||||
|
bool $translated = true
|
||||||
|
): array {
|
||||||
|
if (!$this->formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
|
||||||
|
throw new PropertyException(
|
||||||
|
sprintf(
|
||||||
|
'No selectOptions found for form element type "%s" and property path "%s"',
|
||||||
|
$dto->getFormElementType(),
|
||||||
|
$dto->getPropertyPath()
|
||||||
|
),
|
||||||
|
1614264312
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
|
||||||
|
$propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
|
||||||
|
return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$propertyPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a form elements finisher|validator editor has a property called "selectOptions"
|
||||||
|
* (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
|
||||||
|
* then only the defined values within the selectOptions are allowed to be written
|
||||||
|
* by the form editor.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
|
||||||
|
ValidationDto $dto
|
||||||
|
): bool {
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
|
||||||
|
return isset(
|
||||||
|
$formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['selectOptions'][$propertyPath]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "selectOptions" value for a form elements finisher|validator property from the form setup.
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup(
|
||||||
|
ValidationDto $dto,
|
||||||
|
bool $translated = true
|
||||||
|
): array {
|
||||||
|
if (!$this->propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
|
||||||
|
throw new PropertyException(
|
||||||
|
sprintf(
|
||||||
|
'No selectOptions found for property collection "%s" and identifier "%s" and property path "%s"',
|
||||||
|
$dto->getPropertyCollectionName(),
|
||||||
|
$dto->getPropertyCollectionElementIdentifier(),
|
||||||
|
$dto->getPropertyPath()
|
||||||
|
),
|
||||||
|
1614264313
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
|
||||||
|
$propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
|
||||||
|
return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$propertyPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getBasePropertyPathFromMultiValueFormElementProperty(ValidationDto $dto): string
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$propertyPath = $dto->getPropertyPath();
|
||||||
|
$multiValueProperties = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['multiValueProperties'] ?? [];
|
||||||
|
foreach ($multiValueProperties as $multiValueProperty) {
|
||||||
|
if (str_starts_with($propertyPath, $multiValueProperty)) {
|
||||||
|
$propertyPath = $multiValueProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $propertyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getBasePropertyPathFromMultiValuePropertyCollectionElement(ValidationDto $dto): string
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$propertyPath = $dto->getPropertyPath();
|
||||||
|
$multiValueProperties = $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['multiValueProperties'] ?? [];
|
||||||
|
foreach ($multiValueProperties as $multiValueProperty) {
|
||||||
|
if (str_starts_with($propertyPath, $multiValueProperty)) {
|
||||||
|
$propertyPath = $multiValueProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $propertyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a form element property is defined in "predefinedDefaults" in the form setup.
|
||||||
|
* If a form element property is defined in the "predefinedDefaults" in the form setup then it
|
||||||
|
* means that the form element property can be written by the form editor.
|
||||||
|
* A form element default property is defined within the following form editor properties:
|
||||||
|
* * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
return isset(
|
||||||
|
$formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "predefinedDefaults" value for a form element property from the form setup.
|
||||||
|
* A form element default property is defined within the following form editor properties:
|
||||||
|
* * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true): mixed
|
||||||
|
{
|
||||||
|
if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
|
||||||
|
throw new PropertyException(
|
||||||
|
sprintf(
|
||||||
|
'No predefinedDefaults found for form element type "%s" and property path "%s"',
|
||||||
|
$dto->getFormElementType(),
|
||||||
|
$dto->getPropertyPath()
|
||||||
|
),
|
||||||
|
1528578401
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
|
||||||
|
return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$dto->getPropertyPath()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup.
|
||||||
|
* If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it
|
||||||
|
* means that the form elements finisher|validator property can be written by the form editor.
|
||||||
|
* A form elements finisher|validator default property is defined within the following form editor properties:
|
||||||
|
* * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
return isset(
|
||||||
|
$formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup.
|
||||||
|
* A form elements finisher|validator default property is defined within the following form editor properties:
|
||||||
|
* * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true): mixed
|
||||||
|
{
|
||||||
|
if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
|
||||||
|
throw new PropertyException(
|
||||||
|
sprintf(
|
||||||
|
'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"',
|
||||||
|
$dto->getPropertyCollectionName(),
|
||||||
|
$dto->getPropertyCollectionElementIdentifier(),
|
||||||
|
$dto->getPropertyPath()
|
||||||
|
),
|
||||||
|
1528578402
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
$property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
|
||||||
|
return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$dto->getPropertyPath()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the form element is creatable through the form editor.
|
||||||
|
* A form element is creatable if the following properties are set:
|
||||||
|
* * formElementsDefinition.<formElementType>.formEditor.group
|
||||||
|
* * formElementsDefinition.<formElementType>.formEditor.groupSorting
|
||||||
|
* And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
|
||||||
|
* one of the keys within "formEditor.formElementGroups"
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
if ($dto->getFormElementType() === 'Form') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the form elements finisher|validator is creatable through the form editor.
|
||||||
|
* A form elements finisher|validator is creatable if the following conditions are true:
|
||||||
|
* "formElementsDefinition.<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>"
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
|
||||||
|
$dto->getPrototypeName()
|
||||||
|
);
|
||||||
|
return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the form elements type is defined within the form setup.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
|
||||||
|
{
|
||||||
|
$prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
|
||||||
|
return ArrayUtility::isValidPath(
|
||||||
|
$prototypeConfiguration,
|
||||||
|
'formElementsDefinition.' . $dto->getFormElementType(),
|
||||||
|
'.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getAllBackendTranslationsForTranslationKeys(array $keys, string $prototypeName): array
|
||||||
|
{
|
||||||
|
$translations = [];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!is_string($key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$translations[$key] = $this->getAllBackendTranslationsForTranslationKey($key, $prototypeName);
|
||||||
|
}
|
||||||
|
return $translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array
|
||||||
|
{
|
||||||
|
$prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
|
||||||
|
return $this->translationService->translateToAllBackendLanguages(
|
||||||
|
$key,
|
||||||
|
[],
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormSettings(): array
|
||||||
|
{
|
||||||
|
// @todo: This is needed for extFormConfigurationManager to apply stdWrap on TS configuration.
|
||||||
|
// Find a way to get rid of this.
|
||||||
|
$isFrontend = false;
|
||||||
|
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||||
|
if ($request instanceof ServerRequestInterface) {
|
||||||
|
$isFrontend = ApplicationType::fromRequest($request)->isFrontend();
|
||||||
|
}
|
||||||
|
// @todo: Note this code relies on the fact that the request has been set to ExtbaseConfigurationManagerInterface already.
|
||||||
|
$typoScriptSettings = $this->extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||||
|
return $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, $isFrontend, $isFrontend ? $request : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all the form editor configurations which are needed to check if a
|
||||||
|
* form definition property can be written or not.
|
||||||
|
*/
|
||||||
|
protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array
|
||||||
|
{
|
||||||
|
$cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]);
|
||||||
|
$configuration = $this->getCacheEntry($cacheKey);
|
||||||
|
if ($configuration === null) {
|
||||||
|
$prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
|
||||||
|
$extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration);
|
||||||
|
GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach(
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementPropertyPaths',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
|
||||||
|
GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementAdditionalElementPropertyPaths',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
|
||||||
|
GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementRelativeMultiValueProperties',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
|
||||||
|
GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementSelectOptions',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
|
||||||
|
GeneralUtility::makeInstance(SelectOptionsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementPredefinedDefaults',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
|
||||||
|
GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'formElementCreatable',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor.group$',
|
||||||
|
GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionCreatable',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
|
||||||
|
GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionPropertyPaths',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
|
||||||
|
GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionAdditionalElementPropertyPaths',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
|
||||||
|
GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionRelativeMultiValueProperties',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$',
|
||||||
|
GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionSelectOptions',
|
||||||
|
'^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
|
||||||
|
GeneralUtility::makeInstance(CollectionSelectOptionsExtractor::class, $extractorDto)
|
||||||
|
),
|
||||||
|
GeneralUtility::makeInstance(
|
||||||
|
ArrayProcessing::class,
|
||||||
|
'propertyCollectionPredefinedDefaults',
|
||||||
|
'^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
|
||||||
|
GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$configuration = $extractorDto->getResult();
|
||||||
|
$configuration = $this->translateValues($prototypeConfiguration, $configuration);
|
||||||
|
$configuration = $this->eventDispatcher
|
||||||
|
->dispatch(new AfterFormDefinitionValidationConfigurationIsBuiltEvent($prototypeName, $configuration))
|
||||||
|
->getConfiguration();
|
||||||
|
$this->setCacheEntry($cacheKey, $configuration);
|
||||||
|
}
|
||||||
|
return $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool
|
||||||
|
{
|
||||||
|
if (empty($subConfig)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true)
|
||||||
|
|| in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true)
|
||||||
|
|| in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) {
|
||||||
|
if (str_starts_with($propertyPath, $relativeMultiValueProperty)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function translateValues(array $prototypeConfiguration, array $configuration): array
|
||||||
|
{
|
||||||
|
if (isset($configuration['formElements'])) {
|
||||||
|
$configuration['formElements'] = $this->translatePredefinedDefaults(
|
||||||
|
$prototypeConfiguration,
|
||||||
|
$configuration['formElements']
|
||||||
|
);
|
||||||
|
$configuration['formElements'] = $this->translateSelectOptions(
|
||||||
|
$prototypeConfiguration,
|
||||||
|
$configuration['formElements']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
foreach ($configuration['collections'] ?? [] as $name => $collections) {
|
||||||
|
$configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections);
|
||||||
|
$configuration['collections'][$name] = $this->translateSelectOptions($prototypeConfiguration, $configuration['collections'][$name]);
|
||||||
|
}
|
||||||
|
return $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array
|
||||||
|
{
|
||||||
|
foreach ($formElements as $name => $formElement) {
|
||||||
|
if (!isset($formElement['predefinedDefaults'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$formElement['untranslatedPredefinedDefaults'] = $formElement['predefinedDefaults'];
|
||||||
|
$formElement['predefinedDefaults'] = $this->translationService->translateValuesRecursive(
|
||||||
|
$formElement['predefinedDefaults'],
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
$formElements[$name] = $formElement;
|
||||||
|
}
|
||||||
|
return $formElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function translateSelectOptions(array $prototypeConfiguration, array $formElements): array
|
||||||
|
{
|
||||||
|
foreach ($formElements as $name => $formElement) {
|
||||||
|
if (empty($formElement['selectOptions']) || !is_array($formElement['selectOptions'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$formElement['untranslatedSelectOptions'] = $formElement['selectOptions'];
|
||||||
|
$formElement['selectOptions'] = $this->translationService->translateValuesRecursive(
|
||||||
|
$formElement['selectOptions'],
|
||||||
|
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||||
|
);
|
||||||
|
$formElements[$name] = $formElement;
|
||||||
|
}
|
||||||
|
return $formElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCacheEntry(string $cacheKey): mixed
|
||||||
|
{
|
||||||
|
$cacheKey = 'form_' . $cacheKey;
|
||||||
|
if ($this->runtimeCache->has($cacheKey)) {
|
||||||
|
return $this->runtimeCache->get($cacheKey);
|
||||||
|
}
|
||||||
|
if ($this->assetsCache->has($cacheKey)) {
|
||||||
|
return $this->assetsCache->get($cacheKey);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setCacheEntry(string $cacheKey, mixed $value): void
|
||||||
|
{
|
||||||
|
$cacheKey = 'form_' . $cacheKey;
|
||||||
|
$this->runtimeCache->set($cacheKey, $value);
|
||||||
|
$this->assetsCache->set($cacheKey, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ArrayProcessorException extends Exception {}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
|
||||||
|
|
||||||
|
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,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ProcessorDto
|
||||||
|
*/
|
||||||
|
protected $converterDto;
|
||||||
|
|
||||||
|
public function __construct(ProcessorDto $converterDto)
|
||||||
|
{
|
||||||
|
$this->converterDto = $converterDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a FlexForm element for a finisher option
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FinisherOptionGenerator extends AbstractProcessor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @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 (isset($elementConfiguration['config'])) {
|
||||||
|
$elementConfiguration['config']['default'] = $optionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$languageService = $this->getLanguageService();
|
||||||
|
$elementConfiguration['label'] = (string)($elementConfiguration['label'] ?? '');
|
||||||
|
if (empty($optionValue)) {
|
||||||
|
$optionValue = $languageService->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:empty');
|
||||||
|
} elseif (is_array($optionValue)) {
|
||||||
|
$optionValue = implode(',', $optionValue);
|
||||||
|
}
|
||||||
|
$elementConfiguration['label'] .= sprintf(' (%s: "%s")', $languageService->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:default'), $optionValue);
|
||||||
|
|
||||||
|
$sheetElements = $this->converterDto->getResult();
|
||||||
|
$sheetElements['settings.finishers.' . $finisherIdentifier . '.' . $optionKey] = $elementConfiguration;
|
||||||
|
|
||||||
|
$this->converterDto->setResult($sheetElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLanguageService(): LanguageService
|
||||||
|
{
|
||||||
|
return $GLOBALS['LANG'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $finisherIdentifier,
|
||||||
|
array $finisherDefinitionFromSetup,
|
||||||
|
array $finisherDefinitionFromFormDefinition
|
||||||
|
) {
|
||||||
|
$this->finisherIdentifier = $finisherIdentifier;
|
||||||
|
$this->finisherDefinitionFromSetup = $finisherDefinitionFromSetup;
|
||||||
|
$this->finisherDefinitionFromFormDefinition = $finisherDefinitionFromFormDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->finisherIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherDefinitionFromSetup(): array
|
||||||
|
{
|
||||||
|
return $this->finisherDefinitionFromSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherDefinitionFromFormDefinition(): array
|
||||||
|
{
|
||||||
|
return $this->finisherDefinitionFromFormDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(): array
|
||||||
|
{
|
||||||
|
return $this->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResult(array $result): ProcessorDto
|
||||||
|
{
|
||||||
|
$this->result = $result;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FlexformConfiguration\Processors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for FlexForm processors
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(ProcessorDto $converterDto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value, array $matches);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractConverter implements ConverterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $sessionToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ConverterDto
|
||||||
|
*/
|
||||||
|
protected $converterDto;
|
||||||
|
|
||||||
|
public function __construct(ConverterDto $converterDto, string $sessionToken = '')
|
||||||
|
{
|
||||||
|
$this->converterDto = $converterDto;
|
||||||
|
$this->sessionToken = $sessionToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
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 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',
|
||||||
|
'^.*',
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AddHmacDataToFormElementPropertyConverter extends AbstractConverter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value): void
|
||||||
|
{
|
||||||
|
$formDefinition = $this->converterDto->getFormDefinition();
|
||||||
|
|
||||||
|
$propertyPathParts = explode('.', $key);
|
||||||
|
$lastKeySegment = array_pop($propertyPathParts);
|
||||||
|
$propertyPathParts[] = '_orig_' . $lastKeySegment;
|
||||||
|
|
||||||
|
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||||
|
$hmacValuePath = implode('.', array_merge($this->converterDto->getRenderablePathParts(), $propertyPathParts));
|
||||||
|
$hmacValue = [
|
||||||
|
'value' => $value,
|
||||||
|
'hmac' => $hashService->hmac(serialize([$this->converterDto->getFormElementIdentifier(), $key, $value]), $this->sessionToken),
|
||||||
|
];
|
||||||
|
|
||||||
|
$formDefinition = ArrayUtility::setValueByPath($formDefinition, $hmacValuePath, $hmacValue, '.');
|
||||||
|
|
||||||
|
$this->converterDto->setFormDefinition($formDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AddHmacDataToPropertyCollectionElementConverter extends AbstractConverter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
));
|
||||||
|
|
||||||
|
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||||
|
$hmacValue = [
|
||||||
|
'value' => $value,
|
||||||
|
'hmac' => $hashService->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,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 = '';
|
||||||
|
|
||||||
|
public function __construct(array $formDefinition)
|
||||||
|
{
|
||||||
|
$this->formDefinition = $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefinition(): array
|
||||||
|
{
|
||||||
|
return $this->formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormDefinition(array $formDefinition): ConverterDto
|
||||||
|
{
|
||||||
|
$this->formDefinition = $formDefinition;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRenderablePathParts(): array
|
||||||
|
{
|
||||||
|
return $this->renderablePathParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRenderablePathParts(array $renderablePathParts): ConverterDto
|
||||||
|
{
|
||||||
|
$this->renderablePathParts = $renderablePathParts;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormElementIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->formElementIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormElementIdentifier(string $formElementIdentifier): ConverterDto
|
||||||
|
{
|
||||||
|
$this->formElementIdentifier = $formElementIdentifier;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyCollectionIndex(): int
|
||||||
|
{
|
||||||
|
return $this->propertyCollectionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPropertyCollectionIndex(int $propertyCollectionIndex): ConverterDto
|
||||||
|
{
|
||||||
|
$this->propertyCollectionIndex = $propertyCollectionIndex;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyCollectionName(): string
|
||||||
|
{
|
||||||
|
return $this->propertyCollectionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPropertyCollectionName(string $propertyCollectionName): ConverterDto
|
||||||
|
{
|
||||||
|
$this->propertyCollectionName = $propertyCollectionName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyCollectionElementIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->propertyCollectionElementIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPropertyCollectionElementIdentifier(string $propertyCollectionElementIdentifier): ConverterDto
|
||||||
|
{
|
||||||
|
$this->propertyCollectionElementIdentifier = $propertyCollectionElementIdentifier;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ConverterInterface
|
||||||
|
{
|
||||||
|
public function __construct(ConverterDto $converterDto, string $sessionToken = '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value);
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$prototypeFinisherDefinition = $this->converterDto->getPrototypeFinisherDefinition();
|
||||||
|
$finisherDefinition = $this->converterDto->getFinisherDefinition();
|
||||||
|
$finisherIdentifier = $this->converterDto->getFinisherIdentifier();
|
||||||
|
$flexFormSheetSettings = $this->converterDto->getFlexFormSheetSettings();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = ArrayUtility::getValueByPath(
|
||||||
|
$flexFormSheetSettings['finishers'][$finisherIdentifier],
|
||||||
|
$optionKey,
|
||||||
|
'.'
|
||||||
|
);
|
||||||
|
} catch (MissingArrayPathException $exception) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldConfiguration = $prototypeFinisherDefinition['FormEngine']['elements'][$optionKey] ?? [];
|
||||||
|
|
||||||
|
if ($fieldConfiguration['section'] ?? false) {
|
||||||
|
if (!is_array($value) || $value === []) {
|
||||||
|
// Do not process empty values for sections
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedOptionValue = [];
|
||||||
|
|
||||||
|
foreach ($value as $optionListValue) {
|
||||||
|
$key = $optionListValue[$fieldConfiguration['sectionItemKey']];
|
||||||
|
$value = $optionListValue[$fieldConfiguration['sectionItemValue']];
|
||||||
|
$processedOptionValue[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $processedOptionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optionPath = 'options.' . $optionKey;
|
||||||
|
|
||||||
|
// Skip additional translation for finisher options that were changed via flexform
|
||||||
|
if ($this->optionValueHasChanged($finisherDefinition, $optionPath, $value)) {
|
||||||
|
$finisherDefinition['options']['translation']['propertiesExcludedFromTranslation'][] = $optionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finisherDefinition = ArrayUtility::setValueByPath($finisherDefinition, $optionPath, $value, '.');
|
||||||
|
|
||||||
|
$this->converterDto->setFinisherDefinition($finisherDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if finisher option value differs from finisher definition.
|
||||||
|
*
|
||||||
|
* Compares the given finisher option value with the corresponding value in the
|
||||||
|
* finisher definition. Returns `true` if both values are equal, `false` otherwise.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $finisherDefinition
|
||||||
|
*/
|
||||||
|
protected function optionValueHasChanged(array $finisherDefinition, string $optionPath, mixed $value): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $value !== ArrayUtility::getValueByPath($finisherDefinition, $optionPath, '.');
|
||||||
|
} catch (MissingArrayPathException) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FlexFormFinisherOverridesConverterDto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $prototypeFinisherDefinition = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $finisherDefinition = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $finisherIdentifier = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $flexFormSheetSettings = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
array $prototypeFinisherDefinition,
|
||||||
|
array $finisherDefinition,
|
||||||
|
string $finisherIdentifier,
|
||||||
|
array $flexFormSheetSettings
|
||||||
|
) {
|
||||||
|
$this->prototypeFinisherDefinition = $prototypeFinisherDefinition;
|
||||||
|
$this->finisherDefinition = $finisherDefinition;
|
||||||
|
$this->finisherIdentifier = $finisherIdentifier;
|
||||||
|
$this->flexFormSheetSettings = $flexFormSheetSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrototypeFinisherDefinition(): array
|
||||||
|
{
|
||||||
|
return $this->prototypeFinisherDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherDefinition(): array
|
||||||
|
{
|
||||||
|
return $this->finisherDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFinisherDefinition(array $finisherDefinition): FlexFormFinisherOverridesConverterDto
|
||||||
|
{
|
||||||
|
$this->finisherDefinition = $finisherDefinition;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->finisherIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFlexFormSheetSettings(): array
|
||||||
|
{
|
||||||
|
return $this->flexFormSheetSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class RemoveHmacDataConverter extends AbstractConverter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Remove the hmac data ("_orig_<propertyName>") for the corresponding property.
|
||||||
|
*
|
||||||
|
* @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,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractValidator implements ValidatorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $currentElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $sessionToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ValidationDto
|
||||||
|
*/
|
||||||
|
protected $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.
|
||||||
|
*/
|
||||||
|
protected function buildHmacDataPath(string $propertyPath): string
|
||||||
|
{
|
||||||
|
$pathParts = explode('.', $propertyPath);
|
||||||
|
$lastPathSegment = array_pop($pathParts);
|
||||||
|
$pathParts[] = '_orig_' . $lastPathSegment;
|
||||||
|
|
||||||
|
return implode('.', $pathParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormDefinitionValidationService(): FormDefinitionValidationService
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfigurationService(): ConfigurationService
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(ConfigurationService::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
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 mixed $value
|
||||||
|
* @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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
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 defined 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 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
|
||||||
|
* @throws PropertyException
|
||||||
|
*/
|
||||||
|
protected function validateFormElementPredefinedDefaultValue(
|
||||||
|
$value,
|
||||||
|
ValidationDto $dto
|
||||||
|
): void {
|
||||||
|
// If the form element is newly 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 editor 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
|
||||||
|
* @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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
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 defined 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 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
|
||||||
|
* @throws PropertyException
|
||||||
|
*/
|
||||||
|
protected function validatePropertyCollectionElementPredefinedDefaultValue(
|
||||||
|
$value,
|
||||||
|
ValidationDto $dto
|
||||||
|
): void {
|
||||||
|
// If the property collection element is newly 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
|
||||||
|
* @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,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
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 mixed $value
|
||||||
|
* @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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FormElementHmacDataValidator extends ElementBasedValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if the form element property value matches to its hmac hash.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value): void
|
||||||
|
{
|
||||||
|
$dto = $this->validationDto->withPropertyPath($key);
|
||||||
|
$this->validateFormElementPropertyValueByHmacData(
|
||||||
|
$this->currentElement,
|
||||||
|
$value,
|
||||||
|
$this->sessionToken,
|
||||||
|
$dto
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PropertyCollectionElementHmacDataValidator extends CollectionBasedValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if the property collection element values matches to its hmac hash.
|
||||||
|
*
|
||||||
|
* @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,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrototypeName(): string
|
||||||
|
{
|
||||||
|
return $this->prototypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormElementType(): string
|
||||||
|
{
|
||||||
|
return $this->formElementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormElementIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->formElementIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyPath(): string
|
||||||
|
{
|
||||||
|
return $this->propertyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyCollectionName(): string
|
||||||
|
{
|
||||||
|
return $this->propertyCollectionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyCollectionElementIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->propertyCollectionElementIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPrototypeName(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->prototypeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasFormElementType(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->formElementType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasFormElementIdentifier(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->formElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPropertyPath(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->propertyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPropertyCollectionName(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->propertyCollectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPropertyCollectionElementIdentifier(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPrototypeName(string $prototypeName): ValidationDto
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(self::class, $prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withFormElementType(string $formElementType): ValidationDto
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withFormElementIdentifier(string $formElementIdentifier): ValidationDto
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPropertyPath(string $propertyPath): ValidationDto
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPropertyCollectionName(string $propertyCollectionName): ValidationDto
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $propertyCollectionName, $this->propertyCollectionElementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ValidatorInterface
|
||||||
|
{
|
||||||
|
public function __construct(array $currentElement, string $sessionToken, ValidationDto $validationDto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||||
|
use TYPO3\CMS\Core\Crypto\Random;
|
||||||
|
use TYPO3\CMS\Core\Html\SanitizerBuilderFactory;
|
||||||
|
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;
|
||||||
|
use TYPO3\CMS\Form\Service\RichTextConfigurationService;
|
||||||
|
use TYPO3\HtmlSanitizer\Sanitizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true)]
|
||||||
|
readonly class FormDefinitionConversionService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RichTextConfigurationService $richTextConfigurationService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
*/
|
||||||
|
public function addHmacData(array $formDefinition, string $formPersistenceIdentifier): array
|
||||||
|
{
|
||||||
|
// Extend the hmac hashing key with a "per form editor session" unique key.
|
||||||
|
$sessionToken = $this->generateSessionToken();
|
||||||
|
$this->persistSessionToken($sessionToken, $formPersistenceIdentifier);
|
||||||
|
|
||||||
|
$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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $converterDto->getFormDefinition();
|
||||||
|
|
||||||
|
// Embed the form persistence identifier so the TypeConverter can
|
||||||
|
// look up the correct per-form session token when saving.
|
||||||
|
$result['_formPersistenceIdentifier'] = $formPersistenceIdentifier;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the "_orig_<propertyName>" values and the
|
||||||
|
* "_formPersistenceIdentifier" marker from the form definition.
|
||||||
|
*/
|
||||||
|
public function removeHmacData(array $formDefinition): array
|
||||||
|
{
|
||||||
|
unset($formDefinition['_formPersistenceIdentifier']);
|
||||||
|
|
||||||
|
$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
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function persistSessionToken(string $sessionToken, string $formPersistenceIdentifier): void
|
||||||
|
{
|
||||||
|
$tokens = $this->getBackendUser()->getSessionData('extFormProtectionSessionTokens') ?? [];
|
||||||
|
if (!is_array($tokens)) {
|
||||||
|
$tokens = [];
|
||||||
|
}
|
||||||
|
$tokens[$formPersistenceIdentifier] = $sessionToken;
|
||||||
|
$this->getBackendUser()->setAndSaveSessionData('extFormProtectionSessionTokens', $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the session token for a specific form persistence identifier.
|
||||||
|
*/
|
||||||
|
public function retrieveSessionToken(string $formPersistenceIdentifier): string
|
||||||
|
{
|
||||||
|
$tokens = $this->getBackendUser()->getSessionData('extFormProtectionSessionTokens');
|
||||||
|
if (is_array($tokens) && isset($tokens[$formPersistenceIdentifier]) && is_string($tokens[$formPersistenceIdentifier])) {
|
||||||
|
return $tokens[$formPersistenceIdentifier];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sanitizeHtml(array $rawFormDefinitionArray, array $rtePropertyPaths = [], string $defaultBuild = 'default'): array
|
||||||
|
{
|
||||||
|
return $this->sanitizeValuesRecursively($rawFormDefinitionArray, $rtePropertyPaths, $defaultBuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformRteContentForPersistence(array $formDefinition, array $rtePropertyPaths): array
|
||||||
|
{
|
||||||
|
if ($rtePropertyPaths === []) {
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->transformRteContentRecursively($formDefinition, $rtePropertyPaths, $this->richTextConfigurationService, 'persistence');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformRteContentForRichTextEditor(array $formDefinition, array $rtePropertyPaths): array
|
||||||
|
{
|
||||||
|
if ($rtePropertyPaths === []) {
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->transformRteContentRecursively($formDefinition, $rtePropertyPaths, $this->richTextConfigurationService, 'rte');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function transformRteContentRecursively(
|
||||||
|
array $formDefinition,
|
||||||
|
array $rtePropertyPaths,
|
||||||
|
RichTextConfigurationService $richTextConfigurationService,
|
||||||
|
string $direction = 'persistence'
|
||||||
|
): array {
|
||||||
|
// Get the element type (e.g., 'Checkbox', 'StaticText', 'Form')
|
||||||
|
$elementType = $formDefinition['type'] ?? null;
|
||||||
|
|
||||||
|
// Transform properties for this specific element type
|
||||||
|
if ($elementType !== null && isset($rtePropertyPaths[$elementType])) {
|
||||||
|
foreach ($rtePropertyPaths[$elementType] as $propertyPath => $presetName) {
|
||||||
|
$value = $this->getValueByPath($formDefinition, $propertyPath);
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
$transformedValue = $direction === 'persistence'
|
||||||
|
? $richTextConfigurationService->transformTextForPersistence($value, $presetName)
|
||||||
|
: $richTextConfigurationService->transformTextForRichTextEditor($value, $presetName);
|
||||||
|
$formDefinition = $this->setValueByPath($formDefinition, $propertyPath, $transformedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into renderables (form elements on pages)
|
||||||
|
if (is_array($formDefinition['renderables'] ?? null)) {
|
||||||
|
foreach ($formDefinition['renderables'] as $key => $renderable) {
|
||||||
|
if (is_array($renderable)) {
|
||||||
|
$formDefinition['renderables'][$key] = $this->transformRteContentRecursively(
|
||||||
|
$renderable,
|
||||||
|
$rtePropertyPaths,
|
||||||
|
$richTextConfigurationService,
|
||||||
|
$direction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform finisher options
|
||||||
|
if (is_array($formDefinition['finishers'] ?? null)) {
|
||||||
|
$finisherRtePaths = $rtePropertyPaths['_finishers'] ?? [];
|
||||||
|
foreach ($formDefinition['finishers'] as $key => $finisher) {
|
||||||
|
if (!is_array($finisher)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finisherIdentifier = $finisher['identifier'] ?? null;
|
||||||
|
if ($finisherIdentifier === null || !isset($finisherRtePaths[$finisherIdentifier])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($finisherRtePaths[$finisherIdentifier] as $propertyPath => $presetName) {
|
||||||
|
// Property path in finisher config is like 'options.message'
|
||||||
|
$value = $this->getValueByPath($finisher, $propertyPath);
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
$transformedValue = $direction === 'persistence'
|
||||||
|
? $richTextConfigurationService->transformTextForPersistence($value, $presetName)
|
||||||
|
: $richTextConfigurationService->transformTextForRichTextEditor($value, $presetName);
|
||||||
|
$finisher = $this->setValueByPath($finisher, $propertyPath, $transformedValue);
|
||||||
|
$formDefinition['finishers'][$key] = $finisher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getValueByPath(array $array, string $path): mixed
|
||||||
|
{
|
||||||
|
$keys = explode('.', $path);
|
||||||
|
$current = $array;
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!is_array($current) || !array_key_exists($key, $current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$current = $current[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setValueByPath(array $array, string $path, mixed $value): array
|
||||||
|
{
|
||||||
|
$keys = explode('.', $path);
|
||||||
|
$current = &$array;
|
||||||
|
|
||||||
|
foreach ($keys as $i => $key) {
|
||||||
|
if ($i === count($keys) - 1) {
|
||||||
|
$current[$key] = $value;
|
||||||
|
} else {
|
||||||
|
if (!isset($current[$key]) || !is_array($current[$key])) {
|
||||||
|
$current[$key] = [];
|
||||||
|
}
|
||||||
|
$current = &$current[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract RTE-enabled property paths from prototype configuration.
|
||||||
|
*
|
||||||
|
* Scans the form editor configuration for all form element types and finishers
|
||||||
|
* to find editors with enableRichtext=true and returns their property paths
|
||||||
|
* along with the RTE preset name, organized by element type.
|
||||||
|
*
|
||||||
|
* @param array $prototypeConfiguration The prototype configuration array
|
||||||
|
* @return array Map of element types to their RTE property paths
|
||||||
|
* Format: [
|
||||||
|
* 'Checkbox' => ['label' => 'form-label'],
|
||||||
|
* 'StaticText' => ['properties.text' => 'form-content'],
|
||||||
|
* '_finishers' => ['Confirmation' => ['options.message' => 'form-label']]
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
public function extractRtePropertyPaths(array $prototypeConfiguration): array
|
||||||
|
{
|
||||||
|
$rtePropertyPaths = [];
|
||||||
|
|
||||||
|
// Extract from form elements definition
|
||||||
|
$formElementsDefinition = $prototypeConfiguration['formElementsDefinition'] ?? [];
|
||||||
|
foreach ($formElementsDefinition as $formElementType => $elementConfig) {
|
||||||
|
$editors = $elementConfig['formEditor']['editors'] ?? [];
|
||||||
|
foreach ($editors as $editor) {
|
||||||
|
if ($this->isRteEditor($editor)) {
|
||||||
|
$propertyPath = $editor['propertyPath'] ?? '';
|
||||||
|
$presetName = $editor['richtextConfiguration'] ?? 'form-label';
|
||||||
|
if ($propertyPath !== '') {
|
||||||
|
$rtePropertyPaths[$formElementType][$propertyPath] = $presetName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract from finisher property collections on the Form element
|
||||||
|
// Finisher editors are defined in:
|
||||||
|
// formElementsDefinition.Form.formEditor.propertyCollections.finishers.<index>.editors
|
||||||
|
$finisherCollections = $formElementsDefinition['Form']['formEditor']['propertyCollections']['finishers'] ?? [];
|
||||||
|
foreach ($finisherCollections as $finisherCollection) {
|
||||||
|
$finisherIdentifier = $finisherCollection['identifier'] ?? '';
|
||||||
|
if ($finisherIdentifier === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$editors = $finisherCollection['editors'] ?? [];
|
||||||
|
foreach ($editors as $editor) {
|
||||||
|
if ($this->isRteEditor($editor)) {
|
||||||
|
$propertyPath = $editor['propertyPath'] ?? '';
|
||||||
|
$presetName = $editor['richtextConfiguration'] ?? 'form-label';
|
||||||
|
if ($propertyPath !== '') {
|
||||||
|
$rtePropertyPaths['_finishers'][$finisherIdentifier][$propertyPath] = $presetName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rtePropertyPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an editor configuration represents an RTE-enabled textarea.
|
||||||
|
*/
|
||||||
|
protected function isRteEditor(array $editor): bool
|
||||||
|
{
|
||||||
|
return ($editor['templateName'] ?? '') === 'Inspector-TextareaEditor'
|
||||||
|
&& ($editor['enableRichtext'] ?? false) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively sanitizes values in form definition.
|
||||||
|
*
|
||||||
|
* For RTE-enabled fields: Uses HtmlSanitizer with the preset configured in the RTE configuration
|
||||||
|
* For all other string fields: Uses strip_tags to remove ALL HTML
|
||||||
|
*
|
||||||
|
* @param array $array The array to sanitize
|
||||||
|
* @param array $rtePropertyPaths Map of element types to their RTE property paths with preset names
|
||||||
|
* @param string $defaultBuild Default sanitizer build name for RTE fields without specific preset
|
||||||
|
* @param string|null $currentElementType The current element type being processed
|
||||||
|
* @param string $currentPath The current property path being processed
|
||||||
|
*/
|
||||||
|
protected function sanitizeValuesRecursively(
|
||||||
|
array $array,
|
||||||
|
array $rtePropertyPaths = [],
|
||||||
|
string $defaultBuild = 'default',
|
||||||
|
?string $currentElementType = null,
|
||||||
|
string $currentPath = ''
|
||||||
|
): array {
|
||||||
|
$result = $array;
|
||||||
|
|
||||||
|
// Detect element type from current array (only at element root level)
|
||||||
|
$elementType = $result['type'] ?? $currentElementType;
|
||||||
|
|
||||||
|
// Get RTE property paths for this element type (with their preset names)
|
||||||
|
$elementRtePaths = [];
|
||||||
|
if ($elementType !== null && isset($rtePropertyPaths[$elementType])) {
|
||||||
|
$elementRtePaths = $rtePropertyPaths[$elementType];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($result as $key => $value) {
|
||||||
|
// Build the full property path
|
||||||
|
$propertyPath = $currentPath === '' ? $key : $currentPath . '.' . $key;
|
||||||
|
|
||||||
|
if ($key === 'renderables' && is_array($value)) {
|
||||||
|
// For renderables, process each child element with fresh context
|
||||||
|
foreach ($value as $childKey => $childValue) {
|
||||||
|
if (is_array($childValue)) {
|
||||||
|
$result[$key][$childKey] = $this->sanitizeValuesRecursively(
|
||||||
|
$childValue,
|
||||||
|
$rtePropertyPaths,
|
||||||
|
$defaultBuild
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($key === 'finishers' && is_array($value)) {
|
||||||
|
// Handle finishers separately
|
||||||
|
$finisherRtePaths = $rtePropertyPaths['_finishers'] ?? [];
|
||||||
|
foreach ($value as $finisherKey => $finisher) {
|
||||||
|
if (is_array($finisher)) {
|
||||||
|
$finisherIdentifier = $finisher['identifier'] ?? null;
|
||||||
|
$finisherRteFields = [];
|
||||||
|
if ($finisherIdentifier !== null && isset($finisherRtePaths[$finisherIdentifier])) {
|
||||||
|
$finisherRteFields = $finisherRtePaths[$finisherIdentifier];
|
||||||
|
}
|
||||||
|
$result[$key][$finisherKey] = $this->sanitizeFinisherRecursively(
|
||||||
|
$finisher,
|
||||||
|
$finisherRteFields,
|
||||||
|
$defaultBuild
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (is_array($value)) {
|
||||||
|
// Recurse into nested arrays, keeping the element type and building path
|
||||||
|
$result[$key] = $this->sanitizeValuesRecursively(
|
||||||
|
$value,
|
||||||
|
$rtePropertyPaths,
|
||||||
|
$defaultBuild,
|
||||||
|
$elementType,
|
||||||
|
$propertyPath
|
||||||
|
);
|
||||||
|
} elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||||
|
$stringValue = (string)$value;
|
||||||
|
|
||||||
|
// Check if this property path is an RTE field for the current element type
|
||||||
|
if (isset($elementRtePaths[$propertyPath])) {
|
||||||
|
// RTE field: use HtmlSanitizer with the configured preset
|
||||||
|
// This ensures sanitization even for form definitions from external sources
|
||||||
|
$presetBuild = $this->resolveSanitizerBuildFromPreset($elementRtePaths[$propertyPath]);
|
||||||
|
$result[$key] = $this->sanitizeWithBuild($stringValue, $presetBuild ?? $defaultBuild);
|
||||||
|
} else {
|
||||||
|
// Non-RTE field: strip ALL HTML tags for security
|
||||||
|
$result[$key] = strip_tags($stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively sanitize finisher values.
|
||||||
|
*
|
||||||
|
* @param array $finisher The finisher configuration
|
||||||
|
* @param array $rteFields Map of RTE field paths to their preset names
|
||||||
|
* @param string $defaultBuild Default sanitizer build name
|
||||||
|
* @param string $currentPath Current property path
|
||||||
|
*/
|
||||||
|
protected function sanitizeFinisherRecursively(
|
||||||
|
array $finisher,
|
||||||
|
array $rteFields,
|
||||||
|
string $defaultBuild = 'default',
|
||||||
|
string $currentPath = ''
|
||||||
|
): array {
|
||||||
|
foreach ($finisher as $key => $value) {
|
||||||
|
$fullPath = $currentPath === '' ? $key : $currentPath . '.' . $key;
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$finisher[$key] = $this->sanitizeFinisherRecursively($value, $rteFields, $defaultBuild, $fullPath);
|
||||||
|
} elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||||
|
$stringValue = (string)$value;
|
||||||
|
|
||||||
|
if (isset($rteFields[$fullPath])) {
|
||||||
|
// RTE field: use HtmlSanitizer with the configured preset
|
||||||
|
$presetBuild = $this->resolveSanitizerBuildFromPreset($rteFields[$fullPath]);
|
||||||
|
$finisher[$key] = $this->sanitizeWithBuild($stringValue, $presetBuild ?? $defaultBuild);
|
||||||
|
} else {
|
||||||
|
// Non-RTE field: strip ALL HTML tags for security
|
||||||
|
$finisher[$key] = strip_tags($stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $finisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the sanitizer build name from an RTE preset configuration.
|
||||||
|
*
|
||||||
|
* @param string $presetName The RTE preset name (e.g., 'form-label', 'form-content')
|
||||||
|
* @return string|null The sanitizer build name, or null if not configured
|
||||||
|
*/
|
||||||
|
protected function resolveSanitizerBuildFromPreset(string $presetName): ?string
|
||||||
|
{
|
||||||
|
$processingConfig = $this->richTextConfigurationService->resolveProcessingConfiguration($presetName);
|
||||||
|
return $processingConfig['HTMLparser_db.']['htmlSanitize.']['build'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML content with the specified sanitizer build.
|
||||||
|
*
|
||||||
|
* @param string $content The HTML content to sanitize
|
||||||
|
* @param string $build The sanitizer build name or class name
|
||||||
|
* @return string The sanitized content
|
||||||
|
*/
|
||||||
|
protected function sanitizeWithBuild(string $content, string $build): string
|
||||||
|
{
|
||||||
|
return $this->createSanitizer($build)->sanitize($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sanitizer instance for the given build configuration.
|
||||||
|
*
|
||||||
|
* Supports both preset names (e.g., 'default') and class names implementing BuilderInterface.
|
||||||
|
*
|
||||||
|
* @param string $build The sanitizer build name or class name
|
||||||
|
* @return Sanitizer The sanitizer instance
|
||||||
|
*/
|
||||||
|
protected function createSanitizer(string $build): Sanitizer
|
||||||
|
{
|
||||||
|
if (class_exists($build) && is_a($build, \TYPO3\HtmlSanitizer\Builder\BuilderInterface::class, true)) {
|
||||||
|
$builder = GeneralUtility::makeInstance($build);
|
||||||
|
} else {
|
||||||
|
$factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class);
|
||||||
|
$builder = $factory->build($build);
|
||||||
|
}
|
||||||
|
return $builder->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the random token which is used in the hash for the form tokens.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function generateSessionToken(): string
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getBackendUser(): BackendUserAuthentication
|
||||||
|
{
|
||||||
|
return $GLOBALS['BE_USER'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
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\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
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true)]
|
||||||
|
readonly class FormDefinitionValidationService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected HashService $hashService,
|
||||||
|
protected ConfigurationService $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
|
||||||
|
*
|
||||||
|
* @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->configurationService->isFormElementTypeCreatableByFormEditor($validationDto)) {
|
||||||
|
$this->validateAllPropertyValuesFromCreatableFormElement(
|
||||||
|
$currentFormElement,
|
||||||
|
$sessionToken,
|
||||||
|
$validationDto
|
||||||
|
);
|
||||||
|
foreach ($propertyCollectionElements as $propertyCollectionElement) {
|
||||||
|
$validationDto = $validationDto->withPropertyCollectionElementIdentifier(
|
||||||
|
$propertyCollectionElement['identifier']
|
||||||
|
);
|
||||||
|
if ($this->configurationService->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.
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
*/
|
||||||
|
public function isPropertyValueEqualToHistoricalValue(
|
||||||
|
array $hmacContent,
|
||||||
|
mixed $propertyValue,
|
||||||
|
array $hmacData,
|
||||||
|
string $sessionToken
|
||||||
|
): bool {
|
||||||
|
$this->checkHmacDataIntegrity($hmacData, $hmacContent, $sessionToken);
|
||||||
|
$hmacContent[] = $propertyValue;
|
||||||
|
$expectedHash = $this->hashService->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.
|
||||||
|
*
|
||||||
|
* @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 = $this->hashService->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.
|
||||||
|
*/
|
||||||
|
protected function validateAllFormElementPropertyValuesByHmac(
|
||||||
|
array $currentElement,
|
||||||
|
string $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.
|
||||||
|
*/
|
||||||
|
protected function validateAllPropertyCollectionElementValuesByHmac(
|
||||||
|
array $currentElement,
|
||||||
|
string $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 defined 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.
|
||||||
|
*/
|
||||||
|
protected function validateAllPropertyValuesFromCreatableFormElement(
|
||||||
|
array $currentElement,
|
||||||
|
string $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 defined 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.
|
||||||
|
*/
|
||||||
|
protected function validateAllPropertyValuesFromCreatablePropertyCollectionElement(
|
||||||
|
array $currentElement,
|
||||||
|
string $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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract class AbstractExtractor implements ExtractorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ExtractorDto
|
||||||
|
*/
|
||||||
|
protected $extractorDto;
|
||||||
|
|
||||||
|
public function __construct(ExtractorDto $extractorDto)
|
||||||
|
{
|
||||||
|
$this->extractorDto = $extractorDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AdditionalElementPropertyPathsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ExtractorDto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $prototypeConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $result = [];
|
||||||
|
|
||||||
|
public function __construct(array $prototypeConfiguration)
|
||||||
|
{
|
||||||
|
$this->prototypeConfiguration = $prototypeConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrototypeConfiguration(): array
|
||||||
|
{
|
||||||
|
return $this->prototypeConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(): array
|
||||||
|
{
|
||||||
|
return $this->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResult(array $result): ExtractorDto
|
||||||
|
{
|
||||||
|
$this->result = $result;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface ExtractorInterface
|
||||||
|
{
|
||||||
|
public function __construct(ExtractorDto $extractorDto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $key, $value, array $matches);
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class IsCreatableFormElementExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class MultiValuePropertiesExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $_, $value, array $matches)
|
||||||
|
{
|
||||||
|
[, $formElementType, $formEditorIndex] = $matches;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$value !== 'Inspector-PropertyGridEditor'
|
||||||
|
&& $value !== 'Inspector-MultiSelectEditor'
|
||||||
|
&& $value !== 'Inspector-CountrySelectEditor'
|
||||||
|
&& $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PredefinedDefaultsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $_, $value, array $matches)
|
||||||
|
{
|
||||||
|
[, $formElementType, $propertyPath] = $matches;
|
||||||
|
|
||||||
|
$result = $this->extractorDto->getResult();
|
||||||
|
$result['formElements'][$formElementType]['predefinedDefaults'][$propertyPath] = $value;
|
||||||
|
$this->extractorDto->setResult($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PropertyPathsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPropertyPaths(string $value, array $matches): array
|
||||||
|
{
|
||||||
|
$paths = [];
|
||||||
|
[, $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class SelectOptionsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class IsCreatablePropertyCollectionElementExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class MultiValuePropertiesExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __invoke(string $_, $value, array $matches)
|
||||||
|
{
|
||||||
|
[, $formElementType, $propertyCollectionName, $propertyCollectionIndex, $propertyCollectionEditorIndex] = $matches;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$value !== 'Inspector-PropertyGridEditor'
|
||||||
|
&& $value !== 'Inspector-MultiSelectEditor'
|
||||||
|
&& $value !== 'Inspector-CountrySelectEditor'
|
||||||
|
&& $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PredefinedDefaultsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PropertyPathsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class SelectOptionsExtractor extends AbstractExtractor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
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,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Configuration;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Form\Domain\DTO\PersistenceManagerConfiguration;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for accessing form storage configuration (persistenceManager settings)
|
||||||
|
*
|
||||||
|
* This service provides a clean interface to access form storage related settings
|
||||||
|
* from the YAML configuration without coupling every component to the configuration
|
||||||
|
* loading mechanism.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true)]
|
||||||
|
final readonly class PersistenceConfigurationService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(lazy: true)]
|
||||||
|
private ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
|
||||||
|
#[Autowire(lazy: ExtFormConfigurationManagerInterface::class)]
|
||||||
|
private ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all form settings
|
||||||
|
*/
|
||||||
|
public function getFormSettings(): array
|
||||||
|
{
|
||||||
|
$isFrontend = $this->isFrontendRequest();
|
||||||
|
$request = $this->getCurrentRequest();
|
||||||
|
|
||||||
|
$typoScriptSettings = $this->extbaseConfigurationManager->getConfiguration(
|
||||||
|
ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS,
|
||||||
|
'form'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->extFormConfigurationManager->getYamlConfiguration(
|
||||||
|
$typoScriptSettings,
|
||||||
|
$isFrontend,
|
||||||
|
$isFrontend ? $request : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get persistence manager settings as a typed DTO
|
||||||
|
*/
|
||||||
|
public function getPersistenceManagerConfiguration(): PersistenceManagerConfiguration
|
||||||
|
{
|
||||||
|
$formSettings = $this->getFormSettings();
|
||||||
|
return PersistenceManagerConfiguration::fromArray($formSettings['persistenceManager'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get allowed extension paths from form configuration
|
||||||
|
*
|
||||||
|
* @return string[] Array of allowed extension paths (e.g., ["EXT:my_extension/Configuration/Forms/"])
|
||||||
|
*/
|
||||||
|
public function getAllowedExtensionPaths(): array
|
||||||
|
{
|
||||||
|
return $this->getPersistenceManagerConfiguration()->allowedExtensionPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get allowed pages for form storage.
|
||||||
|
*
|
||||||
|
* Forms are always stored on pid 0 (root level).
|
||||||
|
*
|
||||||
|
* @return array<int, array{uid: int, title: string}> Array of allowed page IDs
|
||||||
|
*/
|
||||||
|
public function getAllowedPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
0 => [
|
||||||
|
'uid' => 0,
|
||||||
|
'title' => 'Root',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if saving to extension paths is allowed
|
||||||
|
*/
|
||||||
|
public function isAllowedToSaveToExtensionPaths(): bool
|
||||||
|
{
|
||||||
|
return $this->getPersistenceManagerConfiguration()->allowSaveToExtensionPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if deleting from extension paths is allowed
|
||||||
|
*/
|
||||||
|
public function isAllowedToDeleteFromExtensionPaths(): bool
|
||||||
|
{
|
||||||
|
return $this->getPersistenceManagerConfiguration()->allowDeleteFromExtensionPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sort configuration for form listing
|
||||||
|
*
|
||||||
|
* @return array{sortByKeys: string[], sortAscending: bool}
|
||||||
|
*/
|
||||||
|
public function getSortConfiguration(): array
|
||||||
|
{
|
||||||
|
$configuration = $this->getPersistenceManagerConfiguration();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sortByKeys' => $configuration->sortByKeys,
|
||||||
|
'sortAscending' => $configuration->sortAscending,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current request is a frontend request
|
||||||
|
*/
|
||||||
|
private function isFrontendRequest(): bool
|
||||||
|
{
|
||||||
|
$request = $this->getCurrentRequest();
|
||||||
|
|
||||||
|
if ($request !== null) {
|
||||||
|
return ApplicationType::fromRequest($request)->isFrontend();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current request from globals
|
||||||
|
*/
|
||||||
|
private function getCurrentRequest(): ?ServerRequestInterface
|
||||||
|
{
|
||||||
|
return $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\DTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormData - Complete form definition DTO
|
||||||
|
* Used for read/write operations with full form structure
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class FormData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $identifier,
|
||||||
|
public string $type,
|
||||||
|
public string $name,
|
||||||
|
public string $prototypeName,
|
||||||
|
public array $renderingOptions,
|
||||||
|
public array $finishers,
|
||||||
|
public array $renderables,
|
||||||
|
public array $variants,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
identifier: $data['identifier'] ?? '',
|
||||||
|
type: $data['type'] ?? 'Form',
|
||||||
|
name: $data['label'] ?? $data['identifier'] ?? '',
|
||||||
|
prototypeName: $data['prototypeName'] ?? 'standard',
|
||||||
|
renderingOptions: $data['renderingOptions'] ?? [],
|
||||||
|
finishers: $data['finishers'] ?? [],
|
||||||
|
renderables: $data['renderables'] ?? [],
|
||||||
|
variants: $data['variants'] ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'identifier' => $this->identifier,
|
||||||
|
'type' => $this->type,
|
||||||
|
'label' => $this->name,
|
||||||
|
'prototypeName' => $this->prototypeName,
|
||||||
|
'renderingOptions' => $this->renderingOptions,
|
||||||
|
'finishers' => $this->finishers,
|
||||||
|
'renderables' => $this->renderables,
|
||||||
|
'variants' => $this->variants,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\DTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormMetadata - Lightweight DTO for form listings
|
||||||
|
* Contains only metadata, not the full form definition
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class FormMetadata
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $identifier,
|
||||||
|
public string $type,
|
||||||
|
public string $name,
|
||||||
|
public string $prototypeName,
|
||||||
|
public ?string $persistenceIdentifier = null,
|
||||||
|
public bool $invalid = false,
|
||||||
|
public bool $readOnly = false,
|
||||||
|
public bool $removable = true,
|
||||||
|
public ?string $storageType = null,
|
||||||
|
public bool $duplicateIdentifier = false,
|
||||||
|
public ?int $fileUid = null,
|
||||||
|
public int $referenceCount = 0,
|
||||||
|
public ?string $editUrl = null,
|
||||||
|
public ?string $storageLocation = null,
|
||||||
|
public array $actions = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
identifier: $data['identifier'] ?? '',
|
||||||
|
type: $data['type'] ?? 'Form',
|
||||||
|
name: $data['label'] ?? $data['identifier'] ?? '',
|
||||||
|
prototypeName: $data['prototypeName'] ?? 'standard',
|
||||||
|
persistenceIdentifier: $data['persistenceIdentifier'] ?? null,
|
||||||
|
invalid: $data['invalid'] ?? false,
|
||||||
|
readOnly: $data['readOnly'] ?? false,
|
||||||
|
removable: $data['removable'] ?? true,
|
||||||
|
storageType: $data['storageType'] ?? null,
|
||||||
|
duplicateIdentifier: $data['duplicateIdentifier'] ?? false,
|
||||||
|
fileUid: $data['fileUid'] ?? null,
|
||||||
|
referenceCount: $data['referenceCount'] ?? 0,
|
||||||
|
editUrl: $data['editUrl'] ?? null,
|
||||||
|
storageLocation: $data['storageLocation'] ?? null,
|
||||||
|
actions: $data['actions'] ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createInvalid(
|
||||||
|
string $persistenceIdentifier,
|
||||||
|
string $errorMessage
|
||||||
|
): self {
|
||||||
|
return new self(
|
||||||
|
identifier: $persistenceIdentifier,
|
||||||
|
type: 'Form',
|
||||||
|
name: $errorMessage,
|
||||||
|
prototypeName: 'standard',
|
||||||
|
persistenceIdentifier: $persistenceIdentifier,
|
||||||
|
invalid: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createFromYaml(
|
||||||
|
array $yamlData,
|
||||||
|
string $persistenceIdentifier,
|
||||||
|
?int $fileUid = null
|
||||||
|
): self {
|
||||||
|
return self::fromArray($yamlData)
|
||||||
|
->withPersistenceIdentifier($persistenceIdentifier)
|
||||||
|
->withFileUid($fileUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'identifier' => $this->identifier,
|
||||||
|
'type' => $this->type,
|
||||||
|
'label' => $this->name,
|
||||||
|
'name' => $this->name,
|
||||||
|
'prototypeName' => $this->prototypeName,
|
||||||
|
'persistenceIdentifier' => $this->persistenceIdentifier ?? $this->identifier,
|
||||||
|
'invalid' => $this->invalid,
|
||||||
|
'readOnly' => $this->readOnly,
|
||||||
|
'removable' => $this->removable,
|
||||||
|
'storageType' => $this->storageType,
|
||||||
|
'storageLocation' => $this->storageLocation ?? $this->storageType,
|
||||||
|
'duplicateIdentifier' => $this->duplicateIdentifier,
|
||||||
|
'fileUid' => $this->fileUid,
|
||||||
|
'referenceCount' => $this->referenceCount,
|
||||||
|
'editUrl' => $this->editUrl,
|
||||||
|
'actions' => $this->actions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function with(array $changes): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
identifier: $changes['identifier'] ?? $this->identifier,
|
||||||
|
type: $changes['type'] ?? $this->type,
|
||||||
|
name: $changes['name'] ?? $this->name,
|
||||||
|
prototypeName: $changes['prototypeName'] ?? $this->prototypeName,
|
||||||
|
persistenceIdentifier: $changes['persistenceIdentifier'] ?? $this->persistenceIdentifier,
|
||||||
|
invalid: $changes['invalid'] ?? $this->invalid,
|
||||||
|
readOnly: $changes['readOnly'] ?? $this->readOnly,
|
||||||
|
removable: $changes['removable'] ?? $this->removable,
|
||||||
|
storageType: $changes['storageType'] ?? $this->storageType,
|
||||||
|
duplicateIdentifier: $changes['duplicateIdentifier'] ?? $this->duplicateIdentifier,
|
||||||
|
fileUid: $changes['fileUid'] ?? $this->fileUid,
|
||||||
|
referenceCount: $changes['referenceCount'] ?? $this->referenceCount,
|
||||||
|
editUrl: $changes['editUrl'] ?? $this->editUrl,
|
||||||
|
storageLocation: $changes['storageLocation'] ?? $this->storageLocation,
|
||||||
|
actions: $changes['actions'] ?? $this->actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPersistenceIdentifier(string $persistenceIdentifier): self
|
||||||
|
{
|
||||||
|
return $this->with(['persistenceIdentifier' => $persistenceIdentifier]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withStorageType(string $storageType): self
|
||||||
|
{
|
||||||
|
return $this->with(['storageType' => $storageType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withDuplicateIdentifier(bool $duplicateIdentifier): self
|
||||||
|
{
|
||||||
|
return $this->with(['duplicateIdentifier' => $duplicateIdentifier]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withReadOnly(bool $readOnly): self
|
||||||
|
{
|
||||||
|
return $this->with(['readOnly' => $readOnly]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withRemovable(bool $removable): self
|
||||||
|
{
|
||||||
|
return $this->with(['removable' => $removable]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withFileUid(?int $fileUid): self
|
||||||
|
{
|
||||||
|
return $this->with(['fileUid' => $fileUid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withReferenceCount(int $referenceCount): self
|
||||||
|
{
|
||||||
|
return $this->with(['referenceCount' => $referenceCount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withInvalid(bool $invalid): self
|
||||||
|
{
|
||||||
|
return $this->with(['invalid' => $invalid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withEditUrl(string $editUrl): self
|
||||||
|
{
|
||||||
|
return $this->with(['editUrl' => $editUrl]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withStorageLocation(?string $storageLocation): self
|
||||||
|
{
|
||||||
|
return $this->with(['storageLocation' => $storageLocation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withActions(array $actions): self
|
||||||
|
{
|
||||||
|
return $this->with(['actions' => $actions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a comparable scalar value for the given sort field.
|
||||||
|
*
|
||||||
|
* Field names in SearchCriteria::ORDER_FIELDS are intentionally kept
|
||||||
|
* identical to the property names of this class, so a dynamic lookup
|
||||||
|
* is sufficient. Unknown fields yield null and are skipped by the
|
||||||
|
* caller. Booleans are cast to int for correct numeric ordering.
|
||||||
|
*/
|
||||||
|
public function getSortableValue(string $field): int|string|null
|
||||||
|
{
|
||||||
|
if (!property_exists($this, $field)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$value = $this->$field;
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return (int)$value;
|
||||||
|
}
|
||||||
|
return is_int($value) || is_string($value) ? $value : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\DTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed representation of the "persistenceManager" section of the form
|
||||||
|
* YAML configuration.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class PersistenceManagerConfiguration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
public const DEFAULT_SORT_BY_KEYS = ['name', 'fileUid'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $sortByKeys Keys the forms are sorted by in the form manager and plugin select
|
||||||
|
* @param list<string> $allowedExtensionPaths EXT: paths that contain forms shipped within extensions
|
||||||
|
* @param list<string> $allowedFileMounts File mounts forms may be stored in
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public bool $allowSaveToExtensionPaths = false,
|
||||||
|
public bool $allowDeleteFromExtensionPaths = false,
|
||||||
|
public array $sortByKeys = self::DEFAULT_SORT_BY_KEYS,
|
||||||
|
public bool $sortAscending = true,
|
||||||
|
public array $allowedExtensionPaths = [],
|
||||||
|
public array $allowedFileMounts = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the DTO from the raw "persistenceManager" configuration array.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $configuration
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $configuration): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
allowSaveToExtensionPaths: (bool)($configuration['allowSaveToExtensionPaths'] ?? false),
|
||||||
|
allowDeleteFromExtensionPaths: (bool)($configuration['allowDeleteFromExtensionPaths'] ?? false),
|
||||||
|
sortByKeys: self::normalizeStringList($configuration['sortByKeys'] ?? null, self::DEFAULT_SORT_BY_KEYS),
|
||||||
|
sortAscending: (bool)($configuration['sortAscending'] ?? true),
|
||||||
|
allowedExtensionPaths: self::normalizeStringList($configuration['allowedExtensionPaths'] ?? null, []),
|
||||||
|
allowedFileMounts: self::normalizeStringList($configuration['allowedFileMounts'] ?? null, []),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a configuration value into a numerically indexed list of strings.
|
||||||
|
*
|
||||||
|
* The YAML configuration may use associative keys (e.g. `10:`, `20:`) to
|
||||||
|
* define ordering, so values are cast to strings and re-indexed.
|
||||||
|
*
|
||||||
|
* @param list<string> $default
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function normalizeStringList(mixed $value, array $default): array
|
||||||
|
{
|
||||||
|
if (!is_array($value)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_map(strval(...), $value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\DTO;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search criteria for filtering and sorting form lists
|
||||||
|
*
|
||||||
|
* Follows TYPO3 Demand pattern naming conventions:
|
||||||
|
* - searchTerm: Text to search for in form properties
|
||||||
|
* - orderField: Field name to sort by
|
||||||
|
* - orderDirection: Sort direction ('asc' or 'desc')
|
||||||
|
* - limit: Maximum number of results
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class SearchCriteria
|
||||||
|
{
|
||||||
|
private const string ORDER_ASCENDING = 'asc';
|
||||||
|
private const string ORDER_DESCENDING = 'desc';
|
||||||
|
private const string DEFAULT_ORDER_FIELD = 'name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed sort fields. Each entry MUST match a public property name of
|
||||||
|
* FormMetadata exactly, because FormMetadata::getSortableValue() uses
|
||||||
|
* dynamic property access ($this->$field) instead of an explicit mapping.
|
||||||
|
*/
|
||||||
|
private const array ORDER_FIELDS = ['name', 'identifier', 'persistenceIdentifier', 'prototypeName', 'storageLocation', 'duplicateIdentifier', 'referenceCount'];
|
||||||
|
|
||||||
|
public string $orderField;
|
||||||
|
public string $orderDirection;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public ?string $searchTerm = null,
|
||||||
|
?string $orderField = null,
|
||||||
|
?string $orderDirection = null,
|
||||||
|
public ?int $limit = null,
|
||||||
|
) {
|
||||||
|
// Validate and normalize orderField
|
||||||
|
$this->orderField = in_array($orderField, self::ORDER_FIELDS, true)
|
||||||
|
? $orderField
|
||||||
|
: self::DEFAULT_ORDER_FIELD;
|
||||||
|
|
||||||
|
// Validate and normalize orderDirection
|
||||||
|
$this->orderDirection = in_array($orderDirection, [self::ORDER_ASCENDING, self::ORDER_DESCENDING], true)
|
||||||
|
? $orderDirection
|
||||||
|
: self::ORDER_ASCENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
searchTerm: $data['searchTerm'] ?? null,
|
||||||
|
orderField: $data['orderField'] ?? null,
|
||||||
|
orderDirection: $data['orderDirection'] ?? null,
|
||||||
|
limit: $data['limit'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRequest(ServerRequestInterface $request): self
|
||||||
|
{
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
$parsedBody = $request->getParsedBody() ?? [];
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
searchTerm: $queryParams['searchTerm'] ?? $parsedBody['searchTerm'] ?? null,
|
||||||
|
orderField: $queryParams['orderField'] ?? $parsedBody['orderField'] ?? null,
|
||||||
|
orderDirection: $queryParams['orderDirection'] ?? $parsedBody['orderDirection'] ?? null,
|
||||||
|
limit: isset($queryParams['limit']) ? (int)$queryParams['limit'] : (isset($parsedBody['limit']) ? (int)$parsedBody['limit'] : null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrderField(): string
|
||||||
|
{
|
||||||
|
return $this->orderField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrderDirection(): string
|
||||||
|
{
|
||||||
|
return $this->orderDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultOrderDirection(): string
|
||||||
|
{
|
||||||
|
return self::ORDER_ASCENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReverseOrderDirection(): string
|
||||||
|
{
|
||||||
|
return $this->orderDirection === self::ORDER_ASCENDING
|
||||||
|
? self::ORDER_DESCENDING
|
||||||
|
: self::ORDER_ASCENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSearchTerm(): ?string
|
||||||
|
{
|
||||||
|
return $this->searchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasSearchTerm(): bool
|
||||||
|
{
|
||||||
|
return $this->searchTerm !== null && $this->searchTerm !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLimit(): ?int
|
||||||
|
{
|
||||||
|
return $this->limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasLimit(): bool
|
||||||
|
{
|
||||||
|
return $this->limit !== null && $this->limit > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any filter/search constraints are set
|
||||||
|
*/
|
||||||
|
public function hasConstraints(): bool
|
||||||
|
{
|
||||||
|
return $this->hasSearchTerm() || $this->hasLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameters(): array
|
||||||
|
{
|
||||||
|
$parameters = [];
|
||||||
|
if ($this->hasSearchTerm()) {
|
||||||
|
$parameters['searchTerm'] = $this->searchTerm;
|
||||||
|
}
|
||||||
|
if ($this->hasLimit()) {
|
||||||
|
$parameters['limit'] = $this->limit;
|
||||||
|
}
|
||||||
|
$parameters['orderField'] = $this->orderField;
|
||||||
|
$parameters['orderDirection'] = $this->orderDirection;
|
||||||
|
return $parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\DTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage context for form persistence operations
|
||||||
|
* Contains additional metadata required for storing forms
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class StorageContext
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $pid = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function create(?int $pid = null): self
|
||||||
|
{
|
||||||
|
return new self($pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Exception as FormException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic Form domain Exception
|
||||||
|
*/
|
||||||
|
class Exception extends FormException {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown if a rendering error occurs
|
||||||
|
*/
|
||||||
|
class RenderingException extends Exception {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
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,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
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,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
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,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Factory;
|
||||||
|
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
|
||||||
|
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
|
||||||
|
use TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for custom *Form Factories*. A Form Factory is responsible for building
|
||||||
|
* a {@link TYPO3\CMS\Form\Domain\Model\FormDefinition}.
|
||||||
|
*
|
||||||
|
* Example
|
||||||
|
* =======
|
||||||
|
*
|
||||||
|
* Generally, you should use this class as follows:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* class MyFooBarFactory extends AbstractFormFactory {
|
||||||
|
* public function build(array $configuration, $prototypeName) {
|
||||||
|
* $configurationService = GeneralUtility::makeInstance(ConfigurationService::class);
|
||||||
|
* $prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
|
||||||
|
* $formDefinition = GeneralUtility::makeInstance(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
|
||||||
|
{
|
||||||
|
protected ?EventDispatcherInterface $eventDispatcher = null;
|
||||||
|
protected ?FormDefinitionConversionService $formDefinitionConversionService = null;
|
||||||
|
|
||||||
|
public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||||
|
{
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function injectFormDefinitionConversionService(FormDefinitionConversionService $formDefinitionConversionService): void
|
||||||
|
{
|
||||||
|
$this->formDefinitionConversionService = $formDefinitionConversionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function triggerFormBuildingFinished(FormDefinition $form): FormDefinition
|
||||||
|
{
|
||||||
|
return $this->eventDispatcher->dispatch(new AfterFormIsBuiltEvent($form))->form;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormDefinitionConversionService(): FormDefinitionConversionService
|
||||||
|
{
|
||||||
|
return $this->formDefinitionConversionService;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Factory;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
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;
|
||||||
|
use TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory that creates a FormDefinition from an array
|
||||||
|
*
|
||||||
|
* Scope: frontend / backend
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true, shared: false)]
|
||||||
|
class ArrayFormFactory extends AbstractFormFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Build a form definition, depending on some configuration.
|
||||||
|
*
|
||||||
|
* @throws RenderingException
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function build(
|
||||||
|
array $configuration,
|
||||||
|
?string $prototypeName = null,
|
||||||
|
?ServerRequestInterface $request = null
|
||||||
|
): FormDefinition {
|
||||||
|
if (empty($prototypeName)) {
|
||||||
|
$prototypeName = $configuration['prototypeName'] ?? 'standard';
|
||||||
|
}
|
||||||
|
$persistenceIdentifier = $configuration['persistenceIdentifier'] ?? null;
|
||||||
|
|
||||||
|
// Get prototype configuration once and reuse it
|
||||||
|
$prototypeConfiguration = GeneralUtility::makeInstance(ConfigurationService::class)
|
||||||
|
->getPrototypeConfiguration($prototypeName);
|
||||||
|
|
||||||
|
// Get RTE property paths for proper sanitization
|
||||||
|
$rtePropertyPaths = $this->getFormDefinitionConversionService()->extractRtePropertyPaths($prototypeConfiguration);
|
||||||
|
|
||||||
|
$configuration = $this->getFormDefinitionConversionService()->sanitizeHtml($configuration, $rtePropertyPaths);
|
||||||
|
|
||||||
|
if ($configuration['invalid'] ?? false) {
|
||||||
|
throw new RenderingException($configuration['label'], 1529710560);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = GeneralUtility::makeInstance(
|
||||||
|
FormDefinition::class,
|
||||||
|
$configuration['identifier'],
|
||||||
|
$prototypeConfiguration,
|
||||||
|
'Form',
|
||||||
|
$persistenceIdentifier
|
||||||
|
);
|
||||||
|
// Set renderingOptions before processing renderables, so that options
|
||||||
|
// like 'previewMode' are available during initializeFormElement().
|
||||||
|
if (isset($configuration['renderingOptions'])) {
|
||||||
|
foreach ($configuration['renderingOptions'] as $key => $value) {
|
||||||
|
$form->setRenderingOption($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($configuration['renderables'])) {
|
||||||
|
foreach ($configuration['renderables'] as $pageConfiguration) {
|
||||||
|
$this->addNestedRenderable($pageConfiguration, $form, $request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($configuration['persistenceIdentifier']);
|
||||||
|
unset($configuration['prototypeName']);
|
||||||
|
unset($configuration['renderables']);
|
||||||
|
unset($configuration['type']);
|
||||||
|
unset($configuration['identifier']);
|
||||||
|
$form->setOptions($configuration);
|
||||||
|
$form->setRequest($request);
|
||||||
|
|
||||||
|
return $this->triggerFormBuildingFinished($form);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add form elements to the $parentRenderable
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
* @throws IdentifierNotValidException
|
||||||
|
* @throws UnknownCompositRenderableException
|
||||||
|
*/
|
||||||
|
protected function addNestedRenderable(
|
||||||
|
array $nestedRenderableConfiguration,
|
||||||
|
CompositeRenderableInterface $parentRenderable,
|
||||||
|
?ServerRequestInterface $request = null
|
||||||
|
) {
|
||||||
|
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']);
|
||||||
|
if ($request !== null && method_exists($renderable, 'setRequest')) {
|
||||||
|
$renderable->setRequest($request);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new UnknownCompositRenderableException('Unknown composit renderable "' . get_class($parentRenderable) . '"', 1479593622);
|
||||||
|
}
|
||||||
|
|
||||||
|
$childRenderables = is_array($nestedRenderableConfiguration['renderables'] ?? null)
|
||||||
|
? $nestedRenderableConfiguration['renderables']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
unset($nestedRenderableConfiguration['type']);
|
||||||
|
unset($nestedRenderableConfiguration['identifier']);
|
||||||
|
unset($nestedRenderableConfiguration['renderables']);
|
||||||
|
|
||||||
|
$renderable->setOptions($nestedRenderableConfiguration);
|
||||||
|
|
||||||
|
if ($renderable instanceof CompositeRenderableInterface) {
|
||||||
|
foreach ($childRenderables as $elementConfiguration) {
|
||||||
|
$this->addNestedRenderable($elementConfiguration, $renderable, $request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->eventDispatcher->dispatch(new BeforeRenderableIsAddedToFormEvent($renderable))->renderable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Factory;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
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.
|
||||||
|
* @param ServerRequestInterface $request The PSR-7 request object
|
||||||
|
* @return FormDefinition a newly built form definition
|
||||||
|
*/
|
||||||
|
public function build(
|
||||||
|
array $configuration,
|
||||||
|
?string $prototypeName = null,
|
||||||
|
?ServerRequestInterface $request = null
|
||||||
|
): FormDefinition;
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerAwareInterface;
|
||||||
|
use Psr\Log\LoggerAwareTrait;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||||
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||||
|
use TYPO3\CMS\Form\Domain\Model\FormElements\StringableFormElementInterface;
|
||||||
|
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
|
||||||
|
use TYPO3\CMS\Form\Service\TranslationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finisher base class.
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
* **This class is meant to be sub classed by developers**
|
||||||
|
*/
|
||||||
|
abstract class AbstractFinisher implements FinisherInterface, LoggerAwareInterface
|
||||||
|
{
|
||||||
|
use LoggerAwareTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 FinisherContext
|
||||||
|
*/
|
||||||
|
protected $finisherContext;
|
||||||
|
|
||||||
|
private ViewFactoryInterface $viewFactory;
|
||||||
|
|
||||||
|
private TranslationService $translationService;
|
||||||
|
|
||||||
|
public function injectViewFactory(ViewFactoryInterface $viewFactory)
|
||||||
|
{
|
||||||
|
$this->viewFactory = $viewFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function injectTranslationService(TranslationService $translationService)
|
||||||
|
{
|
||||||
|
$this->translationService = $translationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $finisherIdentifier The identifier for this finisher
|
||||||
|
*/
|
||||||
|
public function setFinisherIdentifier(string $finisherIdentifier): void
|
||||||
|
{
|
||||||
|
$this->finisherIdentifier = $finisherIdentifier;
|
||||||
|
$this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->executeInternal();
|
||||||
|
} catch (FinisherException $e) {
|
||||||
|
$this->logger->error('Failed to execute finisher', ['exception' => $e]);
|
||||||
|
$this->finisherContext->cancel();
|
||||||
|
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||||
|
$renderingOptions = $formRuntime->getRenderingOptions();
|
||||||
|
$viewFactoryData = new ViewFactoryData(
|
||||||
|
templateRootPaths: is_array($renderingOptions['templateRootPaths'] ?? null) ? $renderingOptions['templateRootPaths'] : [],
|
||||||
|
partialRootPaths: is_array($renderingOptions['partialRootPaths'] ?? null) ? $renderingOptions['partialRootPaths'] : [],
|
||||||
|
layoutRootPaths: is_array($renderingOptions['layoutRootPaths'] ?? null) ? $renderingOptions['layoutRootPaths'] : [],
|
||||||
|
request: $this->finisherContext->getRequest(),
|
||||||
|
);
|
||||||
|
$view = $this->viewFactory->create($viewFactoryData);
|
||||||
|
$message = $this->parseOption('errorMessage') ?: $this->translationService->translate('form.finisher.error', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
|
||||||
|
$view->assign('message', $message);
|
||||||
|
return $view->render('Finishers/Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called in the concrete finisher whenever self::execute() is called.
|
||||||
|
*
|
||||||
|
* Override and fill with your own implementation!
|
||||||
|
*
|
||||||
|
* @throws FinisherException
|
||||||
|
* @return string|void|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|int|bool|\Closure|callable|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 = is_array($this->options['translation'] ?? null)
|
||||||
|
? $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|array $optionValue
|
||||||
|
* @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 $this->translationService->translateFinisherOption(
|
||||||
|
$formRuntime,
|
||||||
|
$this->finisherIdentifier,
|
||||||
|
$optionName,
|
||||||
|
$optionValue,
|
||||||
|
$translationOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can encapsulate an option value with {}.
|
||||||
|
* This enables you to access every gettable property from the
|
||||||
|
* TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
|
||||||
|
*
|
||||||
|
* For example: {formState.formValues.<elementIdentifier>}
|
||||||
|
* or {<elementIdentifier>}
|
||||||
|
*
|
||||||
|
* Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elementIdentifier>]"
|
||||||
|
* 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)) {
|
||||||
|
$substitutedNeedle = [];
|
||||||
|
foreach ($needle as $key => $item) {
|
||||||
|
$key = $this->substituteRuntimeReferences($key, $formRuntime);
|
||||||
|
$item = $this->substituteRuntimeReferences($item, $formRuntime);
|
||||||
|
$substitutedNeedle[$key] = $item;
|
||||||
|
}
|
||||||
|
return $substitutedNeedle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
*
|
||||||
|
* @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 (is_object($value)) {
|
||||||
|
$element = $formRuntime->getFormDefinition()->getElementByIdentifier($property);
|
||||||
|
|
||||||
|
if (!$element instanceof StringableFormElementInterface) {
|
||||||
|
throw new FinisherException(
|
||||||
|
sprintf('Cannot convert object value of "%s" to string', $property),
|
||||||
|
1574362327
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $element->valueToString($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple finisher that invokes a closure when executed
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* //...
|
||||||
|
* $closureFinisher = GeneralUtility::makeInstance(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()
|
||||||
|
{
|
||||||
|
$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,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||||
|
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||||
|
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||||
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||||
|
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 = GeneralUtility::makeInstance(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',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
|
||||||
|
private readonly ViewFactoryInterface $viewFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws FinisherException
|
||||||
|
*/
|
||||||
|
protected function executeInternal(): string
|
||||||
|
{
|
||||||
|
$options = $this->options;
|
||||||
|
if (!isset($options['templateName']) || !is_string($options['templateName'])) {
|
||||||
|
throw new FinisherException(
|
||||||
|
'The option "templateName" must be set for the ConfirmationFinisher.',
|
||||||
|
1521573955
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentElementUid = $this->parseOption('contentElementUid');
|
||||||
|
$typoscriptObjectPath = $this->parseOption('typoscriptObjectPath');
|
||||||
|
$typoscriptObjectPath = is_string($typoscriptObjectPath) ? $typoscriptObjectPath : '';
|
||||||
|
if (!empty($contentElementUid)) {
|
||||||
|
$pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath);
|
||||||
|
$lastSegment = array_pop($pathSegments);
|
||||||
|
$setup = $this->extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
|
||||||
|
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 . '.'];
|
||||||
|
}
|
||||||
|
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||||
|
$contentObjectRenderer->setRequest($this->finisherContext->getRequest()->withoutAttribute('extbase'));
|
||||||
|
$contentObjectRenderer->start([$contentElementUid]);
|
||||||
|
$contentObjectRenderer->setCurrentVal((string)$contentElementUid);
|
||||||
|
$message = $contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'], $lastSegment);
|
||||||
|
} else {
|
||||||
|
$message = $this->parseOption('message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||||
|
$viewFactoryData = new ViewFactoryData(
|
||||||
|
templateRootPaths: is_array($options['templateRootPaths'] ?? null) ? $options['templateRootPaths'] : [],
|
||||||
|
partialRootPaths: is_array($options['partialRootPaths'] ?? null) ? $options['partialRootPaths'] : [],
|
||||||
|
layoutRootPaths: is_array($options['layoutRootPaths'] ?? null) ? $options['layoutRootPaths'] : [],
|
||||||
|
request: $this->finisherContext->getRequest(),
|
||||||
|
);
|
||||||
|
$view = $this->viewFactory->create($viewFactoryData);
|
||||||
|
if ($view instanceof FluidViewAdapter) {
|
||||||
|
$view->getRenderingContext()->getViewHelperVariableContainer()
|
||||||
|
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
|
||||||
|
}
|
||||||
|
if (is_array($this->options['variables'] ?? null)) {
|
||||||
|
$view->assignMultiple($this->options['variables']);
|
||||||
|
}
|
||||||
|
$view->assignMultiple([
|
||||||
|
'form' => $formRuntime,
|
||||||
|
'finisherVariableProvider' => $this->finisherContext->getFinisherVariableProvider(),
|
||||||
|
'message' => $message,
|
||||||
|
'isPreparedMessage' => !empty($contentElementUid),
|
||||||
|
]);
|
||||||
|
return $view->render($options['templateName']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Resource\FileReference;
|
||||||
|
use TYPO3\CMS\Core\Resource\Folder;
|
||||||
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference;
|
||||||
|
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||||
|
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This finisher remove the submitted 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 ExtbaseFileReference) {
|
||||||
|
$file = $file->getOriginalResource();
|
||||||
|
}
|
||||||
|
if ($file instanceof FileReference) {
|
||||||
|
$this->deleteFileAndCollectFolder($file, $uploadFolders);
|
||||||
|
} elseif ($file instanceof ObjectStorage) {
|
||||||
|
foreach ($file as $singleFile) {
|
||||||
|
if ($singleFile instanceof ExtbaseFileReference) {
|
||||||
|
$singleFile = $singleFile->getOriginalResource();
|
||||||
|
}
|
||||||
|
if ($singleFile instanceof FileReference) {
|
||||||
|
$this->deleteFileAndCollectFolder($singleFile, $uploadFolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->deleteEmptyUploadFolders($uploadFolders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the file and collects its parent folder for later cleanup.
|
||||||
|
*
|
||||||
|
* @param array<string, Folder> $uploadFolders
|
||||||
|
*/
|
||||||
|
private function deleteFileAndCollectFolder(FileReference $file, array &$uploadFolders): void
|
||||||
|
{
|
||||||
|
$folder = $file->getParentFolder();
|
||||||
|
if ($folder instanceof Folder) {
|
||||||
|
$uploadFolders[$folder->getCombinedIdentifier()] = $folder;
|
||||||
|
}
|
||||||
|
$file->getStorage()->deleteFile($file->getOriginalFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isEmptyFolder(Folder $folder): bool
|
||||||
|
{
|
||||||
|
return $folder->getFileCount() === 0
|
||||||
|
&& $folder->getStorage()->countFoldersInFolder($folder) === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use TYPO3\CMS\Core\Mail\FluidEmail;
|
||||||
|
use TYPO3\CMS\Core\Mail\MailerInterface;
|
||||||
|
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
|
||||||
|
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||||
|
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||||
|
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\Event\BeforeEmailFinisherInitializedEvent;
|
||||||
|
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This finisher sends an email to one recipient
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
*
|
||||||
|
* - templateName (mandatory): Template name for the mail body
|
||||||
|
* - templateRootPaths: root paths for the templates
|
||||||
|
* - layoutRootPaths: root paths for the layouts
|
||||||
|
* - partialRootPaths: root paths 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 senderAddress
|
||||||
|
* makes the recipient address configurable.
|
||||||
|
*
|
||||||
|
* - subject (mandatory): Subject of the email
|
||||||
|
* - recipients (mandatory): Email addresses and human-readable names of the recipients
|
||||||
|
* - senderAddress (mandatory): Email address of the sender
|
||||||
|
* - senderName: Human-readable name of the sender
|
||||||
|
* - replyToRecipients: Email addresses and human-readable names of the reply-to recipients
|
||||||
|
* - carbonCopyRecipients: Email addresses and human-readable names of the copy recipients
|
||||||
|
* - blindCarbonCopyRecipients: Email addresses and human-readable names of the blind copy recipients
|
||||||
|
* - title: The title of the email - If not set "subject" is used by default
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class EmailFinisher extends AbstractFinisher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $defaultOptions = [
|
||||||
|
'recipientName' => '',
|
||||||
|
'senderName' => '',
|
||||||
|
'addHtmlPart' => true,
|
||||||
|
'attachUploads' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||||
|
protected readonly TemplatedEmailFactory $templatedEmailFactory,
|
||||||
|
protected readonly MailerInterface $mailer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes this finisher
|
||||||
|
* @see AbstractFinisher::execute()
|
||||||
|
*
|
||||||
|
* @throws FinisherException
|
||||||
|
*/
|
||||||
|
protected function executeInternal(): void
|
||||||
|
{
|
||||||
|
$this->options = $this->eventDispatcher
|
||||||
|
->dispatch(new BeforeEmailFinisherInitializedEvent($this->finisherContext, $this->options))
|
||||||
|
->getOptions();
|
||||||
|
// Flexform overrides write strings instead of integers so
|
||||||
|
// we need to cast the string '0' to false.
|
||||||
|
if (
|
||||||
|
isset($this->options['addHtmlPart'])
|
||||||
|
&& $this->options['addHtmlPart'] === '0'
|
||||||
|
) {
|
||||||
|
$this->options['addHtmlPart'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = (string)$this->parseOption('subject');
|
||||||
|
$recipients = $this->getRecipients('recipients');
|
||||||
|
$senderAddress = $this->parseOption('senderAddress');
|
||||||
|
$senderAddress = is_string($senderAddress) ? $senderAddress : '';
|
||||||
|
$senderName = $this->parseOption('senderName');
|
||||||
|
$senderName = is_string($senderName) ? $senderName : '';
|
||||||
|
$replyToRecipients = $this->getRecipients('replyToRecipients');
|
||||||
|
$carbonCopyRecipients = $this->getRecipients('carbonCopyRecipients');
|
||||||
|
$blindCarbonCopyRecipients = $this->getRecipients('blindCarbonCopyRecipients');
|
||||||
|
$addHtmlPart = (bool)$this->parseOption('addHtmlPart');
|
||||||
|
$attachUploads = $this->parseOption('attachUploads');
|
||||||
|
$title = (string)$this->parseOption('title') ?: $subject;
|
||||||
|
|
||||||
|
if ($subject === '') {
|
||||||
|
throw new FinisherException('The option "subject" must be set for the EmailFinisher.', 1327060320);
|
||||||
|
}
|
||||||
|
if (empty($recipients)) {
|
||||||
|
throw new FinisherException('The option "recipients" must be set for the EmailFinisher.', 1327060200);
|
||||||
|
}
|
||||||
|
if (empty($senderAddress)) {
|
||||||
|
throw new FinisherException('The option "senderAddress" must be set for the EmailFinisher.', 1327060210);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||||
|
|
||||||
|
$mail = $this
|
||||||
|
->initializeFluidEmail($formRuntime)
|
||||||
|
->from(new Address($senderAddress, $senderName))
|
||||||
|
->to(...$recipients)
|
||||||
|
->subject($subject)
|
||||||
|
->format($addHtmlPart ? FluidEmail::FORMAT_BOTH : FluidEmail::FORMAT_PLAIN)
|
||||||
|
->assign('title', $title);
|
||||||
|
|
||||||
|
if (!empty($replyToRecipients)) {
|
||||||
|
$mail->replyTo(...$replyToRecipients);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($carbonCopyRecipients)) {
|
||||||
|
$mail->cc(...$carbonCopyRecipients);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($blindCarbonCopyRecipients)) {
|
||||||
|
$mail->bcc(...$blindCarbonCopyRecipients);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($this->options['translation']['language'] ?? null) && $this->options['translation']['language'] !== '') {
|
||||||
|
$mail->assign('languageKey', $this->options['translation']['language']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $this->parseOption('message');
|
||||||
|
if (is_string($message) && $message !== '') {
|
||||||
|
// Remove whitespace between HTML tags to prevent lib.parseFunc_RTE
|
||||||
|
// from converting newlines into additional blank lines in the email output
|
||||||
|
$message = preg_replace('/>\s+</', '><', $message);
|
||||||
|
$placeholderPos = strpos($message, '{formValues}');
|
||||||
|
if ($placeholderPos !== false) {
|
||||||
|
$mail->assign('messageBefore', substr($message, 0, $placeholderPos));
|
||||||
|
$mail->assign('messageAfter', substr($message, $placeholderPos + strlen('{formValues}')));
|
||||||
|
} else {
|
||||||
|
// No placeholder - show message only, no form values
|
||||||
|
$mail->assign('messageBefore', $message);
|
||||||
|
$mail->assign('messageAfter', '');
|
||||||
|
$mail->assign('hideFormValues', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($attachUploads) {
|
||||||
|
foreach ($formRuntime->getFormDefinition()->getRenderablesRecursively() as $element) {
|
||||||
|
if (!$element instanceof FileUpload) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$file = $formRuntime[$element->getIdentifier()];
|
||||||
|
if ($file instanceof FileReference) {
|
||||||
|
$file = $file->getOriginalResource();
|
||||||
|
}
|
||||||
|
if ($file instanceof FileInterface) {
|
||||||
|
$mail->attach($file->getContents(), $file->getName(), $file->getMimeType());
|
||||||
|
} elseif ($file instanceof ObjectStorage) {
|
||||||
|
foreach ($file as $singleFile) {
|
||||||
|
if ($singleFile instanceof FileReference) {
|
||||||
|
$singleFile = $singleFile->getOriginalResource();
|
||||||
|
}
|
||||||
|
if ($singleFile instanceof FileInterface) {
|
||||||
|
$mail->attach($singleFile->getContents(), $singleFile->getName(), $singleFile->getMimeType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->mailer->send($mail);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
throw new FinisherException(
|
||||||
|
'Failed to send the email: ' . $e->getMessage(),
|
||||||
|
1754047320,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initializeFluidEmail(FormRuntime $formRuntime): FluidEmail
|
||||||
|
{
|
||||||
|
$mailMessage = $this->templatedEmailFactory->createWithOverrides(
|
||||||
|
$this->options['templateRootPaths'] ?? [],
|
||||||
|
$this->options['layoutRootPaths'] ?? [],
|
||||||
|
$this->options['partialRootPaths'] ?? [],
|
||||||
|
$this->finisherContext->getRequest(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($this->options['templateName']) || $this->options['templateName'] === '') {
|
||||||
|
throw new FinisherException('The option "templateName" must be set to use FluidEmail.', 1599834020);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate old template name to default FluidEmail name
|
||||||
|
if ($this->options['templateName'] === '{@format}.html') {
|
||||||
|
$this->options['templateName'] = 'Default';
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailMessage
|
||||||
|
->setTemplate($this->options['templateName'])
|
||||||
|
->assignMultiple([
|
||||||
|
'finisherVariableProvider' => $this->finisherContext->getFinisherVariableProvider(),
|
||||||
|
'form' => $formRuntime,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_array($this->options['variables'] ?? null)) {
|
||||||
|
$mailMessage->assignMultiple($this->options['variables']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailMessage
|
||||||
|
->getViewHelperVariableContainer()
|
||||||
|
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
|
||||||
|
|
||||||
|
return $mailMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRecipients(string $listOption): array
|
||||||
|
{
|
||||||
|
$recipients = $this->parseOption($listOption) ?? [];
|
||||||
|
if (!is_array($recipients) || $recipients === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$addresses = [];
|
||||||
|
foreach ($recipients as $address => $name) {
|
||||||
|
// The if is needed to set address and name with TypoScript
|
||||||
|
if (MathUtility::canBeInterpretedAsInteger($address)) {
|
||||||
|
if (is_array($name)) {
|
||||||
|
$address = $name[0] ?? '';
|
||||||
|
$name = $name[1] ?? '';
|
||||||
|
} else {
|
||||||
|
$address = $name;
|
||||||
|
$name = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$address = trim((string)$address);
|
||||||
|
|
||||||
|
if (!GeneralUtility::validEmail($address)) {
|
||||||
|
// Drop entries without a valid address
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$addresses[] = new Address($address, $name);
|
||||||
|
}
|
||||||
|
return $addresses;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers\Exception;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Form\Domain\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown in Form Finishers
|
||||||
|
*/
|
||||||
|
class FinisherException extends Exception {}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||||
|
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 the finisher belongs to
|
||||||
|
*/
|
||||||
|
protected FormRuntime $formRuntime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The assigned controller context which might be needed by the finisher.
|
||||||
|
*/
|
||||||
|
protected FinisherVariableProvider $finisherVariableProvider;
|
||||||
|
|
||||||
|
private Request $request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct(FormRuntime $formRuntime, Request $request)
|
||||||
|
{
|
||||||
|
$this->formRuntime = $formRuntime;
|
||||||
|
$this->request = $request;
|
||||||
|
$this->finisherVariableProvider = new FinisherVariableProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isCancelled(): bool
|
||||||
|
{
|
||||||
|
return $this->cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Form Runtime that is associated with the current finisher
|
||||||
|
*/
|
||||||
|
public function getFormRuntime(): FormRuntime
|
||||||
|
{
|
||||||
|
return $this->formRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The values of the submitted form (after validation and property mapping)
|
||||||
|
*/
|
||||||
|
public function getFormValues(): array
|
||||||
|
{
|
||||||
|
return $this->formRuntime->getFormState()->getFormValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFinisherVariableProvider(): FinisherVariableProvider
|
||||||
|
{
|
||||||
|
return $this->finisherVariableProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequest(): Request
|
||||||
|
{
|
||||||
|
return $this->request;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
public function setFinisherIdentifier(string $finisherIdentifier): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public function isEnabled(): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private $objects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a variable to the finisher container.
|
||||||
|
*
|
||||||
|
* @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 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 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
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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 https://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(mixed $offset): bool
|
||||||
|
{
|
||||||
|
return isset($this->objects[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to retrieve
|
||||||
|
*
|
||||||
|
* @link https://php.net/manual/en/arrayaccess.offsetget.php
|
||||||
|
* @param mixed $offset The offset to retrieve.
|
||||||
|
* @return mixed Can return all value types.
|
||||||
|
*/
|
||||||
|
public function offsetGet(mixed $offset): mixed
|
||||||
|
{
|
||||||
|
return $this->objects[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to set
|
||||||
|
*
|
||||||
|
* @link https://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(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->objects[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to unset
|
||||||
|
*
|
||||||
|
* @link https://php.net/manual/en/arrayaccess.offsetunset.php
|
||||||
|
* @param mixed $offset The offset to unset.
|
||||||
|
*/
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
unset($this->objects[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator(): \Traversable
|
||||||
|
{
|
||||||
|
foreach ($this->objects as $offset => $value) {
|
||||||
|
yield $offset => $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count elements of an object
|
||||||
|
*
|
||||||
|
* @link https://php.net/manual/en/countable.count.php
|
||||||
|
* @return int The custom count as an integer.
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->objects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||||
|
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\Extbase\Service\ExtensionService;
|
||||||
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple finisher that adds a message to the FlashMessageContainer
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* //...
|
||||||
|
* $flashMessageFinisher = GeneralUtility::makeInstance(FlashMessageFinisher::class);
|
||||||
|
* $flashMessageFinisher->setOptions(
|
||||||
|
* [
|
||||||
|
* 'messageBody' => 'Some message body',
|
||||||
|
* 'messageTitle' => 'Some message title',
|
||||||
|
* 'messageArguments' => ['foo' => 'bar'],
|
||||||
|
* 'severity' => \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
* $formDefinition->addFinisher($flashMessageFinisher);
|
||||||
|
* // ...
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class FlashMessageFinisher extends AbstractFinisher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $defaultOptions = [
|
||||||
|
'messageBody' => null,
|
||||||
|
'messageTitle' => '',
|
||||||
|
'messageArguments' => [],
|
||||||
|
'messageCode' => null,
|
||||||
|
'severity' => ContextualFeedbackSeverity::OK,
|
||||||
|
];
|
||||||
|
|
||||||
|
private ExtensionService $extensionService;
|
||||||
|
private FlashMessageService $flashMessageService;
|
||||||
|
|
||||||
|
public function injectFlashMessageService(FlashMessageService $flashMessageService): void
|
||||||
|
{
|
||||||
|
$this->flashMessageService = $flashMessageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function injectExtensionService(ExtensionService $extensionService): void
|
||||||
|
{
|
||||||
|
$this->extensionService = $extensionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
|
||||||
|
if (MathUtility::canBeInterpretedAsInteger($severity)) {
|
||||||
|
$severity = ContextualFeedbackSeverity::tryFrom((int)$severity);
|
||||||
|
}
|
||||||
|
if (!$severity instanceof ContextualFeedbackSeverity) {
|
||||||
|
$severity = $this->defaultOptions['severity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageClass = match ($severity) {
|
||||||
|
ContextualFeedbackSeverity::NOTICE => Notice::class,
|
||||||
|
ContextualFeedbackSeverity::WARNING => Warning::class,
|
||||||
|
ContextualFeedbackSeverity::ERROR => Error::class,
|
||||||
|
default => Message::class,
|
||||||
|
};
|
||||||
|
/** @var Message|Notice|Warning|Error $message */
|
||||||
|
$message = GeneralUtility::makeInstance($messageClass, $messageBody, $messageCode, $messageArguments, $messageTitle);
|
||||||
|
$flashMessage = new FlashMessage(
|
||||||
|
$message->render(),
|
||||||
|
$message->getTitle(),
|
||||||
|
$severity,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// todo: this value has to be taken from the request directly in the future
|
||||||
|
$pluginNamespace = $this->extensionService->getPluginNamespace(
|
||||||
|
$this->finisherContext->getRequest()->getControllerExtensionName(),
|
||||||
|
$this->finisherContext->getRequest()->getPluginName()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->flashMessageService->getMessageQueueByIdentifier('extbase.flashmessages.' . $pluginNamespace)->addMessage($flashMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||||
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This finisher redirects to another Controller.
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class RedirectFinisher extends AbstractFinisher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $defaultOptions = [
|
||||||
|
'pageUid' => 1,
|
||||||
|
'additionalParameters' => '',
|
||||||
|
'statusCode' => 303,
|
||||||
|
'fragment' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes this finisher
|
||||||
|
* @see AbstractFinisher::execute()
|
||||||
|
*/
|
||||||
|
protected function executeInternal(): void
|
||||||
|
{
|
||||||
|
$pageUid = $this->parseOption('pageUid');
|
||||||
|
$pageUid = (int)str_replace('pages_', '', (string)$pageUid);
|
||||||
|
$additionalParameters = $this->parseOption('additionalParameters');
|
||||||
|
$additionalParameters = is_string($additionalParameters) ? $additionalParameters : '';
|
||||||
|
$additionalParameters = '&' . ltrim($additionalParameters, '&');
|
||||||
|
$statusCode = (int)$this->parseOption('statusCode');
|
||||||
|
$fragment = (string)$this->parseOption('fragment');
|
||||||
|
|
||||||
|
$this->finisherContext->cancel();
|
||||||
|
$this->redirect($pageUid, $additionalParameters, $fragment, $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 int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
|
||||||
|
* @see forward()
|
||||||
|
*/
|
||||||
|
protected function redirect(int $pageUid, string $additionalParameters, string $fragment, int $statusCode): never
|
||||||
|
{
|
||||||
|
$redirectUri = $this->finisherContext->getRequest()->getAttribute('currentContentObject')->createUrl([
|
||||||
|
'parameter' => $pageUid,
|
||||||
|
'additionalParams' => $additionalParameters,
|
||||||
|
'section' => $fragment,
|
||||||
|
]);
|
||||||
|
$this->redirectToUri($redirectUri, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects the web request to another uri.
|
||||||
|
*
|
||||||
|
* NOTE: This method only supports web requests and will throw an exception if used with other request types.
|
||||||
|
*
|
||||||
|
* @param string $uri A string representation of a URI
|
||||||
|
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
|
||||||
|
* @throws PropagateResponseException
|
||||||
|
*/
|
||||||
|
protected function redirectToUri(string $uri, int $statusCode = 303): never
|
||||||
|
{
|
||||||
|
$uri = $this->addBaseUriIfNecessary($uri);
|
||||||
|
$response = new RedirectResponse($uri, $statusCode);
|
||||||
|
// End processing and dispatching by throwing a PropagateResponseException with our response.
|
||||||
|
// @todo: Should be changed to *return* a response instead, but this requires the ContentObjectRender
|
||||||
|
// @todo: to deal with responses instead of strings, if the form is used in a fluid template rendered by the
|
||||||
|
// @todo: FluidTemplateContentObject and the extbase bootstrap isn't used.
|
||||||
|
throw new PropagateResponseException($response, 1477070964);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the base uri if not already in place.
|
||||||
|
*
|
||||||
|
* @param string $uri The URI
|
||||||
|
*/
|
||||||
|
protected function addBaseUriIfNecessary(string $uri): string
|
||||||
|
{
|
||||||
|
return GeneralUtility::locationHeaderUrl($uri, $this->finisherContext->getRequest());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Exception;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||||
|
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||||
|
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 a 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>.hashed (default: false)
|
||||||
|
* ------------------------------------------------------
|
||||||
|
* Set this to true if the value from the submitted form element
|
||||||
|
* should be hashed before writing into the database.
|
||||||
|
*
|
||||||
|
* 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 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
|
||||||
|
* hashed: 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(): void
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
protected function prepareData(array $elementsConfiguration, array $databaseData): array
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|| !isset($elementsConfiguration[$elementIdentifier])
|
||||||
|
|| !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn'])
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) {
|
||||||
|
$saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'];
|
||||||
|
} else {
|
||||||
|
$saveFileIdentifierInsteadOfUid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($elementValue instanceof FileReference) {
|
||||||
|
$elementValue = $this->prepareFileForDatabase($elementValue, $saveFileIdentifierInsteadOfUid);
|
||||||
|
} elseif ($elementValue instanceof ObjectStorage) {
|
||||||
|
$fileIdentifiers = [];
|
||||||
|
foreach ($elementValue as $singleElement) {
|
||||||
|
if ($singleElement instanceof FileReference) {
|
||||||
|
$fileIdentifiers[] = $this->prepareFileForDatabase($singleElement, $saveFileIdentifierInsteadOfUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$elementValue = implode(',', $fileIdentifiers);
|
||||||
|
} elseif (is_array($elementValue)) {
|
||||||
|
$elementValue = implode(',', $elementValue);
|
||||||
|
} elseif ($elementValue instanceof \DateTimeInterface) {
|
||||||
|
$format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U';
|
||||||
|
$elementValue = $elementValue->format($format);
|
||||||
|
} elseif ($elementValue && ($elementsConfiguration[$elementIdentifier]['hashed'] ?? false) === true) {
|
||||||
|
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE');
|
||||||
|
$elementValue = $hashInstance->getHashedPassword($elementValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
|
||||||
|
}
|
||||||
|
return $databaseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the current database operation
|
||||||
|
* @throws FinisherException
|
||||||
|
*/
|
||||||
|
protected function process(int $iterationCount): void
|
||||||
|
{
|
||||||
|
$this->throwExceptionOnInconsistentConfiguration();
|
||||||
|
|
||||||
|
$table = $this->parseOption('table');
|
||||||
|
$table = is_string($table) ? $table : '';
|
||||||
|
$elementsConfiguration = $this->parseOption('elements');
|
||||||
|
$elementsConfiguration = is_array($elementsConfiguration) ? $elementsConfiguration : [];
|
||||||
|
$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'] ?? false) === true
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$databaseData[$databaseColumnName] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$databaseData = $this->prepareData($elementsConfiguration, $databaseData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->saveToDatabase($databaseData, $table, $iterationCount);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new FinisherException(
|
||||||
|
'Failed to save data to database table: ' . $table . '. Error message:' . $e->getMessage(),
|
||||||
|
1754050114,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or insert the values from
|
||||||
|
* $databaseData into the table $table
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function saveToDatabase(array $databaseData, string $table, int $iterationCount): void
|
||||||
|
{
|
||||||
|
if (!empty($databaseData)) {
|
||||||
|
if ($this->parseOption('mode') === 'update') {
|
||||||
|
$whereClause = $this->parseOption('whereClause');
|
||||||
|
foreach ($whereClause as $columnName => $columnValue) {
|
||||||
|
$whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
|
||||||
|
}
|
||||||
|
$this->databaseConnection->update(
|
||||||
|
$table,
|
||||||
|
$databaseData,
|
||||||
|
$whereClause
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->databaseConnection->insert($table, $databaseData);
|
||||||
|
try {
|
||||||
|
$insertedUid = (int)$this->databaseConnection->lastInsertId();
|
||||||
|
} catch (Exception) {
|
||||||
|
// Some database tables like sys_category_record_mm may not
|
||||||
|
// have an "identity" (uid column). In this case DBAL may
|
||||||
|
// throw an exception, which we gracefully handle here.
|
||||||
|
$insertedUid = 0;
|
||||||
|
}
|
||||||
|
$this->finisherContext->getFinisherVariableProvider()->add(
|
||||||
|
$this->shortFinisherIdentifier,
|
||||||
|
'insertedUids.' . $iterationCount,
|
||||||
|
$insertedUid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an exception if some inconsistent configuration
|
||||||
|
* are detected.
|
||||||
|
*
|
||||||
|
* @throws FinisherException
|
||||||
|
*/
|
||||||
|
protected function throwExceptionOnInconsistentConfiguration(): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$this->parseOption('mode') === 'update'
|
||||||
|
&& empty($this->parseOption('whereClause'))
|
||||||
|
) {
|
||||||
|
throw new FinisherException(
|
||||||
|
'An empty option "whereClause" is not allowed in update mode.',
|
||||||
|
1480469086
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the values of the submitted form
|
||||||
|
*/
|
||||||
|
protected function getFormValues(): array
|
||||||
|
{
|
||||||
|
return $this->finisherContext->getFormValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a form element object for a given identifier.
|
||||||
|
*
|
||||||
|
* @return FormElementInterface|null
|
||||||
|
*/
|
||||||
|
protected function getElementByIdentifier(string $elementIdentifier): ?FormElementInterface
|
||||||
|
{
|
||||||
|
return $this
|
||||||
|
->finisherContext
|
||||||
|
->getFormRuntime()
|
||||||
|
->getFormDefinition()
|
||||||
|
->getElementByIdentifier($elementIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareFileForDatabase(FileReference $fileReference, bool $saveFileIdentifierInsteadOfUid = false): int|string
|
||||||
|
{
|
||||||
|
if ($saveFileIdentifierInsteadOfUid) {
|
||||||
|
$elementValue = $fileReference->getOriginalResource()->getCombinedIdentifier();
|
||||||
|
} else {
|
||||||
|
$elementValue = $fileReference->getOriginalResource()->getProperty('uid_local');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $elementValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\Exception;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\Exception;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\Exception;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\Exception;
|
||||||
|
|
||||||
|
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,698 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||||
|
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 = GeneralUtility::makeInstance(FormDefinition::class, 'myForm');
|
||||||
|
*
|
||||||
|
* $page1 = GeneralUtility::makeInstance(Page::class, 'page1');
|
||||||
|
* $formDefinition->addPage($page);
|
||||||
|
*
|
||||||
|
* $element1 = GeneralUtility::makeInstance(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 = GeneralUtility::makeInstance(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\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. Inside a ViewHelper you would use $this->renderingContext->getRequest()
|
||||||
|
* $form = $formDefinition->bind($currentRequest);
|
||||||
|
*
|
||||||
|
* # 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.**
|
||||||
|
*
|
||||||
|
* @internal May change any time, use FormFactoryInterface to select a different FormDefinition if needed
|
||||||
|
* @todo: Declare final in v12
|
||||||
|
*/
|
||||||
|
class FormDefinition extends AbstractCompositeRenderable implements VariableRenderableInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Form's pages
|
||||||
|
*
|
||||||
|
* @var array<int, Page>
|
||||||
|
*/
|
||||||
|
protected $renderables = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The finishers for this form
|
||||||
|
*
|
||||||
|
* @var list<FinisherInterface>
|
||||||
|
*/
|
||||||
|
protected array $finishers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property Mapping Rules, indexed by element identifier
|
||||||
|
*
|
||||||
|
* @var array<string, ProcessingRule>
|
||||||
|
*/
|
||||||
|
protected array $processingRules = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all elements of the form, indexed by identifier.
|
||||||
|
* Is used as internal cache as we need this really often.
|
||||||
|
*
|
||||||
|
* @var array<string, FormElementInterface>
|
||||||
|
*/
|
||||||
|
protected array $elementsByIdentifier = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form element default values in the format ['elementIdentifier' => 'default value']
|
||||||
|
*
|
||||||
|
* @var array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected array $elementDefaultValues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderer class name to be used.
|
||||||
|
*/
|
||||||
|
protected string $rendererClassName = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
protected array $typeDefinitions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
protected array $validatorsDefinition = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
protected array $finishersDefinition = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The persistence identifier of the form
|
||||||
|
*/
|
||||||
|
protected string $persistenceIdentifier = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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|null $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'] ?? [];
|
||||||
|
|
||||||
|
if ($identifier === '') {
|
||||||
|
throw new IdentifierNotValidException('The given identifier was empty.', 1477082503);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->identifier = $identifier;
|
||||||
|
$this->type = $type;
|
||||||
|
$this->persistenceIdentifier = (string)$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.
|
||||||
|
*
|
||||||
|
* @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'];
|
||||||
|
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = GeneralUtility::makeInstance($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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($typeDefinition['variants'])) {
|
||||||
|
foreach ($typeDefinition['variants'] as $variantConfiguration) {
|
||||||
|
$page->createVariant($variantConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayUtility::assertAllArrayKeysAreValid(
|
||||||
|
$typeDefinition,
|
||||||
|
['implementationClassName', 'label', 'renderingOptions', 'formEditor', 'variants']
|
||||||
|
);
|
||||||
|
|
||||||
|
$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<int, 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
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
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', ...]
|
||||||
|
* @throws FinisherPresetNotFoundException
|
||||||
|
*/
|
||||||
|
public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
|
||||||
|
{
|
||||||
|
if (isset($this->finishersDefinition[$finisherIdentifier]['implementationClassName'])) {
|
||||||
|
$implementationClassName = $this->finishersDefinition[$finisherIdentifier]['implementationClassName'];
|
||||||
|
$defaultOptions = $this->finishersDefinition[$finisherIdentifier]['options'] ?? [];
|
||||||
|
ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
|
||||||
|
/** @var FinisherInterface $finisher */
|
||||||
|
$finisher = GeneralUtility::makeInstance($implementationClassName);
|
||||||
|
$finisher->setFinisherIdentifier($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 list<FinisherInterface>
|
||||||
|
*/
|
||||||
|
public function getFinishers(): array
|
||||||
|
{
|
||||||
|
return $this->finishers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an element to the ElementsByIdentifier Cache.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function unregisterRenderable(RenderableInterface $renderable)
|
||||||
|
{
|
||||||
|
if ($renderable instanceof FormElementInterface) {
|
||||||
|
unset($this->elementsByIdentifier[$renderable->getIdentifier()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all form elements with their identifiers as keys
|
||||||
|
*
|
||||||
|
* @return array<string, FormElementInterface>
|
||||||
|
*/
|
||||||
|
public function getElements(): array
|
||||||
|
{
|
||||||
|
return $this->elementsByIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Form Element by its identifier
|
||||||
|
*
|
||||||
|
* If identifier does not exist, returns NULL.
|
||||||
|
*
|
||||||
|
* @param string $elementIdentifier
|
||||||
|
* @return FormElementInterface|null 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
|
||||||
|
*/
|
||||||
|
public function movePageBefore(Page $pageToMove, Page $referencePage)
|
||||||
|
{
|
||||||
|
$this->moveRenderableBefore($pageToMove, $referencePage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move $pageToMove after $referencePage
|
||||||
|
*/
|
||||||
|
public function movePageAfter(Page $pageToMove, Page $referencePage)
|
||||||
|
{
|
||||||
|
$this->moveRenderableAfter($pageToMove, $referencePage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove $pageToRemove from form
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public function bind(RequestInterface $request): FormRuntime
|
||||||
|
{
|
||||||
|
$formRuntime = GeneralUtility::makeInstance(FormRuntime::class);
|
||||||
|
$formRuntime->setFormDefinition($this);
|
||||||
|
$formRuntime->setRequest($request);
|
||||||
|
$formRuntime->initialize();
|
||||||
|
return $formRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProcessingRule(string $propertyPath): ProcessingRule
|
||||||
|
{
|
||||||
|
if (!isset($this->processingRules[$propertyPath])) {
|
||||||
|
$this->processingRules[$propertyPath] = GeneralUtility::makeInstance(ProcessingRule::class);
|
||||||
|
}
|
||||||
|
return $this->processingRules[$propertyPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all mapping rules
|
||||||
|
*
|
||||||
|
* @return array<string, ProcessingRule>
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getProcessingRules(): array
|
||||||
|
{
|
||||||
|
return $this->processingRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getTypeDefinitions(): array
|
||||||
|
{
|
||||||
|
return $this->typeDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getValidatorsDefinition(): array
|
||||||
|
{
|
||||||
|
return $this->validatorsDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the persistence identifier of the form
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getPersistenceIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->persistenceIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the renderer class name
|
||||||
|
*/
|
||||||
|
public function setRendererClassName(string $rendererClassName)
|
||||||
|
{
|
||||||
|
$this->rendererClassName = $rendererClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the classname of the renderer
|
||||||
|
*/
|
||||||
|
public function getRendererClassName(): string
|
||||||
|
{
|
||||||
|
return $this->rendererClassName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
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 (strlen($identifier) === 0) {
|
||||||
|
throw new IdentifierNotValidException('The given identifier string is empty.', 1477082502);
|
||||||
|
}
|
||||||
|
$this->identifier = $identifier;
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method in your custom FormElements if needed
|
||||||
|
*/
|
||||||
|
public function initializeFormElement() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the global unique identifier of the element
|
||||||
|
*/
|
||||||
|
public function getUniqueIdentifier(): string
|
||||||
|
{
|
||||||
|
$formDefinition = $this->getRootForm();
|
||||||
|
$uniqueIdentifier = sprintf('%s-%s', $formDefinition->getIdentifier(), $this->identifier);
|
||||||
|
$uniqueIdentifier = (string)preg_replace('/[^a-zA-Z0-9_-]/', '_', $uniqueIdentifier);
|
||||||
|
return lcfirst($uniqueIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOptions(array $options, bool $resetValidators = false)
|
||||||
|
{
|
||||||
|
if (isset($options['defaultValue'])) {
|
||||||
|
$this->setDefaultValue($options['defaultValue']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['properties'])) {
|
||||||
|
foreach ($options['properties'] as $key => $value) {
|
||||||
|
$this->setProperty($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::setOptions($options, $resetValidators);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public function isRequired(): bool
|
||||||
|
{
|
||||||
|
foreach ($this->getValidators() as $validator) {
|
||||||
|
if ($validator instanceof NotEmptyValidator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a property of the element
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public function getProperties(): array
|
||||||
|
{
|
||||||
|
return $this->properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var FormElementInterface[]
|
||||||
|
*/
|
||||||
|
protected $renderables = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ($identifier === '') {
|
||||||
|
throw new IdentifierNotValidException('The given identifier 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(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($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
|
||||||
|
*/
|
||||||
|
public function removeElement(FormElementInterface $elementToRemove)
|
||||||
|
{
|
||||||
|
$this->removeRenderable($elementToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date form element
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class Date extends AbstractFormElement implements StringableFormElementInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initializes the Form Element by setting the data type to "DateTime"
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function initializeFormElement()
|
||||||
|
{
|
||||||
|
$this->setDataType(\DateTime::class);
|
||||||
|
/** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
|
||||||
|
$propertyMappingConfiguration = $this->getRootForm()->getProcessingRule($this->getIdentifier())->getPropertyMappingConfiguration();
|
||||||
|
// @see https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html#input.date.attrs.value
|
||||||
|
// 'Y-m-d' = https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
|
||||||
|
$propertyMappingConfiguration->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, 'Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $value
|
||||||
|
*/
|
||||||
|
public function valueToString($value): string
|
||||||
|
{
|
||||||
|
$dateFormat = $this->properties['displayFormat'] ?? 'Y-m-d';
|
||||||
|
|
||||||
|
return $value->format($dateFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
||||||
|
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||||
|
use TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic file upload form element
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class FileUpload extends AbstractFormElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initializes the Form Element by setting the data type to an Extbase File Reference
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function initializeFormElement()
|
||||||
|
{
|
||||||
|
$this->setDataType(FileReference::class);
|
||||||
|
|
||||||
|
// Set the property mapping configuration for the file upload element.
|
||||||
|
// * Add the UploadedFileReferenceConverter to convert an uploaded file to a
|
||||||
|
// FileReference (single upload) or ObjectStorage (multiple uploads).
|
||||||
|
// * Setup the storage:
|
||||||
|
// If the property "saveToFileMount" exist for this element it will be used.
|
||||||
|
// If this file mount or the property "saveToFileMount" does not exist
|
||||||
|
// the default storage "1:/user_uploads/" will be used. Uploads are placed
|
||||||
|
// in a dedicated sub-folder (e.g. ".../form_<40-chars-hash>/actual.file").
|
||||||
|
$typeConverter = GeneralUtility::makeInstance(UploadedFileReferenceConverter::class);
|
||||||
|
/** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
|
||||||
|
$propertyMappingConfiguration = $this->getRootForm()
|
||||||
|
->getProcessingRule($this->getIdentifier())
|
||||||
|
->getPropertyMappingConfiguration()
|
||||||
|
->setTypeConverter($typeConverter);
|
||||||
|
|
||||||
|
$uploadConfiguration = [
|
||||||
|
UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_CONFLICT_MODE => 'rename',
|
||||||
|
];
|
||||||
|
|
||||||
|
// In preview mode (Form Editor backend module), skip upload folder resolution
|
||||||
|
// entirely. File uploads are non-functional during preview and resolving the
|
||||||
|
// target folder may throw access permission exceptions for backend users who
|
||||||
|
// do not have access to the configured upload storage.
|
||||||
|
if (!($this->getRootForm()->getRenderingOptions()['previewMode'] ?? false)) {
|
||||||
|
$saveToFileMountIdentifier = $this->getProperties()['saveToFileMount'] ?? '';
|
||||||
|
if ($this->checkSaveFileMountAccess($saveToFileMountIdentifier)) {
|
||||||
|
$uploadConfiguration[UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_FOLDER] = $saveToFileMountIdentifier;
|
||||||
|
} else {
|
||||||
|
// @todo Why should uploaded files be stored to the same directory as the *.form.yaml definitions?
|
||||||
|
$persistenceIdentifier = $this->getRootForm()->getPersistenceIdentifier();
|
||||||
|
if (!empty($persistenceIdentifier)) {
|
||||||
|
$pathinfo = PathUtility::pathinfo($persistenceIdentifier);
|
||||||
|
$saveToFileMountIdentifier = $pathinfo['dirname'];
|
||||||
|
if ($this->checkSaveFileMountAccess($saveToFileMountIdentifier)) {
|
||||||
|
$uploadConfiguration[UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_FOLDER] = $saveToFileMountIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$propertyMappingConfiguration->setTypeConverterOptions(UploadedFileReferenceConverter::class, $uploadConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected function checkSaveFileMountAccess(string $saveToFileMountIdentifier): bool
|
||||||
|
{
|
||||||
|
if (empty($saveToFileMountIdentifier)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PathUtility::isExtensionPath($saveToFileMountIdentifier)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory->getFolderObjectFromCombinedIdentifier($saveToFileMountIdentifier);
|
||||||
|
return true;
|
||||||
|
} catch (\InvalidArgumentException|InsufficientFolderAccessPermissionsException|FolderDoesNotExistException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
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 mixed $value
|
||||||
|
*/
|
||||||
|
public function setProperty(string $key, $value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all element-specific configuration properties
|
||||||
|
*/
|
||||||
|
public function getProperties(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a rendering option
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public function isRequired(): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic form element
|
||||||
|
*
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
class GenericFormElement extends AbstractFormElement {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A grid column, 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 GridColumn extends Section implements GridColumnInterface {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
interface GridColumnInterface extends FormElementInterface {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: frontend
|
||||||
|
*/
|
||||||
|
interface GridRowInterface extends FormElementInterface {}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @throws FormException
|
||||||
|
*/
|
||||||
|
public function setParentRenderable(CompositeRenderableInterface $parentRenderable)
|
||||||
|
{
|
||||||
|
if (!($parentRenderable instanceof FormDefinition)) {
|
||||||
|
throw new FormException(sprintf('The specified parentRenderable must be a FormDefinition, got "%s"', get_debug_type($parentRenderable)), 1329233747);
|
||||||
|
}
|
||||||
|
parent::setParentRenderable($parentRenderable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Form\Domain\Model\FormElements;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
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() {}
|
||||||
|
|
||||||
|
public function setOptions(array $options, bool $resetValidators = false)
|
||||||
|
{
|
||||||
|
if (isset($options['properties'])) {
|
||||||
|
foreach ($options['properties'] as $key => $value) {
|
||||||
|
$this->setProperty($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::setOptions($options, $resetValidators);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public function getProperties(): array
|
||||||
|
{
|
||||||
|
return $this->properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an element-specific configuration property.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public function isRequired(): bool
|
||||||
|
{
|
||||||
|
foreach ($this->getValidators() as $validator) {
|
||||||
|
if ($validator instanceof NotEmptyValidator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user