TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:24 +02:00
commit aad9daaefd
1506 changed files with 94005 additions and 0 deletions
@@ -0,0 +1,140 @@
<?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\Mvc\Configuration;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
/**
* Extend the ExtbaseConfigurationManager to read YAML configurations.
*
* YAML files are discovered automatically from every active extension's
* Configuration/Form/<SetName>/ directory (see {@see \TYPO3\CMS\Form\DependencyInjection\FormYamlCollectorConfigurator}).
*
* Scope: frontend / backend
* @internal
*/
#[AsAlias(ConfigurationManagerInterface::class, public: true)]
readonly class ConfigurationManager implements ExtFormConfigurationManagerInterface
{
public function __construct(
private YamlSource $yamlSource,
#[Autowire(service: 'cache.assets')]
private FrontendInterface $cache,
private TypoScriptService $typoScriptService,
private FormYamlCollector $formYamlCollector,
) {}
/**
* Load and parse YAML files for the current rendering context.
*
* Files are resolved via auto-discovery: {@see FormYamlCollector} scans every
* active extension's Configuration/Form/<SetName>/ directory and returns
* all paths sorted by priority.
*
* The following post-processing steps are applied to the merged configuration:
*
* * Remove all keys whose values are NULL
* * Sort by array keys if all keys within a nesting level are numerical
* * Resolve possible TypoScript settings in FE mode
*/
public function getYamlConfiguration(array $typoScriptSettings, bool $isFrontend, ?ServerRequestInterface $request = null): array
{
$yamlSettingsFilePaths = $this->formYamlCollector->getPaths();
$cacheKey = strtolower('YamlSettings_form' . md5(json_encode($yamlSettingsFilePaths)));
if ($this->cache->has($cacheKey)) {
$yamlSettings = $this->cache->get($cacheKey);
} else {
$yamlSettings = $this->yamlSource->load($yamlSettingsFilePaths);
$yamlSettings = ArrayUtility::removeNullValuesRecursive($yamlSettings);
$yamlSettings = ArrayUtility::sortArrayWithIntegerKeysRecursive($yamlSettings);
$this->cache->set($cacheKey, $yamlSettings);
}
$this->applySiteSettingsOverrides($yamlSettings, $request);
if (is_array($typoScriptSettings['yamlSettingsOverrides'] ?? null) && !empty($typoScriptSettings['yamlSettingsOverrides'])) {
$yamlSettingsOverrides = $typoScriptSettings['yamlSettingsOverrides'];
if ($isFrontend) {
if ($request === null) {
throw new \RuntimeException('Frontend rendering an ext:form requires the request being hand over', 1760451538);
}
$yamlSettingsOverrides = $this->typoScriptService->resolvePossibleTypoScriptConfiguration($yamlSettingsOverrides, $request);
}
ArrayUtility::mergeRecursiveWithOverrule($yamlSettings, $yamlSettingsOverrides);
}
return $yamlSettings;
}
/**
* Read form template/translation site set settings from the current site
* and merge them into the YAML configuration.
* Non-empty values are added at key 20 so they overlay the base paths (key 10)
* while still allowing higher-priority overrides from form sets or yamlSettingsOverrides.
*/
private function applySiteSettingsOverrides(array &$yamlSettings, ?ServerRequestInterface $request): void
{
$site = $request?->getAttribute('site');
if (!$site instanceof Site) {
return;
}
$siteSettings = $site->getSettings();
$renderingOptionsOverrides = [];
$templateRootPath = (string)$siteSettings->get('form.templates.templateRootPath', '');
if ($templateRootPath !== '') {
$renderingOptionsOverrides['templateRootPaths'][20] = $templateRootPath;
}
$partialRootPath = (string)$siteSettings->get('form.templates.partialRootPath', '');
if ($partialRootPath !== '') {
$renderingOptionsOverrides['partialRootPaths'][20] = $partialRootPath;
}
$layoutRootPath = (string)$siteSettings->get('form.templates.layoutRootPath', '');
if ($layoutRootPath !== '') {
$renderingOptionsOverrides['layoutRootPaths'][20] = $layoutRootPath;
}
$translationFile = (string)$siteSettings->get('form.translation.translationFile', '');
if ($translationFile !== '') {
$renderingOptionsOverrides['translation']['translationFiles'][20] = $translationFile;
}
if ($renderingOptionsOverrides !== []) {
$overrides = [
'prototypes' => [
'standard' => [
'formElementsDefinition' => [
'Form' => [
'renderingOptions' => $renderingOptionsOverrides,
],
],
],
],
];
ArrayUtility::mergeRecursiveWithOverrule($yamlSettings, $overrides);
}
}
}
@@ -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!
*/
namespace TYPO3\CMS\Form\Mvc\Configuration;
use Psr\Http\Message\ServerRequestInterface;
/**
* Scope: frontend / backend
* @internal
*/
interface ConfigurationManagerInterface
{
public function getYamlConfiguration(array $typoScriptSettings, bool $isFrontend, ?ServerRequestInterface $request = null): array;
}
+27
View File
@@ -0,0 +1,27 @@
<?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\Mvc\Configuration;
use TYPO3\CMS\Form\Exception as FormException;
/**
* A generic Form configuration Exception
*
* @internal
*/
class Exception extends FormException {}
@@ -0,0 +1,24 @@
<?php
/*
* 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\Mvc\Configuration\Exception;
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* Exception for file write errors
* @internal
*/
class FileWriteException extends Exception {}
@@ -0,0 +1,24 @@
<?php
/*
* 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\Mvc\Configuration\Exception;
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* A No Such File exception
* @internal
*/
class NoSuchFileException extends Exception {}
@@ -0,0 +1,24 @@
<?php
/*
* 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\Mvc\Configuration\Exception;
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* A Parse Error exception
* @internal
*/
class ParseErrorException extends Exception {}
@@ -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\Mvc\Configuration;
/**
* Collects all auto-discovered form YAML configuration files across
* all active TYPO3 extensions.
*
* Each extension can provide YAML files under
* EXT:my_extension/Configuration/Form/<SetName>/
* with an accompanying config.yaml that declares name, label and priority.
* The collector is populated by {@see \TYPO3\CMS\Form\DependencyInjection\FormYamlCollectorConfigurator}
* and provides a priority-sorted list of file paths to {@see ConfigurationManager}.
*
* @internal
*/
final class FormYamlCollector
{
/** @var list<FormYamlConfiguration> */
private array $configurations = [];
public function add(FormYamlConfiguration $configuration): void
{
$this->configurations[] = $configuration;
}
/**
* Returns all registered file paths sorted by ascending priority
* (lower = loaded first = acts as base, higher = override).
*
* @return list<string>
*/
public function getPaths(): array
{
$sorted = $this->configurations;
usort($sorted, static fn(FormYamlConfiguration $a, FormYamlConfiguration $b): int => $a->priority <=> $b->priority);
return array_values(array_map(
static fn(FormYamlConfiguration $c): string => $c->path,
$sorted
));
}
/**
* Returns all registered configurations, regardless of priority.
* Useful for diagnostic/debug purposes.
*
* @return list<FormYamlConfiguration>
*/
public function getAllConfigurations(): array
{
return array_values($this->configurations);
}
}
@@ -0,0 +1,47 @@
<?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\Mvc\Configuration;
/**
* Represents a single auto-discovered form YAML configuration file.
*
* Carries the EXT: path, the priority used for merge ordering, and the
* declared set name from config.yaml. A lower priority value is loaded first
* (acts as base); higher priority values are merged on top and can override
* earlier values.
*
* The {@see $setName} field mirrors the "name" key from config.yaml
* (e.g. "my-vendor/my-set") and is used both for the disable mechanism
* ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets']) and
* for diagnostic / logging output.
*
* @internal
*/
final readonly class FormYamlConfiguration
{
/**
* @param string $path EXT: path to the YAML file, e.g. 'EXT:my_extension/Configuration/Form/MySet/config.yaml'
* @param int $priority Load order: lower = earlier = acts as base (default: 100)
* @param string $setName Declared "name" from config.yaml, e.g. "my-vendor/my-set". Empty string if omitted.
*/
public function __construct(
public string $path,
public int $priority,
public string $setName = '',
) {}
}
@@ -0,0 +1,84 @@
<?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\Mvc\Configuration;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\TypoScript\TypoScriptService as CoreTypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Utilities to manage and convert TypoScript
*
* Scope: frontend
*/
readonly class TypoScriptService
{
public function __construct(
protected CoreTypoScriptService $coreTypoScriptService
) {}
/**
* Parse a configuration with ContentObjectRenderer::cObjGetSingle()
* and return the result.
*
* @internal
*/
public function resolvePossibleTypoScriptConfiguration(array $configuration, ServerRequestInterface $request): array
{
$configuration = $this->coreTypoScriptService->convertPlainArrayToTypoScriptArray($configuration);
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($request);
// @todo: Setting request to COR is probably important, but setting page record here *may* not be needed in this case?
$contentObjectRenderer->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages');
$configuration = $this->resolveTypoScriptConfiguration($configuration, $contentObjectRenderer);
return $this->coreTypoScriptService->convertTypoScriptArrayToPlainArray($configuration);
}
/**
* Parse a configuration with ContentObjectRenderer::cObjGetSingle()
* if there is an array key without and with a dot at the end.
* This sample would be identified as a TypoScript parsable configuration
* part:
*
* [
* 'example' => 'TEXT'
* 'example.' => [
* 'value' => 'some value'
* ]
* ]
*/
protected function resolveTypoScriptConfiguration(array $configuration, ContentObjectRenderer $contentObjectRenderer): array
{
foreach ($configuration as $key => $value) {
$keyWithoutDot = rtrim((string)$key, '.');
if (isset($configuration[$keyWithoutDot]) && isset($configuration[$keyWithoutDot . '.'])) {
$value = $contentObjectRenderer->cObjGetSingle(
$configuration[$keyWithoutDot],
$configuration[$keyWithoutDot . '.'],
$keyWithoutDot
);
$configuration[$keyWithoutDot] = $value;
} elseif (!isset($configuration[$keyWithoutDot]) && isset($configuration[$keyWithoutDot . '.'])) {
$configuration[$keyWithoutDot] = $this->resolveTypoScriptConfiguration($value, $contentObjectRenderer);
}
unset($configuration[$keyWithoutDot . '.']);
}
return $configuration;
}
}
+196
View File
@@ -0,0 +1,196 @@
<?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\Mvc\Configuration;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
/**
* Configuration source based on YAML files
*
* Scope: frontend / backend
* @internal
*/
readonly class YamlSource
{
public function __construct(
private FilePersistenceSlot $filePersistenceSlot,
private YamlFileLoader $yamlFileLoader,
) {}
/**
* Loads the specified configuration files and returns its merged content
* as an array.
*
* @internal
*/
public function load(array $filesToLoad): array
{
$configuration = [];
foreach ($filesToLoad as $fileToLoad) {
if ($fileToLoad instanceof File) {
$loadedConfiguration = $this->loadFromFile($fileToLoad);
} else {
$loadedConfiguration = $this->loadFromFilePath($fileToLoad);
if (isset($loadedConfiguration['TYPO3']['CMS']['Form'])) {
$namespacedConfiguration = $loadedConfiguration['TYPO3']['CMS']['Form'];
unset($loadedConfiguration['TYPO3']);
$loadedConfiguration = array_replace_recursive($namespacedConfiguration, $loadedConfiguration);
}
}
$configuration = array_replace_recursive($configuration, $loadedConfiguration);
}
return ArrayUtility::convertBooleanStringsToBooleanRecursive($configuration);
}
/**
* Save the specified configuration array to the given file in YAML format.
*
* @param File|string $fileToSave The file to write to.
* @param array $configuration The configuration to save
* @throws FileWriteException if the file could not be written
* @internal
*/
public function save(File|string $fileToSave, array $configuration): void
{
try {
$header = $this->getHeaderFromFile($fileToSave);
} catch (InsufficientFileAccessPermissionsException $e) {
throw new FileWriteException($e->getMessage(), 1512584488, $e);
}
$yaml = Yaml::dump($configuration, 99, 2);
if ($fileToSave instanceof File) {
// @deprecated: Remove in v16 along with the FileFormsToDatabaseUpgradeWizard
try {
$this->filePersistenceSlot->allowInvocation(
FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
$this->buildCombinedIdentifier(
$fileToSave->getParentFolder(),
$fileToSave->getName()
),
$this->filePersistenceSlot->getContentSignature(
$header . LF . $yaml
)
);
$fileToSave->setContents($header . LF . $yaml);
} catch (InsufficientFileAccessPermissionsException $e) {
throw new FileWriteException($e->getMessage(), 1512582753, $e);
}
} else {
$byteCount = @file_put_contents($fileToSave, $header . LF . $yaml);
if ($byteCount === false) {
$error = error_get_last();
$errorMessage = $error['message'] ?? 'Check that the file exists and can be written.';
throw new FileWriteException($errorMessage, 1512582929);
}
}
}
/**
* Load YAML configuration from a local file path
*
* @throws ParseErrorException
*/
protected function loadFromFilePath(string $filePath): array
{
try {
$loadedConfiguration = $this->yamlFileLoader->load($filePath);
} catch (\RuntimeException $e) {
throw new ParseErrorException(
sprintf('An error occurred while parsing file "%s": %s', $filePath, $e->getMessage()),
1480195405,
$e
);
}
return $loadedConfiguration;
}
/**
* Load YAML configuration from a FAL file
*
* @throws ParseErrorException
*/
protected function loadFromFile(File $file): array
{
$fileIdentifier = $file->getIdentifier();
$rawYamlContent = $file->getContents();
try {
$loadedConfiguration = Yaml::parse($rawYamlContent);
} catch (ParseException $e) {
throw new ParseErrorException(
sprintf('An error occurred while parsing file "%s": %s', $fileIdentifier, $e->getMessage()),
1574422322,
$e
);
}
return $loadedConfiguration;
}
/**
* Read the header part from the given file. That means, every line
* until the first non comment line is found.
*
* @return string The header of the given YAML file
*/
protected function getHeaderFromFile(File|string $file): string
{
$header = '';
if ($file instanceof File) {
$fileLines = explode(LF, $file->getContents());
} elseif (is_file($file)) {
$fileLines = file($file);
} else {
return '';
}
foreach ($fileLines as $line) {
if (str_starts_with($line, '#')) {
$header .= $line;
} else {
break;
}
}
return $header;
}
/*
* @deprecated: Remove in v16 along with the FileFormsToDatabaseUpgradeWizard
*/
protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
{
return sprintf(
'%d:%s%s',
$folder->getStorage()->getUid(),
$folder->getIdentifier(),
$fileName
);
}
}