TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
<?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\Core\Settings;
use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
/**
* @template T of SettingDefinition|EditableSetting
* @internal
*/
final readonly class Category implements \JsonSerializable
{
/**
* @param list<T> $settings
* @param list<Category<T>> $categories
*/
public function __construct(
public string $key,
public string $label,
public ?string $description = null,
public ?string $icon = null,
public array $settings = [],
public array $categories = [],
) {}
public static function __set_state(array $state): self
{
return new self(...$state);
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}
+93
View File
@@ -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\Core\Settings;
class CategoryAccumulator
{
/**
* Retrieve list of ordered sets, matched by
* $setNames, including their dependencies (recursive)
*
* @param CategoryDefinition[] $categoryDefinitions
* @param SettingDefinition[] $settingsDefinitions
* @return list<Category>
*/
public function getCategories(iterable $categoryDefinitions, iterable $settingsDefinitions): array
{
$categories = [];
foreach ($categoryDefinitions as $category) {
$data = $category->toArray();
$parent = $data['parent'] ?? null;
unset($data['parent']);
$categories[$category->key] = [
'children' => [],
'parent' => $parent,
'data' => $data,
];
}
foreach ($categoryDefinitions as $category) {
if ($category->parent === null) {
continue;
}
if (!isset($categories[$category->parent])) {
throw new \RuntimeException('Missing parent category: ' . $category->parent, 1716291554);
}
$categories[$category->parent]['children'][] = $category->key;
}
$categorizedSettings = [];
foreach ($settingsDefinitions as $definition) {
$category = $definition->category ?? '';
$categorizedSettings[isset($categories[$category]) ? $category : 'other'][] = $definition;
}
if (isset($categorizedSettings['other'])) {
$categories['other'] = [
'children' => [],
'parent' => null,
'data' => [
'key' => 'other',
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:categories.other',
'description' => '',
],
];
}
$instances = [];
foreach ($categories as $key => $category) {
if ($category['parent'] === null) {
$instances[] = $this->createInstance($categories, $key, $categorizedSettings);
}
}
return $instances;
}
private function createInstance(array $categories, string $key, array $categorizedSettings): Category
{
try {
return new Category(...[
...$categories[$key]['data'],
'settings' => $categorizedSettings[$key] ?? [],
'categories' => array_map(fn($key) => $this->createInstance($categories, $key, $categorizedSettings), $categories[$key]['children']),
]);
} catch (\Error $e) {
throw new \Exception('Invalid category definition: ' . json_encode($categories[$key]['data']), 1720528084, $e);
}
}
}
+42
View File
@@ -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\Core\Settings;
/**
* @internal
*/
readonly class CategoryDefinition
{
public function __construct(
public string $key,
public string $label,
public ?string $description = null,
public ?string $icon = null,
public ?string $parent = null,
) {}
public function toArray(): array
{
return array_filter(get_object_vars($this), fn(mixed $value) => $value !== null && $value !== []);
}
public static function __set_state(array $state): self
{
return new self(...$state);
}
}
@@ -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\Core\Settings;
/**
* @internal Only to be used by internal settings functionality
*/
final class InvalidSettingDefinitionException extends \RuntimeException {}
+53
View File
@@ -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\Core\Settings;
final readonly class SettingDefinition implements \JsonSerializable
{
/**
* @param array<int|string, string|int|float|bool> $enum
* @param list<string> $tags
* @param array<string, mixed> $options
*/
public function __construct(
public string $key,
public string $type,
public string|int|float|bool|array|object|null $default,
public string $label,
public ?string $description = null,
public bool $readonly = false,
public array $enum = [],
public ?string $category = null,
public array $tags = [],
public array $options = [],
) {}
public static function __set_state(array $state): self
{
return new self(...$state);
}
public function jsonSerialize(): array
{
return [
...get_object_vars($this),
'enum' => (object)$this->enum,
'options' => (object)$this->options,
];
}
}
@@ -0,0 +1,102 @@
<?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\Core\Settings;
/**
* @internal
*/
final readonly class SettingDefinitionValidation
{
public function __construct(
private SettingsTypeRegistry $settingsTypeRegistry,
) {}
/**
* @throws InvalidSettingDefinitionException
*/
public function validate(SettingDefinition $definition): void
{
if (!$this->settingsTypeRegistry->has($definition->type)) {
throw new InvalidSettingDefinitionException('Invalid settings type "' . $definition->type . '" in setting "' . $definition->key . '"', 1732181103);
}
$type = $this->settingsTypeRegistry->get($definition->type);
// Only validate options if type supports them
if ($type instanceof SettingsTypeOptionAwareInterface) {
$this->validateSettingsTypeOptions($definition, $type);
}
// Validate default value
if (!$type->validate($definition->default, $definition)) {
throw new InvalidSettingDefinitionException('Invalid default value in setting "' . $definition->key . '"', 1732181102);
}
}
private function validateSettingsTypeOptions(
SettingDefinition $definition,
SettingsTypeInterface&SettingsTypeOptionAwareInterface $type
): void {
$supportedOptions = $type->getSupportedOptions();
$options = $definition->options;
foreach ($supportedOptions as $optionName => $optionDefinition) {
if (!array_key_exists($optionName, $options)) {
if ($optionDefinition->required) {
throw new InvalidSettingDefinitionException(
'Required option "' . $optionName . '" missing for type "' . $definition->type . '" in setting "' . $definition->key . '"',
1732181110
);
}
continue;
}
$value = $definition->options[$optionName];
$isValid = match ($optionDefinition->type) {
'string' => is_string($value),
'int' => is_int($value),
'number' => is_int($value) || is_float($value),
'bool' => is_bool($value),
'array' => is_array($value),
default => throw new InvalidSettingDefinitionException('Unsupported settings type option: ' . $optionDefinition->type, 1734513842),
};
if (!$isValid) {
throw new InvalidSettingDefinitionException(
'Invalid value for option "' . $optionName . '" in setting "' . $definition->key . '": expected ' . $optionDefinition->type . ', got ' . gettype($value),
1732181109
);
}
unset($options[$optionName]);
}
if ($options !== []) {
throw new InvalidSettingDefinitionException(
'Unsupported options [' . implode(', ', array_keys($options)) . '] for type "' . $definition->type . '" in setting "' . $definition->key . '". Supported options: [' . implode(', ', array_keys($supportedOptions)) . ']',
1732181108
);
}
if (!$type->validateOptions($definition)) {
throw new InvalidSettingDefinitionException(
'Setting definition options for type "' . $definition->type . '" in setting "' . $definition->key . '" could not be validated.',
1752821671
);
}
}
}
@@ -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\Core\Settings;
use Psr\Container\NotFoundExceptionInterface;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class SettingNotFoundException extends Exception implements NotFoundExceptionInterface {}
+30
View File
@@ -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\Core\Settings;
/**
* @internal
*/
final readonly class SettingValue
{
public function __construct(
public mixed $value,
public string $key,
public ?SettingDefinition $definition,
) {}
}
+51
View File
@@ -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\Core\Settings;
/**
* @internal
*/
final readonly class Settings implements SettingsInterface
{
public function __construct(
private array $settings,
) {}
public function has(string $identifier): bool
{
return array_key_exists($identifier, $this->settings);
}
public function get(string $identifier): mixed
{
if (!$this->has($identifier)) {
throw new SettingNotFoundException('Setting does not exist', 1709555772);
}
return $this->settings[$identifier];
}
public function getIdentifiers(): array
{
return array_keys($this->settings);
}
public static function __set_state(array $state): static
{
return new static(...$state);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?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\Core\Settings;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* @internal
*/
final readonly class SettingsDiff
{
/**
* @param string[] $changes
* @param string[] $deletions
*/
public function __construct(
public array $settings,
public array $changes,
public array $deletions,
) {}
public function asArray(): array
{
return $this->settings;
}
/**
* Calculate a new settings tree for the given $targetSettings
*
* Settings that have the same value as their default value
* are removed (tree is minified) if the list of default settings
* via $defaultSettings.
*
* @param array $currentSettings Current settings
* In case of site settings: config/sites/…/settings.yaml
* @param SettingsInterface $targetSettings Target settings
* (values as supplied via the settings editor)
* @param SettingsInterface $defaultSettings Default settings, without local settings tree applied.
* In case of site settings: Combination of all settings
* defined in settings.definitions.yaml + setting.yaml
* from all selected sets combined
*/
public static function create(
array $currentSettings,
SettingsInterface $targetSettings,
?SettingsInterface $defaultSettings = null,
): self {
// Copy existing settings from current settings map/tree, to keep any settings
// that have been present before (and are not defined in $defaultSettings)
// Usecase for site settings:
// Preserve "anonymous" v12-style site settings that have no definition in settings.definitions.yaml
// and are stored as a tree instead of a map
$settings = $currentSettings;
// Merge target settings into current settings
$changes = [];
$deletions = [];
foreach ($targetSettings->getIdentifiers() as $key) {
$value = $targetSettings->get($key);
if ($defaultSettings !== null && $value === $defaultSettings->get($key)) {
if (ArrayUtility::isValidPath($settings, $key, '.')) {
$settings = self::removeByPathWithAncestors($settings, $key, '.');
$deletions[] = $key;
}
if (array_key_exists($key, $settings)) {
unset($settings[$key]);
$deletions[] = $key;
}
continue;
}
// Remove key from legacy tree
if (str_contains($key, '.') && ArrayUtility::isValidPath($settings, $key, '.')) {
$settings = self::removeByPathWithAncestors($settings, $key, '.');
}
if (!array_key_exists($key, $settings)
|| $value !== $settings[$key]
) {
$settings[$key] = $value;
$changes[] = $key;
}
}
return new self(
$settings,
$changes,
$deletions
);
}
private static function removeByPathWithAncestors(array $array, string $path, string $delimiter): array
{
if ($path === '' || !ArrayUtility::isValidPath($array, $path, $delimiter)) {
return $array;
}
$array = ArrayUtility::removeByPath($array, $path, $delimiter);
$parts = explode($delimiter, $path);
array_pop($parts);
$parentPath = implode($delimiter, $parts);
if ($parentPath !== '' && ArrayUtility::isValidPath($array, $parentPath, $delimiter)) {
$parent = ArrayUtility::getValueByPath($array, $parentPath, $delimiter);
if ($parent === []) {
return self::removeByPathWithAncestors($array, $parentPath, $delimiter);
}
}
return $array;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?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\Core\Settings;
/**
* @internal
*/
final readonly class SettingsFactory
{
public function __construct(
private SettingsTypeRegistry $settingsTypeRegistry,
) {}
public function resolveSettings(SettingsProviderInterface ...$providers): SettingsInterface
{
/** @var SettingDefinition[] $definitions */
$definitions = [];
/** @var SettingValue[] $settings */
$settings = [];
foreach ($providers as $provider) {
foreach ($provider->getDefinitions() as $definition) {
$definitions[$definition->key] = $definition;
}
$settings = [
...$settings,
...$provider->getProvidedSettings($definitions),
];
}
/** @var array<string, string|int|float|bool|array|null> $map */
$map = [];
foreach (array_reverse($settings) as $setting) {
if (array_key_exists($setting->key, $map)) {
continue;
}
$value = $setting->value;
if ($setting->definition !== null && !$this->validateAndTransformValue($value, $setting->definition)) {
continue;
}
$map[$setting->key] = $value;
}
return new Settings($map);
}
public function createSettingsFromFormData(array $settings, iterable $definitions): SettingsInterface
{
$definitionMap = [];
foreach ($definitions as $definition) {
$definitionMap[$definition->key] = $definition;
}
foreach ($settings as $key => $value) {
$definition = $definitionMap[$key] ?? null;
if ($definition === null) {
throw new \RuntimeException('Unexpected setting ' . $key . ' is not defined', 1724067004);
}
if ($definition->readonly) {
unset($settings[$key]);
continue;
}
// @todo We should collect invalid values and report in the UI instead of ignoring them
if (!$this->validateAndTransformValue($value, $definition)) {
$value = $definition->default;
}
$settings[$key] = $value;
}
return new Settings($settings);
}
private function validateAndTransformValue(mixed &$value, SettingDefinition $definition): bool
{
if (!$this->settingsTypeRegistry->has($definition->type)) {
throw new \RuntimeException('Setting type ' . $definition->type . ' is not defined.', 1712437727);
}
$type = $this->settingsTypeRegistry->get($definition->type);
if (!$type->validate($value, $definition)) {
return false;
}
$value = $type->transformValue($value, $definition);
return true;
}
}
+30
View File
@@ -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\Core\Settings;
use Psr\Container\ContainerInterface;
/**
* @internal
*/
interface SettingsInterface extends ContainerInterface
{
public function getIdentifiers(): array;
public static function __set_state(array $state): SettingsInterface;
}
+73
View File
@@ -0,0 +1,73 @@
<?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\Core\Settings;
/**
* Settings provider implementation for providing settings definitions and values.
*
* This provider is used internally by the Settings API to manage setting definitions
* and their corresponding values. It combines default values from definitions with
* runtime values provided through the settings array.
*
* @internal
*/
final readonly class SettingsProvider implements SettingsProviderInterface
{
public function __construct(
public string $name,
private array $settings,
private array $definitions = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getDefinitions(): array
{
return $this->definitions;
}
/**
* @return SettingValue[]
*/
public function getProvidedSettings(array $globalDefinitions): array
{
/** @var SettingValue[] $settings */
$settings = [];
foreach ($this->definitions as $definition) {
$settings[] = new SettingValue(
value: $definition->default,
key: $definition->key,
definition: $definition,
);
}
foreach ($this->settings as $key => $value) {
$definition = $globalDefinitions[$key] ?? null;
if ($definition !== null) {
$settings[] = new SettingValue(
value: $value,
key: $key,
definition: $definition,
);
}
}
return $settings;
}
}
@@ -0,0 +1,35 @@
<?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\Core\Settings;
/**
* @internal
*/
interface SettingsProviderInterface
{
/**
* @return SettingDefinition[]
*/
public function getDefinitions(): array;
/**
* @param SettingDefinition[] $currentDefinitions
* @return SettingValue[]
*/
public function getProvidedSettings(array $currentDefinitions): array;
}
@@ -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\Core\Settings;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* @internal
*/
#[AutoconfigureTag('settings.type')]
interface SettingsTypeInterface
{
public function validate(mixed $value, SettingDefinition $definition): bool;
public function transformValue(mixed $value, SettingDefinition $definition): mixed;
public function getJavaScriptModule(): string;
}
+30
View File
@@ -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\Core\Settings;
/**
* @internal
*/
final readonly class SettingsTypeOption
{
public function __construct(
public string $type,
public string $description,
public bool $required = false,
) {}
}
@@ -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\Core\Settings;
/**
* @internal
*/
interface SettingsTypeOptionAwareInterface
{
/**
* @return array<string, SettingsTypeOption>
*/
public function getSupportedOptions(): array;
public function validateOptions(SettingDefinition $definition): bool;
}
+42
View File
@@ -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\Core\Settings;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
/**
* @internal
*/
final readonly class SettingsTypeRegistry
{
public function __construct(
#[AutowireLocator('settings.type')]
private ServiceLocator $types
) {}
public function has(string $type): bool
{
return $this->types->has($type);
}
public function get(string $type): SettingsTypeInterface
{
return $this->types->get($type);
}
}
+82
View File
@@ -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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
#[AsTaggedItem(index: 'bool')]
readonly class BoolType implements SettingsTypeInterface
{
/** @var array<int|string, bool> */
private array $stringMap;
public function __construct(
protected LoggerInterface $logger,
) {
$this->stringMap = [
'0' => false,
'1' => true,
'false' => false,
'true' => true,
'off' => false,
'on' => true,
'no' => false,
'yes' => true,
];
}
public function validate(mixed $value, SettingDefinition $definition): bool
{
if (is_bool($value)) {
return true;
}
if (($value === 0 || $value === 1)) {
return true;
}
if (is_string($value) && isset($this->stringMap[$value])) {
return true;
}
return false;
}
public function transformValue(mixed $value, SettingDefinition $definition): bool
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
if (is_bool($value)) {
return $value;
}
if (is_int($value)) {
return (bool)$value;
}
if (is_string($value)) {
return $this->stringMap[$value] ?? false;
}
return false;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/bool.js';
}
}
+149
View File
@@ -0,0 +1,149 @@
<?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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* @todo Extract color parsing into utility including a registry for
* custom color spaces.
*/
#[AsTaggedItem(index: 'color')]
readonly class ColorType implements SettingsTypeInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
$stringType = new StringType($this->logger);
if (!$stringType->validate($value, $definition)) {
return false;
}
$value = $stringType->transformValue($value, $definition);
return $this->doColorNormalization($value) !== null;
}
public function transformValue(mixed $value, SettingDefinition $definition): string
{
$stringType = new StringType($this->logger);
if (!$stringType->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
$value = $stringType->transformValue($value, $definition);
return $this->doColorNormalization($value) ?? $definition->default;
}
private function doColorNormalization(string $value): ?string
{
if (str_starts_with($value, 'rgb(') && str_ends_with($value, ')')) {
$values = GeneralUtility::trimExplode(',', substr(substr($value, 0, -1), 4));
return $this->normalizeRgb($values);
}
if (str_starts_with($value, 'rgba(') && str_ends_with($value, ')')) {
$values = GeneralUtility::trimExplode(',', substr(substr($value, 0, -1), 5));
return $this->normalizeRgba($values);
}
if (str_starts_with($value, '#')) {
return $this->normalizeHex(substr($value, 1));
}
return null;
}
private function normalizeRgb(array $values): ?string
{
if (count($values) === 1) {
$values = GeneralUtility::trimExplode('/', $values[0]);
if (count($values) === 2) {
return $this->normalizeRgba([...GeneralUtility::trimExplode(' ', $values[0]), $values[1]]);
}
$values = GeneralUtility::trimExplode(' ', $values[0]);
}
if (count($values) !== 3) {
return null;
}
foreach ($values as $value) {
if (!MathUtility::canBeInterpretedAsInteger($value)) {
return null;
}
$value = (int)$value;
if ($value < 0 || $value > 255) {
return null;
}
}
return 'rgb(' . implode(',', $values) . ')';
}
private function normalizeRgba(array $values): ?string
{
if (count($values) !== 4) {
return null;
}
$a = array_pop($values);
if (!MathUtility::canBeInterpretedAsFloat($a)) {
return null;
}
if ((float)$a < 0 || (float)$a > 1) {
return null;
}
foreach ($values as $value) {
if (!MathUtility::canBeInterpretedAsInteger($value)) {
return null;
}
$value = (int)$value;
if ($value < 0 || $value > 255) {
return null;
}
}
$values[] = $a;
return 'rgba(' . implode(',', $values) . ')';
}
private function normalizeHex(string $values): ?string
{
$len = strlen($values);
if ($len !== 3 && $len !== 6 && $len !== 8) {
return null;
}
if (!preg_match('/^[0-9a-f]+$/i', $values)) {
return null;
}
return '#' . $values;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/color.js';
}
}
+121
View File
@@ -0,0 +1,121 @@
<?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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Settings\SettingsTypeOption;
use TYPO3\CMS\Core\Settings\SettingsTypeOptionAwareInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
#[AsTaggedItem(index: 'int')]
readonly class IntType implements SettingsTypeInterface, SettingsTypeOptionAwareInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
// Normalize to integer if possible
if (is_int($value)) {
$intValue = $value;
} elseif (is_string($value) && MathUtility::canBeInterpretedAsInteger($value)) {
$intValue = (int)$value;
} else {
return false;
}
// Check optional constraints
if (array_key_exists('min', $definition->options)
&& $intValue < $definition->options['min']
) {
return false;
}
if (array_key_exists('max', $definition->options)
&& $intValue > $definition->options['max']
) {
return false;
}
if (array_key_exists('step', $definition->options)) {
$stepBase = array_key_exists('min', $definition->options) ? $definition->options['min'] : 0;
if (($stepBase - $value) % $definition->options['step'] !== 0) {
return false;
}
}
return true;
}
public function transformValue(mixed $value, SettingDefinition $definition): int
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
return (int)$value;
}
public function getSupportedOptions(): array
{
return [
'min' => new SettingsTypeOption(
type: 'int',
description: 'Minimum value allowed',
required: false,
),
'max' => new SettingsTypeOption(
type: 'int',
description: 'Maximum value allowed',
required: false,
),
'step' => new SettingsTypeOption(
type: 'int',
description: 'Step size',
required: false,
),
];
}
public function validateOptions(SettingDefinition $definition): bool
{
$min = $definition->options['min'] ?? null;
$max = $definition->options['max'] ?? null;
$step = $definition->options['step'] ?? null;
if ($min !== null && $max !== null && $min > $max) {
return false;
}
if ($min !== null && $max !== null && $step !== null) {
if ($max - $min < $step) {
return false;
}
if (($max - $min) % $step !== 0) {
return false;
}
}
return true;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/int.js';
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Settings\SettingsTypeOption;
use TYPO3\CMS\Core\Settings\SettingsTypeOptionAwareInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
#[AsTaggedItem(index: 'number')]
readonly class NumberType implements SettingsTypeInterface, SettingsTypeOptionAwareInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
// Normalize value
if (is_int($value) || is_float($value)) {
$numericValue = (float)$value;
} elseif (is_string($value) && (
MathUtility::canBeInterpretedAsInteger($value)
|| MathUtility::canBeInterpretedAsFloat($value)
)) {
$numericValue = (float)$value;
} else {
return false;
}
// Check optional constraints
if (array_key_exists('min', $definition->options)
&& $numericValue < $definition->options['min']
) {
return false;
}
if (array_key_exists('max', $definition->options)
&& $numericValue > $definition->options['max']
) {
return false;
}
if (array_key_exists('step', $definition->options)) {
$stepBase = array_key_exists('min', $definition->options) ? $definition->options['min'] : 0.0;
$offset = ($stepBase - $numericValue) / $definition->options['step'];
if ((string)(int)$offset !== (string)$offset) {
return false;
}
}
return true;
}
public function transformValue(mixed $value, SettingDefinition $definition): int|float
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
if (is_string($value)) {
return MathUtility::canBeInterpretedAsInteger($value)
? (int)$value
: (float)$value;
}
return $value;
}
public function getSupportedOptions(): array
{
return [
'min' => new SettingsTypeOption(
type: 'number',
description: 'Minimum value allowed',
required: false,
),
'max' => new SettingsTypeOption(
type: 'number',
description: 'Maximum value allowed',
required: false,
),
'step' => new SettingsTypeOption(
type: 'number',
description: 'Step size',
required: false,
),
];
}
public function validateOptions(SettingDefinition $definition): bool
{
$min = $definition->options['min'] ?? null;
$max = $definition->options['max'] ?? null;
$step = $definition->options['step'] ?? null;
if ($min !== null && $max !== null && $min > $max) {
return false;
}
if ($min !== null && $max !== null && $step !== null) {
if ($max - $min < $step) {
return false;
}
$steps = ($max - $min) / $step;
if ((string)(int)$steps !== (string)$steps) {
return false;
}
}
return true;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/number.js';
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
#[AsTaggedItem(index: 'page')]
readonly class PageType implements SettingsTypeInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
if (is_int($value)) {
return true;
}
if (is_string($value) && MathUtility::canBeInterpretedAsInteger($value)) {
return true;
}
return false;
}
public function transformValue(mixed $value, SettingDefinition $definition): int
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
return (int)$value;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/page.js';
}
}
+89
View File
@@ -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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
#[AsTaggedItem(index: 'stringlist')]
readonly class StringListType implements SettingsTypeInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
$value = $this->decodeJsonAsFallback($value);
if (!is_array($value)) {
return false;
}
return $this->doValidate(new StringType($this->logger), $value, $definition);
}
public function transformValue(mixed $value, SettingDefinition $definition): array
{
$stringType = new StringType($this->logger);
$value = $this->decodeJsonAsFallback($value);
if (!is_array($value) || !$this->doValidate($stringType, $value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
return array_map(static fn(mixed $v): string => $stringType->transformValue($v, $definition), $value);
}
public function doValidate(StringType $stringType, array $value, SettingDefinition $definition): bool
{
if (!array_is_list($value)) {
return false;
}
foreach ($value as $v) {
if (!$stringType->validate($v, $definition)) {
return false;
}
}
return true;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/stringlist.js';
}
private function decodeJsonAsFallback(mixed $value): mixed
{
if (is_array($value)) {
return $value;
}
// Otherwise, check if given string value is a json-encoded string
if (is_string($value)) {
try {
// A json-encoded stringlist only needs 2-levels
$value = json_decode($value, false, 2, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
}
return $value;
}
}
+110
View File
@@ -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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Settings\SettingsTypeOption;
use TYPO3\CMS\Core\Settings\SettingsTypeOptionAwareInterface;
#[AsTaggedItem(index: 'string')]
readonly class StringType implements SettingsTypeInterface, SettingsTypeOptionAwareInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
if (is_object($value) && !$value instanceof \Stringable) {
return false;
}
// Normalize value
$stringValue = (string)$value;
// Check optional constraints
if (array_key_exists('min', $definition->options)
&& mb_strlen($stringValue) < (int)$definition->options['min']
) {
return false;
}
if (array_key_exists('max', $definition->options)
&& mb_strlen($stringValue) > (int)$definition->options['max']
) {
return false;
}
return true;
}
public function transformValue(mixed $value, SettingDefinition $definition): string
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
return $definition->default;
}
if (is_bool($value)) {
if ($value) {
return 'true';
}
return 'false';
}
return (string)$value;
}
public function getSupportedOptions(): array
{
return [
'min' => new SettingsTypeOption(
type: 'int',
description: 'Minimum character count allowed',
required: false,
),
'max' => new SettingsTypeOption(
type: 'int',
description: 'Maximum character count allowed',
required: false,
),
];
}
public function validateOptions(SettingDefinition $definition): bool
{
$min = $definition->options['min'] ?? null;
$max = $definition->options['max'] ?? null;
if ($min !== null && $min < 0) {
return false;
}
if ($max !== null && $max < 1) {
return false;
}
if ($min !== null && $max !== null && $min > $max) {
return false;
}
return true;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/string.js';
}
}
+23
View File
@@ -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\Core\Settings\Type;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(index: 'text')]
readonly class TextType extends StringType {}
+92
View File
@@ -0,0 +1,92 @@
<?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\Core\Settings\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeInterface;
use TYPO3\CMS\Core\Settings\SettingsTypeOption;
use TYPO3\CMS\Core\Settings\SettingsTypeOptionAwareInterface;
#[AsTaggedItem(index: 'url')]
readonly class UrlType implements SettingsTypeInterface, SettingsTypeOptionAwareInterface
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function validate(mixed $value, SettingDefinition $definition): bool
{
if ($value === null || $value === '') {
return true;
}
if (!is_string($value)) {
return false;
}
if (filter_var($value, FILTER_VALIDATE_URL) === false) {
return false;
}
// Check optional constraints
if (array_key_exists('pattern', $definition->options)
&& preg_match('/' . str_replace('/', '\/', $definition->options['pattern']) . '/', $value) !== 1
) {
return false;
}
return true;
}
public function transformValue(mixed $value, SettingDefinition $definition): string
{
if (!$this->validate($value, $definition)) {
$this->logger->warning('Invalid URL, reverting to default: {key}', ['key' => $definition->key]);
return (string)$definition->default;
}
return (string)$value;
}
public function getSupportedOptions(): array
{
return [
'pattern' => new SettingsTypeOption(
type: 'string',
description: 'Regular expression pattern for URL validation',
required: false,
),
];
}
public function validateOptions(SettingDefinition $definition): bool
{
if (array_key_exists('pattern', $definition->options)
&& @preg_match('/' . str_replace('/', '\/', $definition->options['pattern']) . '/', '') === false
) {
return false;
}
return true;
}
public function getJavaScriptModule(): string
{
return '@typo3/backend/settings/type/url.js';
}
}