TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Exception\SettingsWriteException;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle loading and writing of global and local (instance specific)
|
||||
* configuration.
|
||||
*
|
||||
* This class handles the access to the files
|
||||
* - EXT:core/Configuration/DefaultConfiguration.php (default TYPO3_CONF_VARS)
|
||||
* - config/system/settings.php or typo3conf/system/settings.php - previously known as LocalConfiguration.php
|
||||
* - config/system/additional.php or typo3conf/system/additional.php (optional additional code blocks) - previously known as typo3conf/AdditionalConfiguration.php
|
||||
*
|
||||
* IMPORTANT:
|
||||
* This class is intended for internal core use ONLY.
|
||||
* Extensions should usually use the resulting $GLOBALS['TYPO3_CONF_VARS'] array,
|
||||
* do not try to modify settings in the config/system/settings.php file with an extension.
|
||||
* @internal
|
||||
*/
|
||||
class ConfigurationManager
|
||||
{
|
||||
/**
|
||||
* @var string Path to default TYPO3_CONF_VARS file, relative to the public web folder
|
||||
*/
|
||||
protected $defaultConfigurationFile = __DIR__ . '/../../Configuration/DefaultConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to description file for TYPO3_CONF_VARS, relative to the public web folder
|
||||
*/
|
||||
protected $defaultConfigurationDescriptionFile = 'EXT:core/Configuration/DefaultConfigurationDescription.yaml';
|
||||
|
||||
/**
|
||||
* @var string Path to factory configuration file used during installation as LocalConfiguration boilerplate
|
||||
*/
|
||||
protected $factoryConfigurationFile = __DIR__ . '/../../Configuration/FactoryConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to possible additional factory configuration file delivered by packages
|
||||
*/
|
||||
protected $additionalFactoryConfigurationFile = 'AdditionalFactoryConfiguration.php';
|
||||
|
||||
/**
|
||||
* Writing to these configuration paths is always allowed,
|
||||
* even if the requested sub path does not exist yet.
|
||||
*/
|
||||
protected array $allowedSettingsPaths = [
|
||||
'EXTCONF',
|
||||
'DB',
|
||||
'SYS/caching/cacheConfigurations',
|
||||
'SYS/encryptionKey',
|
||||
'SYS/session',
|
||||
'EXTENSIONS',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return default configuration array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultConfiguration()
|
||||
{
|
||||
return require $this->getDefaultConfigurationFileLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the default configuration file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDefaultConfigurationFileLocation()
|
||||
{
|
||||
return $this->defaultConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the default configuration description file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDefaultConfigurationDescriptionFileLocation()
|
||||
{
|
||||
return $this->defaultConfigurationDescriptionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return configuration array of typo3conf/system/settings.php or config/system/settings.php
|
||||
*
|
||||
* @return array Content array of local configuration file
|
||||
*/
|
||||
public function getLocalConfiguration(): array
|
||||
{
|
||||
return require $this->getSystemConfigurationFileLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the TYPO3-project specific settings file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* Path to local overload TYPO3_CONF_VARS file.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSystemConfigurationFileLocation(bool $relativeToProjectRoot = false): string
|
||||
{
|
||||
// For composer-based installations, the file is in config/system/settings.php
|
||||
$path = Environment::getConfigPath() . '/system/settings.php';
|
||||
if ($relativeToProjectRoot) {
|
||||
return substr($path, strlen(Environment::getProjectPath()) + 1);
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local configuration array merged with default configuration
|
||||
*/
|
||||
public function getMergedLocalConfiguration(): array
|
||||
{
|
||||
$localConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($localConfiguration, $this->getLocalConfiguration());
|
||||
return $localConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the additional configuration file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getAdditionalConfigurationFileLocation()
|
||||
{
|
||||
// For composer-based installations, the file is in config/system/additional.php
|
||||
return Environment::getConfigPath() . '/system/additional.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute file location of factory configuration file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFactoryConfigurationFileLocation()
|
||||
{
|
||||
return $this->factoryConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute file location of factory configuration file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAdditionalFactoryConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/' . $this->additionalFactoryConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override local configuration with new values.
|
||||
*
|
||||
* @param array $configurationToMerge Override configuration array
|
||||
*/
|
||||
public function updateLocalConfiguration(array $configurationToMerge)
|
||||
{
|
||||
$newLocalConfiguration = $this->getLocalConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($newLocalConfiguration, $configurationToMerge);
|
||||
$this->writeLocalConfiguration($newLocalConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value at given path from default configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed Value at path
|
||||
*/
|
||||
public function getDefaultConfigurationValueByPath($path)
|
||||
{
|
||||
return ArrayUtility::getValueByPath($this->getDefaultConfiguration(), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value at given path from local configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed Value at path
|
||||
*/
|
||||
public function getLocalConfigurationValueByPath($path)
|
||||
{
|
||||
return ArrayUtility::getValueByPath($this->getLocalConfiguration(), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from configuration, this is default configuration
|
||||
* merged with local configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfigurationValueByPath($path)
|
||||
{
|
||||
$defaultConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($defaultConfiguration, $this->getLocalConfiguration());
|
||||
return ArrayUtility::getValueByPath($defaultConfiguration, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a given path in local configuration to a new value.
|
||||
* Warning: TO BE USED ONLY to update a single feature.
|
||||
* NOT TO BE USED within iterations to update multiple features.
|
||||
* To update multiple features use setLocalConfigurationValuesByPathValuePairs().
|
||||
*
|
||||
* @param string $path Path to update
|
||||
* @param mixed $value Value to set
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function setLocalConfigurationValueByPath($path, $value)
|
||||
{
|
||||
$result = false;
|
||||
if ($this->isValidLocalConfigurationPath($path)) {
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
$localConfiguration = ArrayUtility::setValueByPath($localConfiguration, $path, $value);
|
||||
$result = $this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update / set a list of path and value pairs in local configuration file
|
||||
*
|
||||
* @param array $pairs Key is path, value is value to set
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function setLocalConfigurationValuesByPathValuePairs(array $pairs)
|
||||
{
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
foreach ($pairs as $path => $value) {
|
||||
if ($this->isValidLocalConfigurationPath($path)) {
|
||||
$localConfiguration = ArrayUtility::setValueByPath($localConfiguration, $path, $value);
|
||||
}
|
||||
}
|
||||
return $this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keys from LocalConfiguration
|
||||
*
|
||||
* @param array $keys Array with key paths to remove from LocalConfiguration
|
||||
* @return bool TRUE if something was removed
|
||||
*/
|
||||
public function removeLocalConfigurationKeysByPath(array $keys): bool
|
||||
{
|
||||
$result = false;
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
foreach ($keys as $path) {
|
||||
// Remove key if path is within LocalConfiguration
|
||||
if (ArrayUtility::isValidPath($localConfiguration, $path)) {
|
||||
$result = true;
|
||||
$localConfiguration = ArrayUtility::removeByPath($localConfiguration, $path);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
$this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a certain feature and writes the option to system/settings.php
|
||||
* Short-hand method
|
||||
* Warning: TO BE USED ONLY to enable a single feature.
|
||||
* NOT TO BE USED within iterations to enable multiple features.
|
||||
* To update multiple features use setLocalConfigurationValuesByPathValuePairs().
|
||||
*
|
||||
* @param string $featureName something like "InlineSvgImages"
|
||||
* @return bool true on successful writing the setting
|
||||
*/
|
||||
public function enableFeature(string $featureName): bool
|
||||
{
|
||||
return $this->setLocalConfigurationValueByPath('SYS/features/' . $featureName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a feature and writes the option to system/settings.php
|
||||
* Short-hand method
|
||||
* Warning: TO BE USED ONLY to disable a single feature.
|
||||
* NOT TO BE USED within iterations to disable multiple features.
|
||||
* To update multiple features use setLocalConfigurationValuesByPathValuePairs().
|
||||
*
|
||||
* @param string $featureName something like "InlineSvgImages"
|
||||
* @return bool true on successful writing the setting
|
||||
*/
|
||||
public function disableFeature(string $featureName): bool
|
||||
{
|
||||
return $this->setLocalConfigurationValueByPath('SYS/features/' . $featureName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the configuration can be written.
|
||||
*
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
public function canWriteConfiguration()
|
||||
{
|
||||
$fileLocation = $this->getSystemConfigurationFileLocation();
|
||||
return @is_writable(file_exists($fileLocation) ? $fileLocation : dirname($fileLocation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration array and exports it to the global variable
|
||||
*
|
||||
* @internal
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function exportConfiguration(): void
|
||||
{
|
||||
if (@is_file($this->getSystemConfigurationFileLocation())) {
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
$defaultConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($defaultConfiguration, $localConfiguration);
|
||||
$GLOBALS['TYPO3_CONF_VARS'] = $defaultConfiguration;
|
||||
} else {
|
||||
// No LocalConfiguration (yet), load DefaultConfiguration
|
||||
$GLOBALS['TYPO3_CONF_VARS'] = $this->getDefaultConfiguration();
|
||||
}
|
||||
|
||||
// Load AdditionalConfiguration
|
||||
if (@is_file($this->getAdditionalConfigurationFileLocation())) {
|
||||
require $this->getAdditionalConfigurationFileLocation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration array to %config-dir%/system/settings.php
|
||||
*
|
||||
* @param array $configuration The local configuration to be written
|
||||
* @throws \RuntimeException
|
||||
* @return bool TRUE on success
|
||||
* @internal
|
||||
*/
|
||||
public function writeLocalConfiguration(array $configuration)
|
||||
{
|
||||
$systemSettingsFile = $this->getSystemConfigurationFileLocation();
|
||||
if (!$this->canWriteConfiguration()) {
|
||||
throw new SettingsWriteException(
|
||||
$this->getSystemConfigurationFileLocation(true) . ' is not writable.',
|
||||
1346323822
|
||||
);
|
||||
}
|
||||
$configuration = ArrayUtility::sortByKeyRecursive($configuration);
|
||||
$result = GeneralUtility::writeFile(
|
||||
$systemSettingsFile,
|
||||
"<?php\n"
|
||||
. 'return '
|
||||
. ArrayUtility::arrayExport($configuration)
|
||||
. ";\n",
|
||||
true
|
||||
);
|
||||
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($systemSettingsFile);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write additional configuration array to config/system/additional.php / typo3conf/system/additional.php
|
||||
*
|
||||
* @param array $additionalConfigurationLines The configuration lines to be written
|
||||
* @throws \RuntimeException
|
||||
* @return bool TRUE on success
|
||||
* @internal
|
||||
*/
|
||||
public function writeAdditionalConfiguration(array $additionalConfigurationLines)
|
||||
{
|
||||
return GeneralUtility::writeFile(
|
||||
$this->getAdditionalConfigurationFileLocation(),
|
||||
"<?php\n" . implode("\n", $additionalConfigurationLines) . "\n",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses FactoryConfiguration file and a possible AdditionalFactoryConfiguration
|
||||
* file in typo3conf to create a basic config/system/settings.php. This is used
|
||||
* by the installer in an early step.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
public function createLocalConfigurationFromFactoryConfiguration()
|
||||
{
|
||||
if (file_exists($this->getSystemConfigurationFileLocation())) {
|
||||
throw new \RuntimeException(
|
||||
basename($this->getSystemConfigurationFileLocation(true)) . ' already exists',
|
||||
1364836026
|
||||
);
|
||||
}
|
||||
$localConfigurationArray = require $this->getFactoryConfigurationFileLocation();
|
||||
$additionalFactoryConfigurationFileLocation = $this->getAdditionalFactoryConfigurationFileLocation();
|
||||
if (file_exists($additionalFactoryConfigurationFileLocation)) {
|
||||
$additionalFactoryConfigurationArray = require $additionalFactoryConfigurationFileLocation;
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$localConfigurationArray,
|
||||
$additionalFactoryConfigurationArray
|
||||
);
|
||||
}
|
||||
$randomKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
|
||||
$localConfigurationArray['SYS']['encryptionKey'] = $randomKey;
|
||||
|
||||
$this->writeLocalConfiguration($localConfigurationArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if access / write to given path in local configuration is allowed.
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return bool TRUE if access is allowed
|
||||
*/
|
||||
protected function isValidLocalConfigurationPath(string $path): bool
|
||||
{
|
||||
// Early return for white listed paths
|
||||
foreach ($this->allowedSettingsPaths as $allowedSettingsPath) {
|
||||
if (str_starts_with($path, $allowedSettingsPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return ArrayUtility::isValidPath($this->getDefaultConfiguration(), $path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to modify or enhance the data structure identifier,
|
||||
* which is used for a given TCA flex field.
|
||||
*
|
||||
* This event can be used to add additional data to an identifier. Be careful here, especially if
|
||||
* stuff from the source record like uid or pid is added! This may easily lead to issues with
|
||||
* data handler details like copy or move records, localization and version overlays.
|
||||
* Test this very well! Multiple listeners may add information to the same identifier here -
|
||||
* take care to namespace array keys. Information added here can be later used in the
|
||||
* data structure related PSR-14 Events (BeforeFlexFormDataStructureParsedEvent and
|
||||
* AfterFlexFormDataStructureParsedEvent) again.
|
||||
*
|
||||
* See the note on FlexFormTools regarding the schema of $dataStructure.
|
||||
*/
|
||||
final class AfterFlexFormDataStructureIdentifierInitializedEvent
|
||||
{
|
||||
/**
|
||||
* @param array $fieldTca Full TCA of the field in question that has type=flex set
|
||||
* @param string $tableName The table name of the TCA field
|
||||
* @param string $fieldName The field name
|
||||
* @param array $row The data row
|
||||
* @param array $identifier The data structure identifier (set by event listener of the default)
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $fieldTca,
|
||||
private readonly string $tableName,
|
||||
private readonly string $fieldName,
|
||||
private readonly array $row,
|
||||
private array $identifier,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the full TCA of the currently handled field, having
|
||||
* `type=flex` set.
|
||||
*/
|
||||
public function getFieldTca(): array
|
||||
{
|
||||
return $this->fieldTca;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return $this->fieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole database row of the current record.
|
||||
*/
|
||||
public function getRow(): array
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to modify or completely replace the initialized data
|
||||
* structure identifier.
|
||||
*/
|
||||
public function setIdentifier(array $identifier): void
|
||||
{
|
||||
$this->identifier = $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initialized data structure identifier, which has
|
||||
* either been defined by an event listener or set to the default
|
||||
* by the `FlexFormTools` component.
|
||||
*/
|
||||
public function getIdentifier(): array
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to modify or enhance a flex form data
|
||||
* structure that corresponds to a given identifier, after it was parsed and
|
||||
* before it is used by further components.
|
||||
*
|
||||
* Note: Since this event is not stoppable, all registered listeners are
|
||||
* called. Therefore, you might want to namespace your identifiers in a way,
|
||||
* that there is little chance they overlap (e.g. prefix with extension name).
|
||||
*
|
||||
* See the note on FlexFormTools regarding the schema of $dataStructure.
|
||||
*/
|
||||
final class AfterFlexFormDataStructureParsedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $dataStructure,
|
||||
private readonly array $identifier,
|
||||
) {}
|
||||
|
||||
public function getIdentifier(): array
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current data structure, which has been processed and
|
||||
* parsed by the `FlexFormTools` component. Might contain additional
|
||||
* data from previously called listeners.
|
||||
*/
|
||||
public function getDataStructure(): array
|
||||
{
|
||||
return $this->dataStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to modify or completely replace the parsed data
|
||||
* structure identifier.
|
||||
*/
|
||||
public function setDataStructure(array $dataStructure): void
|
||||
{
|
||||
$this->dataStructure = $dataStructure;
|
||||
}
|
||||
}
|
||||
@@ -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\Configuration\Event;
|
||||
|
||||
final class AfterRichtextConfigurationPreparedEvent
|
||||
{
|
||||
public function __construct(private array $configuration) {}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public function setConfiguration(array $configuration): void
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Event after $tca which later becomes $GLOBALS['TCA'] has been built.
|
||||
* Allows to further manipulate $tca before it is cached and set as $GLOBALS['TCA'].
|
||||
*/
|
||||
final class AfterTcaCompilationEvent
|
||||
{
|
||||
public function __construct(private array $tca) {}
|
||||
|
||||
public function getTca(): array
|
||||
{
|
||||
return $this->tca;
|
||||
}
|
||||
|
||||
public function setTca(array $tca): void
|
||||
{
|
||||
$this->tca = $tca;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?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\Configuration\Event;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to specify the data structure identifier,
|
||||
* used for a given TCA flex field.
|
||||
*
|
||||
* Listeners should call ->setIdentifier() to set the identifier or ignore the
|
||||
* event to allow other listeners to set it. Do not set an empty string as this
|
||||
* will immediately stop event propagation!
|
||||
*
|
||||
* The identifier SHOULD include the keys specified in the Identifier definition
|
||||
* on FlexFormTools, and nothing else. Adding other keys may or may not work,
|
||||
* depending on other code that is enabled, and they are not guaranteed nor
|
||||
* covered by BC guarantees.
|
||||
*
|
||||
* Warning: If adding source record details like the uid or pid here, this may turn out to be fragile.
|
||||
* Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may
|
||||
* break in between different core versions.
|
||||
* It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see
|
||||
* the core internal 'tca' and 'record' return values below
|
||||
*
|
||||
* See the note on FlexFormTools regarding the schema of $dataStructure.
|
||||
*/
|
||||
final class BeforeFlexFormDataStructureIdentifierInitializedEvent implements StoppableEventInterface
|
||||
{
|
||||
private ?array $identifier = null;
|
||||
|
||||
/**
|
||||
* @param array $fieldTca Full TCA of the field in question that has type=flex set
|
||||
* @param string $tableName The table name of the TCA field
|
||||
* @param string $fieldName The field name
|
||||
* @param array $row The data row
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $fieldTca,
|
||||
private readonly string $tableName,
|
||||
private readonly string $fieldName,
|
||||
private readonly array $row,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the full TCA of the currently handled field, having
|
||||
* `type=flex` set.
|
||||
*/
|
||||
public function getFieldTca(): array
|
||||
{
|
||||
return $this->fieldTca;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return $this->fieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole database row of the current record.
|
||||
*/
|
||||
public function getRow(): array
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to define the data structure identifier for the TCA field.
|
||||
* Setting an identifier will immediately stop propagation. Avoid
|
||||
* setting this parameter to an empty array as this will also stop
|
||||
* propagation.
|
||||
*/
|
||||
public function setIdentifier(array $identifier): void
|
||||
{
|
||||
$this->identifier = $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current data structure identifier, which will always be
|
||||
* `null` for listeners, since the event propagation is
|
||||
* stopped as soon as a listener defines an identifier.
|
||||
*/
|
||||
public function getIdentifier(): ?array
|
||||
{
|
||||
return $this->identifier ?? null;
|
||||
}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return isset($this->identifier);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Configuration\Event;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to specify a flex form data structure that
|
||||
* corresponds to a given identifier.
|
||||
*
|
||||
* Listeners should call ->setDataStructure() to set the data structure (this
|
||||
* can either be a resolved data structure string, a "FILE:" reference or a
|
||||
* fully parsed data structure as array) or ignore the event to allow other
|
||||
* listeners to set it. Do not set an empty array or string as this will
|
||||
* immediately stop event propagation!
|
||||
*
|
||||
* See the note on FlexFormTools regarding the schema of $dataStructure.
|
||||
*/
|
||||
final class BeforeFlexFormDataStructureParsedEvent implements StoppableEventInterface
|
||||
{
|
||||
private array|string|null $dataStructure = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly array $identifier,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the current data structure, which will always be `null`
|
||||
* for listeners, since the event propagation is stopped as soon as
|
||||
* a listener sets a data structure.
|
||||
*/
|
||||
public function getDataStructure(): array|string|null
|
||||
{
|
||||
return $this->dataStructure ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to either set an already parsed data structure as `array`,
|
||||
* a file reference or the XML structure as `string`. Setting a data
|
||||
* structure will immediately stop propagation. Avoid setting this parameter
|
||||
* to an empty array or string as this will also stop propagation.
|
||||
*/
|
||||
public function setDataStructure(array|string $dataStructure): void
|
||||
{
|
||||
$this->dataStructure = $dataStructure;
|
||||
}
|
||||
|
||||
public function getIdentifier(): array
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return isset($this->dataStructure);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Event before $tca which later becomes $GLOBALS['TCA'] is overridden by TCA/Overrides.
|
||||
* Allows to manipulate $tca, before overrides are merged.
|
||||
*/
|
||||
final class BeforeTcaOverridesEvent
|
||||
{
|
||||
public function __construct(private array $tca) {}
|
||||
|
||||
public function getTca(): array
|
||||
{
|
||||
return $this->tca;
|
||||
}
|
||||
|
||||
public function setTca(array $tca): void
|
||||
{
|
||||
$this->tca = $tca;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Event fired before a site configuration is written to a yaml file
|
||||
* allows dynamic modification of the site's configuration before writing.
|
||||
*/
|
||||
final class SiteConfigurationBeforeWriteEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $siteIdentifier,
|
||||
private array $configuration
|
||||
) {}
|
||||
|
||||
public function getSiteIdentifier(): string
|
||||
{
|
||||
return $this->siteIdentifier;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $configuration overwrite the configuration array of the site
|
||||
*/
|
||||
public function setConfiguration(array $configuration): void
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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\Configuration\Event;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class SiteConfigurationChangedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public string $siteIdentifier,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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\Configuration\Event;
|
||||
|
||||
/**
|
||||
* Event after a site configuration has been read from a yaml file
|
||||
* before it is cached - allows dynamic modification of the site's configuration.
|
||||
*/
|
||||
final class SiteConfigurationLoadedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $siteIdentifier,
|
||||
private array $configuration
|
||||
) {}
|
||||
|
||||
public function getSiteIdentifier(): string
|
||||
{
|
||||
return $this->siteIdentifier;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $configuration overwrite the configuration array of the site
|
||||
*/
|
||||
public function setConfiguration(array $configuration): void
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
}
|
||||
+24
@@ -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\Core\Configuration\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if ExtensionConfiguration->get() is called for
|
||||
* an extension that has no configuration.
|
||||
*/
|
||||
class ExtensionConfigurationExtensionNotConfiguredException 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\Core\Configuration\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if ExtensionConfiguration->get() is called with
|
||||
* a path that does not exist within the extension configuration.
|
||||
*/
|
||||
class ExtensionConfigurationPathDoesNotExistException 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\Core\Configuration\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when settings.php cannot be written.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SettingsWriteException extends \RuntimeException {}
|
||||
@@ -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\Core\Configuration\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when a site configuration can not be written.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SiteConfigurationWriteException extends Exception {}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\Configuration\Extension;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
|
||||
/**
|
||||
* @internal Bootstrap related ext_localconf loading. Extensions must not use this.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class ExtLocalconfFactory
|
||||
{
|
||||
public function __construct(
|
||||
private PackageManager $packageManager,
|
||||
#[Autowire(service: 'cache.core')]
|
||||
private PhpFrontend $codeCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute all extension "ext_localconf.php" files of loaded extensions.
|
||||
* Cache to a single file and use if exists.
|
||||
*/
|
||||
public function load(): void
|
||||
{
|
||||
$cacheIdentifier = $this->getExtLocalconfCacheIdentifier();
|
||||
$hasCache = $this->codeCache->require($cacheIdentifier) !== false;
|
||||
if (!$hasCache) {
|
||||
$this->loadSingleExtLocalconfFiles();
|
||||
$this->createCacheEntry();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadUncached(): void
|
||||
{
|
||||
$this->loadSingleExtLocalconfFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cache entry for concatenated ext_localconf.php files
|
||||
*/
|
||||
public function createCacheEntry(): void
|
||||
{
|
||||
$phpCodeToCache = [];
|
||||
// Set same globals as in loadSingleExtLocalconfFiles()
|
||||
$phpCodeToCache[] = '/**';
|
||||
$phpCodeToCache[] = ' * Compiled ext_localconf.php cache file';
|
||||
$phpCodeToCache[] = ' */';
|
||||
// Iterate through loaded extensions and add ext_localconf content
|
||||
foreach ($this->packageManager->getActivePackages() as $package) {
|
||||
$extensionKey = $package->getPackageKey();
|
||||
$extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
|
||||
if (@file_exists($extLocalconfPath)) {
|
||||
// Include a header per extension to make the cache file more readable
|
||||
$phpCodeToCache[] = '/**';
|
||||
$phpCodeToCache[] = ' * Extension: ' . $extensionKey;
|
||||
$phpCodeToCache[] = ' * File: ' . $extLocalconfPath;
|
||||
$phpCodeToCache[] = ' */';
|
||||
// Add ext_localconf.php content of extension
|
||||
$phpCodeToCache[] = 'namespace {';
|
||||
$phpCodeToCache[] = trim((string)file_get_contents($extLocalconfPath));
|
||||
$phpCodeToCache[] = '}';
|
||||
$phpCodeToCache[] = '';
|
||||
$phpCodeToCache[] = '';
|
||||
}
|
||||
}
|
||||
$phpCodeToCache = implode(LF, $phpCodeToCache);
|
||||
// Remove all start and ending php tags from content, and remove strict_types=1 declaration.
|
||||
$phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache);
|
||||
$phpCodeToCache = preg_replace('/declare\\s?+\\(\\s?+strict_types\\s?+=\\s?+1\\s?+\\);/is', '', (string)$phpCodeToCache);
|
||||
$this->codeCache->set($this->getExtLocalconfCacheIdentifier(), $phpCodeToCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require ext_localconf.php files from extensions
|
||||
*/
|
||||
private function loadSingleExtLocalconfFiles(): void
|
||||
{
|
||||
foreach ($this->packageManager->getActivePackages() as $package) {
|
||||
$extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
|
||||
if (file_exists($extLocalconfPath)) {
|
||||
require $extLocalconfPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache identifier of concatenated ext_localconf file
|
||||
*/
|
||||
private function getExtLocalconfCacheIdentifier(): string
|
||||
{
|
||||
return (new PackageDependentCacheIdentifier($this->packageManager))->withPrefix('ext_localconf')->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
|
||||
use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
|
||||
use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* API to get() instance specific extension configuration options.
|
||||
*
|
||||
* Extension authors are encouraged to use this API - it is currently a simple
|
||||
* wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something
|
||||
* different in case core decides to store extension configuration elsewhere.
|
||||
*
|
||||
* Extension authors must not access TYPO3_CONF_VARS['EXTENSIONS'] on their own.
|
||||
*
|
||||
* Extension configurations are often 'feature flags' currently defined by
|
||||
* ext_conf_template.txt files. The core (more specifically the install tool)
|
||||
* takes care default values and overridden values are properly prepared upon
|
||||
* loading or updating an extension.
|
||||
*
|
||||
* Note only ->get() is official API and other public methods are low level
|
||||
* core internal API that is usually only used by extension manager and install tool.
|
||||
*/
|
||||
#[AsAlias('extension-configuration', public: true)]
|
||||
readonly class ExtensionConfiguration
|
||||
{
|
||||
/**
|
||||
* Get a single configuration value, a sub array or the whole configuration.
|
||||
*
|
||||
* Examples:
|
||||
* // Simple and typical usage: Get a single config value, or an array if the key is a "TypoScript"
|
||||
* // a-like sub-path in ext_conf_template.txt "foo.bar = defaultValue"
|
||||
* ->get('myExtension', 'aConfigKey');
|
||||
*
|
||||
* // Get all current configuration values, always an array
|
||||
* ->get('myExtension');
|
||||
*
|
||||
* // Get a nested config value if the path is a "TypoScript" a-like sub-path
|
||||
* // in ext_conf_template.txt "topLevelKey.subLevelKey = defaultValue"
|
||||
* ->get('myExtension', 'topLevelKey/subLevelKey')
|
||||
*
|
||||
* Notes:
|
||||
* - If a configuration or configuration path of an extension is not found, the
|
||||
* code tries to synchronize configuration with ext_conf_template.txt first, only
|
||||
* if still not found, it will throw exceptions.
|
||||
* - Return values are NOT type safe: A boolean false could be returned as string 0.
|
||||
* Cast accordingly.
|
||||
* - This API throws exceptions if the path does not exist or the extension
|
||||
* configuration is not available. The install tool takes care any new
|
||||
* ext_conf_template.txt values are available TYPO3_CONF_VARS['EXTENSIONS'],
|
||||
* a thrown exception indicates a programming error on developer side
|
||||
* and should not be caught.
|
||||
* - It is not checked if the extension in question is loaded at all,
|
||||
* it's just checked the extension configuration path exists.
|
||||
* - Extensions should typically not get configuration of a different extension.
|
||||
*
|
||||
* @param string $extension Extension name
|
||||
* @param string $path Configuration path - e.g. "featureCategory/coolThingIsEnabled"
|
||||
* @return mixed The value. Can be a sub array or a single value.
|
||||
* @throws ExtensionConfigurationExtensionNotConfiguredException If the extension configuration does not exist
|
||||
* @throws ExtensionConfigurationPathDoesNotExistException If a requested path in the extension configuration does not exist
|
||||
*/
|
||||
public function get(string $extension, string $path = ''): mixed
|
||||
{
|
||||
$hasBeenSynchronized = false;
|
||||
if (!$this->hasConfiguration($extension)) {
|
||||
// This if() should not be hit at "casual" runtime, but only in early setup phases
|
||||
$this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true);
|
||||
$hasBeenSynchronized = true;
|
||||
if (!$this->hasConfiguration($extension)) {
|
||||
// If there is still no such entry, even after sync -> throw
|
||||
throw new ExtensionConfigurationExtensionNotConfiguredException(
|
||||
'No extension configuration for extension ' . $extension . ' found. Either this extension'
|
||||
. ' has no extension configuration or the configuration is not up to date. Execute the'
|
||||
. ' install tool to update configuration.',
|
||||
1509654728
|
||||
);
|
||||
}
|
||||
}
|
||||
if (empty($path)) {
|
||||
return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
|
||||
}
|
||||
if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
|
||||
// This if() should not be hit at "casual" runtime, but only in early setup phases
|
||||
if (!$hasBeenSynchronized) {
|
||||
$this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true);
|
||||
}
|
||||
// If there is still no such entry, even after sync -> throw
|
||||
if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
|
||||
throw new ExtensionConfigurationPathDoesNotExistException(
|
||||
'Path ' . $path . ' does not exist in extension configuration',
|
||||
1509977699
|
||||
);
|
||||
}
|
||||
}
|
||||
return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new or overwrite an existing configuration value.
|
||||
*
|
||||
* This is typically used by core internal low level tasks like the install
|
||||
* tool but may become handy if an extension needs to update extension configuration
|
||||
* on the fly for whatever reason.
|
||||
*
|
||||
* Examples:
|
||||
* // Set a full extension configuration ($value could be a nested array, too)
|
||||
* ->set('myExtension', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
|
||||
*
|
||||
* // Unset a whole extension configuration
|
||||
* ->set('myExtension')
|
||||
*
|
||||
* Notes:
|
||||
* - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or
|
||||
* similar). ->set() is not supposed to be called each request since it writes LocalConfiguration
|
||||
* each time. This API is however OK to be called from extension manager hooks.
|
||||
* - Values are not type safe, if the install tool wrote them,
|
||||
* boolean true could become string 1 on ->get()
|
||||
* - It is not possible to store 'null' as value, giving $value=null
|
||||
* or no value at all will unset the path
|
||||
* - Setting a value and calling ->get() afterwards will still return the new value.
|
||||
* - Warning on system/additional.php: If this file overwrites settings, it spoils the
|
||||
* ->set() call and values may not end up as expected.
|
||||
*
|
||||
* @param string $extension Extension name
|
||||
* @param mixed|null $value The value. If null, unset the path
|
||||
* @internal
|
||||
*/
|
||||
public function set(string $extension, mixed $value = null): void
|
||||
{
|
||||
if (empty($extension)) {
|
||||
throw new \RuntimeException('extension name must not be empty', 1509715852);
|
||||
}
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
if ($value === null) {
|
||||
// Remove whole extension config
|
||||
$configurationManager->removeLocalConfigurationKeysByPath(['EXTENSIONS/' . $extension]);
|
||||
if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
|
||||
unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]);
|
||||
}
|
||||
} else {
|
||||
// Set full extension config
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value);
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new configuration of all extensions and reload TYPO3_CONF_VARS.
|
||||
* This is a "do all" variant of set() for all extensions that prevents
|
||||
* writing and loading system/settings.php many times.
|
||||
*
|
||||
* @param array $configuration Configuration of all extensions
|
||||
* @internal
|
||||
*/
|
||||
public function setAll(array $configuration, bool $skipWriteIfLocalConfigurationDoesNotExist = false): void
|
||||
{
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
if ($skipWriteIfLocalConfigurationDoesNotExist === false || @file_exists($configurationManager->getSystemConfigurationFileLocation())) {
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
|
||||
}
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are new config settings in ext_conf_template of an extension,
|
||||
* they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
|
||||
*
|
||||
* Used when entering the install tool, during installation and if calling ->get()
|
||||
* with an extension or path that is not yet found in LocalConfiguration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(bool $skipWriteIfLocalConfigurationDoesNotExist = false): void
|
||||
{
|
||||
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
|
||||
$fullConfiguration = [];
|
||||
$currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? [];
|
||||
foreach ($activePackages as $package) {
|
||||
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
continue;
|
||||
}
|
||||
$extensionKey = $package->getPackageKey();
|
||||
$currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? [];
|
||||
$extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig);
|
||||
if (!empty($extConfTemplateConfiguration)) {
|
||||
$fullConfiguration[$extensionKey] = $extConfTemplateConfiguration;
|
||||
}
|
||||
}
|
||||
// Write new config if changed. Loose array comparison to not write if only array key order is different
|
||||
if ($fullConfiguration != $currentLocalConfiguration) {
|
||||
$this->setAll($fullConfiguration, $skipWriteIfLocalConfigurationDoesNotExist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read values from ext_conf_template, verify if they are in LocalConfiguration.php
|
||||
* already and if not, add them.
|
||||
*
|
||||
* Used public by extension manager when updating extension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey): void
|
||||
{
|
||||
$package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
|
||||
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
return;
|
||||
}
|
||||
$currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? [];
|
||||
$extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
|
||||
// Write new config if changed. Loose array comparison to not write if only array key order is different
|
||||
if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
|
||||
$this->set($extensionKey, $extConfTemplateConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
|
||||
* comments and returns ext_conf_template as array where nested keys have no dots.
|
||||
*/
|
||||
protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
|
||||
{
|
||||
$rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
|
||||
$typoScriptStringFactory = GeneralUtility::makeInstance(TypoScriptStringFactory::class);
|
||||
$typoScriptTree = $typoScriptStringFactory->parseFromString($rawConfigurationString, new AstBuilder(new NoopEventDispatcher()));
|
||||
return GeneralUtility::removeDotsFromTS($typoScriptTree->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Return content of an extensions' ext_conf_template.txt file if
|
||||
* the file exists, empty string if file does not exist.
|
||||
*/
|
||||
protected function getDefaultConfigurationRawString(string $extensionKey): string
|
||||
{
|
||||
$rawString = '';
|
||||
$extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
|
||||
'EXT:' . $extensionKey . '/ext_conf_template.txt'
|
||||
);
|
||||
if (file_exists($extConfTemplateFileLocation)) {
|
||||
$rawString = (string)file_get_contents($extConfTemplateFileLocation);
|
||||
}
|
||||
return $rawString;
|
||||
}
|
||||
|
||||
protected function hasConfiguration(string $extension): bool
|
||||
{
|
||||
return isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
|
||||
/**
|
||||
* A lightweight API class to check if a feature is enabled.
|
||||
*
|
||||
* Features are simple options (true/false), and are stored in the
|
||||
* global configuration array $TYPO3_CONF_VARS[SYS][features].
|
||||
*
|
||||
* For disabling or enabling a feature the "ConfigurationManager"
|
||||
* should be used.
|
||||
*
|
||||
* -- Naming --
|
||||
*
|
||||
* Feature names should NEVER be named "enable" or have a negation, or contain versions or years
|
||||
* "enableFeatureXyz"
|
||||
* "disableOverlays"
|
||||
* "schedulerRevamped"
|
||||
* "useDoctrineQueries"
|
||||
* "disablePreparedStatements"
|
||||
* "disableHooksInFE"
|
||||
*
|
||||
* Proper namings for features
|
||||
* "ExtendedRichtextFormat"
|
||||
* "NativeYamlParser"
|
||||
* "InlinePageTranslations"
|
||||
* "TypoScriptParserIncludesAsXml"
|
||||
* "NativeDoctrineQueries"
|
||||
*
|
||||
* Ideally, these feature switches are added via the Install Tool or via FactoryConfiguration
|
||||
* and can be used for Extensions as well.
|
||||
*
|
||||
* --- Usage ---
|
||||
*
|
||||
* if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('InlineSvg')) {
|
||||
* ... do stuff here ...
|
||||
* }
|
||||
*/
|
||||
#[AsAlias('features', public: true)]
|
||||
readonly class Features
|
||||
{
|
||||
/**
|
||||
* A list of features that are always activated (mainly happens if a previous feature switch is now always
|
||||
* "turned on" to enforce a behaviour, but still valid for extension authors to ensure the feature switch
|
||||
* returns "enabled" for future versions.
|
||||
*/
|
||||
private const array ALWAYS_ACTIVE_FEATURES = [
|
||||
// Enabled since v15.0 at any time.
|
||||
'extbase.consistentDateTimeHandling',
|
||||
// Enabled since v13.0 at any time.
|
||||
'security.usePasswordPolicyForFrontendUsers',
|
||||
'security.backend.enforceContentSecurityPolicy',
|
||||
// Enabled since v12.0 at any time.
|
||||
'subrequestPageErrors',
|
||||
'yamlImportsFollowDeclarationOrder',
|
||||
'security.frontend.htmlSanitizeParseFuncDefault',
|
||||
'runtimeDbQuotingOfTcaConfiguration',
|
||||
// Enabled since v11.0 at any time.
|
||||
'fluidBasedPageModule',
|
||||
// Enabled since v10.0 at any time.
|
||||
'simplifiedControllerActionDispatching',
|
||||
'unifiedPageTranslationHandling',
|
||||
'felogin.extbase',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if a feature is active
|
||||
*
|
||||
* @param string $featureName the name of the feature
|
||||
*/
|
||||
public function isFeatureEnabled(string $featureName): bool
|
||||
{
|
||||
if (in_array($featureName, self::ALWAYS_ACTIVE_FEATURES, true)) {
|
||||
return true;
|
||||
}
|
||||
return isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'][$featureName])
|
||||
&& $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'][$featureName] === true;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Abstract exception thrown if data structures can not be resolved, found or parsed.
|
||||
*/
|
||||
abstract class AbstractInvalidDataStructureException extends Exception {}
|
||||
@@ -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\Configuration\FlexForm\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if parsed data structure contains invalid config like references in sections
|
||||
*/
|
||||
class InvalidDataStructureException extends AbstractInvalidDataStructureException {}
|
||||
@@ -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\Configuration\FlexForm\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if parseFlexFormDataStructureByIdentifier() is given an empty string
|
||||
*/
|
||||
class InvalidIdentifierException extends AbstractInvalidDataStructureException {}
|
||||
@@ -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\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if TCA is invalid.
|
||||
* This may happen if a record is opened that points to a data structure of
|
||||
* a no longer loaded extension
|
||||
*/
|
||||
class InvalidTcaException extends AbstractInvalidDataStructureException {}
|
||||
@@ -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\Configuration\FlexForm\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if data structure can not be determined due to missing TCA schema
|
||||
*/
|
||||
class InvalidTcaSchemaException extends AbstractInvalidDataStructureException {}
|
||||
@@ -0,0 +1,891 @@
|
||||
<?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\Configuration\FlexForm;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidDataStructureException;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaSchemaException;
|
||||
use TYPO3\CMS\Core\Configuration\Tca\TcaMigration;
|
||||
use TYPO3\CMS\Core\Configuration\Tca\TcaPreparation;
|
||||
use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Unified service class for TCA type="flex" operations.
|
||||
*
|
||||
* This service provides comprehensive FlexForm handling capabilities that work with both:
|
||||
* - TCA Schema objects (high-level, schema-aware operations)
|
||||
* - Raw TCA configuration arrays (low-level operations during schema building)
|
||||
*
|
||||
* Note: Using the raw TCA configuration is not recommended and only available to support
|
||||
* FlexFormTools during schema building. For extensions this might be the case on using
|
||||
* {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
|
||||
*
|
||||
* Usage examples:
|
||||
* ```php
|
||||
* // With TCA Schema (typical application usage)
|
||||
* $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $tcaSchema);
|
||||
*
|
||||
* // With raw TCA array (only during schema)
|
||||
* $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $rawTcaArray);
|
||||
* ```
|
||||
*
|
||||
* The service automatically detects the input type and uses the appropriate resolution strategy.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class FlexFormTools
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private TcaMigration $tcaMigration,
|
||||
private TcaPreparation $tcaPreparation,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The method locates a specific data structure from given TCA and row combination
|
||||
* and returns an identifier string that can be handed around, and can be resolved
|
||||
* to a single data structure later without giving $row and $tca data again.
|
||||
*
|
||||
* Note: The returned syntax is meant to only specify the target location of the data structure.
|
||||
* It SHOULD NOT be abused and enriched with data from the record that is dealt with. For
|
||||
* instance, it is not allowed to add source record specific date like the "uid" or the "pid"!
|
||||
* If that is done, it is up to the hook consumer to take care of possible side effects, e.g. if
|
||||
* the DataHandler copies or moves records around and those references change.
|
||||
*
|
||||
* This method gets: Source data that influences the target location of a data structure
|
||||
* This method returns: Target specification of the data structure
|
||||
*
|
||||
* This method is "paired" with method parseDataStructureByIdentifier() that
|
||||
* will resolve the returned syntax again and returns the data structure itself.
|
||||
*
|
||||
* Both methods can be extended via events to return and accept additional
|
||||
* identifier strings if needed, and to transmit further information within the identifier strings.
|
||||
*
|
||||
* Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
|
||||
* as the "ds" config is a string, containing the data structure or a file pointer.
|
||||
*
|
||||
* Note: This method and the resolving methods below are well unit tested and document all
|
||||
* nasty details this way.
|
||||
*
|
||||
* @param array $fieldTca Full TCA of the field in question that has type=flex set
|
||||
* @param string $tableName The table name of the TCA field
|
||||
* @param string $fieldName The field name
|
||||
* @param array $row The data row
|
||||
* @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
|
||||
* case handling is done via events. Otherwise, this will throw an exception
|
||||
* on resolving the default identifier {@see InvalidTcaSchemaException}.
|
||||
* Using the raw TCA configuration is furthermore not recommended and only
|
||||
* available to support FlexFormTools during schema building. For extensions
|
||||
* this might be the case on using {@see BeforeTcaOverridesEvent} or
|
||||
* {@see AfterTcaCompilationEvent}.
|
||||
*
|
||||
* @return string Identifier JSON string
|
||||
* @throws \RuntimeException If TCA is misconfigured
|
||||
* @throws InvalidTcaException
|
||||
*/
|
||||
public function getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): string
|
||||
{
|
||||
$dataStructureIdentifier = $this->eventDispatcher
|
||||
->dispatch(new BeforeFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row))
|
||||
->getIdentifier() ?? $this->getDefaultDataStructureIdentifier($tableName, $fieldName, $row, $schema);
|
||||
$dataStructureIdentifier = $this->eventDispatcher
|
||||
->dispatch(new AfterFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row, $dataStructureIdentifier))
|
||||
->getIdentifier();
|
||||
return json_encode($dataStructureIdentifier, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a data structure identified by $identifier to the final data structure array.
|
||||
* This method is called after getDataStructureIdentifier(), finds the data structure
|
||||
* and returns it.
|
||||
*
|
||||
* Events allow to manipulate the find logic and to post process the data structure array.
|
||||
*
|
||||
* Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
|
||||
* as the "ds" config is a string, containing the data structure or a file pointer.
|
||||
*
|
||||
* After the data structure definition is found, the method resolves:
|
||||
* - FILE:EXT: prefix of the data structure itself - the ds is in a file
|
||||
* - FILE:EXT: prefix for sheets - if single sheets are in files
|
||||
* - Create a sDEF sheet if the data structure has non, yet.
|
||||
* - TCA Migration and Preparation is done for the resolved fields
|
||||
*
|
||||
* After that method is run, the data structure is fully resolved to an array,
|
||||
* and same base normalization is done: If the ds did not contain a sheet,
|
||||
* it will have one afterward as "sDEF".
|
||||
*
|
||||
* This method gets: Target specification of the data structure.
|
||||
* This method returns: The normalized data structure parsed to an array.
|
||||
*
|
||||
* @param string $identifier JSON string to find the data structure location
|
||||
* @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
|
||||
* case handling is done via events. Otherwise, this will throw an exception
|
||||
* on resolving the default identifier {@see InvalidTcaSchemaException}.
|
||||
* Using the raw TCA configuration is furthermore not recommended and only
|
||||
* available to support FlexFormTools during schema building. For extensions
|
||||
* this might be the case on using {@see BeforeTcaOverridesEvent} or
|
||||
* {@see AfterTcaCompilationEvent}.
|
||||
*
|
||||
* @return array Parsed and normalized data structure
|
||||
* @throws InvalidIdentifierException
|
||||
* @throws InvalidTcaSchemaException
|
||||
* @throws InvalidDataStructureException
|
||||
*/
|
||||
public function parseDataStructureByIdentifier(string $identifier, array|TcaSchema|null $schema = null): array
|
||||
{
|
||||
// Throw an exception for an empty string. This might be a valid use case for new
|
||||
// records in some situations, so this is catchable to give callers a chance to deal with that.
|
||||
if ($identifier === '') {
|
||||
throw new InvalidIdentifierException(
|
||||
'Empty string given to parseDataStructureByIdentifier(). This exception might '
|
||||
. ' be caught to handle some new record situations properly',
|
||||
1478100828
|
||||
);
|
||||
}
|
||||
$parsedIdentifier = json_decode($identifier, true);
|
||||
if (!is_array($parsedIdentifier) || $parsedIdentifier === []) {
|
||||
// If there is some identifier and it can't be decoded, programming error -> not catchable
|
||||
throw new \RuntimeException(
|
||||
'Identifier could not be decoded to an array.',
|
||||
1478345642
|
||||
);
|
||||
}
|
||||
$dataStructure = $this->eventDispatcher
|
||||
->dispatch(new BeforeFlexFormDataStructureParsedEvent($parsedIdentifier))
|
||||
->getDataStructure() ?? $this->getDefaultStructureForIdentifier($parsedIdentifier, $schema);
|
||||
$dataStructure = $this->convertDataStructureToArray($dataStructure);
|
||||
$dataStructure = $this->ensureDefaultSheet($dataStructure);
|
||||
$dataStructure = $this->resolveFileDirectives($dataStructure);
|
||||
$dataStructure = $this->checkMigratePrepareFlexTca($dataStructure);
|
||||
return $this->eventDispatcher
|
||||
->dispatch(new AfterFlexFormDataStructureParsedEvent($dataStructure, $parsedIdentifier))
|
||||
->getDataStructure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up FlexForm value XML to hold only the values it may according to its Data Structure.
|
||||
* The order of tags will follow that of the data structure.
|
||||
*
|
||||
* @param array|TcaSchema $schema Main schema only, no sub schema! Using the raw TCA configuration is
|
||||
* furthermore not recommended and only available to support FlexFormTools
|
||||
* during schema building. For extensions this might be the case on
|
||||
* using {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
|
||||
*
|
||||
* @internal Signature may change, for instance to split 'DS finding' and flexArray2Xml(),
|
||||
* which would allow broader use of the method. It is currently consumed by
|
||||
* cleanup:flexforms CLI only.
|
||||
*/
|
||||
public function cleanFlexFormXML(string $table, string $field, array $row, array|TcaSchema $schema): string
|
||||
{
|
||||
if ((is_array($schema) && !isset($schema['columns'][$field]['config'])) || ($schema instanceof TcaSchema && !$schema->hasField($field)) || !isset($row[$field])) {
|
||||
throw new \RuntimeException('Can not clean up FlexForm XML for a column not declared in TCA or not in record.', 1697554398);
|
||||
}
|
||||
try {
|
||||
$fieldTca = is_array($schema) ? ['config' => $schema['columns'][$field]['config']] : ['config' => $schema->getField($field)->getConfiguration()];
|
||||
$dataStructureArray = $this->parseDataStructureByIdentifier($this->getDataStructureIdentifier($fieldTca, $table, $field, $row, $schema), $schema);
|
||||
} catch (InvalidIdentifierException) {
|
||||
// Data structure can not be resolved or parsed. Reset value to empty string.
|
||||
return '';
|
||||
}
|
||||
$valueArray = GeneralUtility::xml2array($row[$field]);
|
||||
if (!is_array($valueArray)) {
|
||||
// Current flex form values can not be parsed to an array. The entire thing is invalid. Reset to empty string.
|
||||
return '';
|
||||
}
|
||||
if (!is_array($dataStructureArray['sheets'] ?? false)) {
|
||||
// We might return empty string instead of throwing here, unsure.
|
||||
throw new \RuntimeException('Data structure should always declare at least one sheet', 1697555523);
|
||||
}
|
||||
$newValueArray = [];
|
||||
foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
|
||||
foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementData) {
|
||||
// For all elements allowed in Data Structure.
|
||||
if (($sheetElementData['type'] ?? '') === 'array') {
|
||||
// This is a section.
|
||||
if (!is_array($sheetElementData['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) {
|
||||
// No possible containers defined for this section in DS, or no values set for this section.
|
||||
continue;
|
||||
}
|
||||
foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) {
|
||||
// We have containers for this section in values.
|
||||
if (!is_array($valueSectionContainers ?? false)) {
|
||||
// Values don't validate to an array, skip.
|
||||
continue;
|
||||
}
|
||||
foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) {
|
||||
// For all value containers in this section.
|
||||
if (!is_array($sheetElementData['el'][$valueContainerType]['el'] ?? false)) {
|
||||
// There is no DS for this container type, skip.
|
||||
continue;
|
||||
}
|
||||
foreach (array_keys($sheetElementData['el'][$valueContainerType]['el']) as $containerElement) {
|
||||
// Container type of this value container exists in DS. Iterate DS container to pick allowed single elements.
|
||||
if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) {
|
||||
$newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF']
|
||||
= $valueContainerElements['el'][$containerElement]['vDEF'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($valueSectionContainers['_TOGGLE'])) {
|
||||
// This was removed in TYPO3 v13, see #102551
|
||||
unset($newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey]['_TOGGLE']);
|
||||
}
|
||||
}
|
||||
} elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) {
|
||||
// Not a section but a simple field. Keep value if set.
|
||||
$newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'] = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->flexArray2Xml($newValueArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert FlexForm data array to XML
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function flexArray2Xml(array $array): string
|
||||
{
|
||||
// Map the weird keys from the internal array to tags and attributes.
|
||||
$options = [
|
||||
'parentTagMap' => [
|
||||
'data' => 'sheet',
|
||||
'sheet' => 'language',
|
||||
'language' => 'field',
|
||||
'el' => 'field',
|
||||
'field' => 'value',
|
||||
'field:el' => 'el',
|
||||
'el:_IS_NUM' => 'section',
|
||||
'section' => 'itemType',
|
||||
],
|
||||
'disableTypeAttrib' => 2,
|
||||
];
|
||||
return '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF
|
||||
. GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the flexForm XML string and converts it to an array.
|
||||
* The resulting array will be multidimensional, as a value "bla.blubb"
|
||||
* results in two levels, and a value "bla.blubb.bla" results in three levels.
|
||||
*/
|
||||
public function convertFlexFormContentToArray(string $flexFormContent): array
|
||||
{
|
||||
$settings = [];
|
||||
$flexFormArray = GeneralUtility::xml2array($flexFormContent);
|
||||
$flexFormArray = $flexFormArray['data'] ?? [];
|
||||
foreach ($flexFormArray as $languages) {
|
||||
if (!is_array($languages['lDEF'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($languages['lDEF'] as $valueKey => $valueDefinition) {
|
||||
if (!str_contains($valueKey, '.')) {
|
||||
$settings[$valueKey] = $this->walkFlexFormNode($valueDefinition);
|
||||
} else {
|
||||
$valueKeyParts = explode('.', $valueKey);
|
||||
$currentNode = &$settings;
|
||||
foreach ($valueKeyParts as $valueKeyPart) {
|
||||
$currentNode = &$currentNode[$valueKeyPart];
|
||||
}
|
||||
if (is_array($valueDefinition)) {
|
||||
if (array_key_exists('vDEF', $valueDefinition)) {
|
||||
$currentNode = $valueDefinition['vDEF'];
|
||||
} else {
|
||||
$currentNode = $this->walkFlexFormNode($valueDefinition);
|
||||
}
|
||||
} else {
|
||||
$currentNode = $valueDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the flexForm XML string and converts it to an array.
|
||||
* The resulting array will be multidimensional. Sheets are
|
||||
* respected to support property paths in multiple sheets.
|
||||
*
|
||||
* A value such as "settings.pageId" results in three levels:
|
||||
* "'sDEF' => ['settings' => ['pageId' => 123]]" and a value such
|
||||
* as "settings.storages.newsPid" results in four levels:
|
||||
* "'sDEF' => ['settings' => ['storages' => ['newsPid' => 123]]]"
|
||||
*
|
||||
* @param string $flexFormContent flexForm xml string
|
||||
*/
|
||||
public function convertFlexFormContentToSheetsArray(string $flexFormContent): array
|
||||
{
|
||||
$settings = [];
|
||||
$flexFormArray = GeneralUtility::xml2array($flexFormContent);
|
||||
$flexFormArray = $flexFormArray['data'] ?? [];
|
||||
foreach ($flexFormArray as $sheetName => $sheet) {
|
||||
foreach ($sheet as $language => $fields) {
|
||||
if ($language !== 'lDEF') {
|
||||
continue;
|
||||
}
|
||||
foreach ($fields as $valueKey => $valueDefinition) {
|
||||
if (!str_contains($valueKey, '.')) {
|
||||
$settings[$sheetName][$valueKey] = $this->walkFlexFormNode($valueDefinition);
|
||||
} else {
|
||||
$valueKeyParts = explode('.', $valueKey);
|
||||
$currentNode = &$settings[$sheetName];
|
||||
foreach ($valueKeyParts as $valueKeyPart) {
|
||||
$currentNode = &$currentNode[$valueKeyPart];
|
||||
}
|
||||
if (is_array($valueDefinition)) {
|
||||
if (array_key_exists('vDEF', $valueDefinition)) {
|
||||
$currentNode = $valueDefinition['vDEF'];
|
||||
} else {
|
||||
$currentNode = $this->walkFlexFormNode($valueDefinition);
|
||||
}
|
||||
} else {
|
||||
$currentNode = $valueDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds data structure in TCA, defined in column config 'ds'
|
||||
*
|
||||
* fieldTca = [
|
||||
* 'config' => [
|
||||
* 'type' => 'flex',
|
||||
* 'ds' => '<T3DataStructure>...' OR 'FILE:...',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* This method returns an array of the form:
|
||||
* [
|
||||
* 'type' => 'tca',
|
||||
* 'tableName' => $tableName,
|
||||
* 'fieldName' => $fieldName,
|
||||
* 'dataStructureKey' => $key,
|
||||
* ];
|
||||
*
|
||||
* Example:
|
||||
* [
|
||||
* 'type' => 'tca',
|
||||
* 'tableName' => 'tt_content',
|
||||
* 'fieldName' => 'pi_flexform',
|
||||
* 'dataStructureKey' => 'default',
|
||||
* ];
|
||||
*
|
||||
* In case the TCA table supports record types and the given $row uses a record type with a custom
|
||||
* data structure (via columnsOverrides) the record type is used as "dataStructureKey".
|
||||
*
|
||||
* Example:
|
||||
* [
|
||||
* 'type' => 'tca',
|
||||
* 'tableName' => 'tt_content',
|
||||
* 'fieldName' => 'pi_flexform',
|
||||
* 'dataStructureKey' => 'powermail_pi1',
|
||||
* ];
|
||||
*
|
||||
* @return array Identifier as array, see example above
|
||||
* @throws InvalidTcaException
|
||||
* @throws InvalidTcaSchemaException
|
||||
*/
|
||||
protected function getDefaultDataStructureIdentifier(string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): array
|
||||
{
|
||||
if ($schema === null) {
|
||||
throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182123);
|
||||
}
|
||||
|
||||
$defaultIdentifier = [
|
||||
'type' => 'tca',
|
||||
'tableName' => $tableName,
|
||||
'fieldName' => $fieldName,
|
||||
'dataStructureKey' => null,
|
||||
];
|
||||
|
||||
return is_array($schema)
|
||||
? $this->getDataStructureIdentifierFromRawTca($schema, $tableName, $fieldName, $row, $defaultIdentifier)
|
||||
: $this->getDataStructureIdentifierFromTcaSchema($schema, $tableName, $fieldName, $row, $defaultIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the data structure from TCA - defined in column config 'ds'
|
||||
*
|
||||
* fieldTca = [
|
||||
* 'config' => [
|
||||
* 'type' => 'flex',
|
||||
* 'ds' => '<T3DataStructure>...' OR 'FILE:...',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Based on an identifier, e.g.:
|
||||
* [
|
||||
* 'type' => 'tca',
|
||||
* 'tableName' => 'tt_content',
|
||||
* 'fieldName' => 'pi_flexform',
|
||||
* 'dataStructureKey' => 'default',
|
||||
* ];
|
||||
*
|
||||
* this method returns '<T3DataStructure>...' OR 'FILE:...'.
|
||||
*
|
||||
* In case the TCA table supports record types and the "dataStructureKey" points to a record type,
|
||||
* which is only the case if a record type defines a custom flex config (via columnsOverrides), this
|
||||
* custom data structure is returned.
|
||||
*
|
||||
* @return string resolved data structure
|
||||
* @throws InvalidTcaSchemaException
|
||||
*/
|
||||
protected function getDefaultStructureForIdentifier(array $identifier, array|TcaSchema|null $schema = null): string
|
||||
{
|
||||
// For the default only type "tca" is handled. Custom types need to be handled by corresponding events.
|
||||
if (($identifier['type'] ?? '') !== 'tca') {
|
||||
throw new InvalidIdentifierException(
|
||||
'Identifier ' . json_encode($identifier) . ' could not be resolved',
|
||||
1478104554
|
||||
);
|
||||
}
|
||||
|
||||
$tableName = (string)($identifier['tableName'] ?? '');
|
||||
$fieldName = (string)($identifier['fieldName'] ?? '');
|
||||
$dataStructureKey = (string)($identifier['dataStructureKey'] ?? '');
|
||||
|
||||
if ($tableName === '' || $fieldName === '' || $dataStructureKey === '') {
|
||||
throw new \RuntimeException(
|
||||
'Incomplete "tca" based identifier: ' . json_encode($identifier),
|
||||
1478113471
|
||||
);
|
||||
}
|
||||
|
||||
if ($schema === null) {
|
||||
throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182125);
|
||||
}
|
||||
|
||||
$dataStructure = is_array($schema)
|
||||
? $this->resolveDataStructureFromRawTca($schema, $fieldName, $dataStructureKey)
|
||||
: $this->resolveDataStructureFromTcaSchema($schema, $tableName, $fieldName, $dataStructureKey);
|
||||
|
||||
if ($dataStructure === '') {
|
||||
throw new InvalidIdentifierException(
|
||||
'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid data structure',
|
||||
1732199538
|
||||
);
|
||||
}
|
||||
|
||||
return $dataStructure;
|
||||
}
|
||||
|
||||
protected function convertDataStructureToArray(string|array $dataStructure): array
|
||||
{
|
||||
if (is_array($dataStructure)) {
|
||||
return $dataStructure;
|
||||
}
|
||||
// Resolve FILE: prefix pointing to a DS in a file
|
||||
if (str_starts_with(trim($dataStructure), 'FILE:')) {
|
||||
$fileName = substr(trim($dataStructure), 5);
|
||||
$file = GeneralUtility::getFileAbsFileName($fileName);
|
||||
if (empty($file) || !is_file($file)) {
|
||||
throw new InvalidIdentifierException(
|
||||
'Data structure file "' . $fileName . '" could not be resolved to an existing file',
|
||||
1478105826
|
||||
);
|
||||
}
|
||||
$dataStructure = (string)file_get_contents($file);
|
||||
}
|
||||
// Parse main structure
|
||||
$dataStructure = GeneralUtility::xml2array($dataStructure);
|
||||
// Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
|
||||
// This also may happen if artificial identifiers were constructed which don't resolve. The
|
||||
// flex form "exclude" access rights systems does that -> catchable
|
||||
if (!is_array($dataStructure)) {
|
||||
throw new InvalidIdentifierException(
|
||||
'Parse error: Data structure could not be resolved to a valid structure.',
|
||||
1478106090
|
||||
);
|
||||
}
|
||||
|
||||
return $dataStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a data structure has a default sheet, and no duplicate data
|
||||
*/
|
||||
protected function ensureDefaultSheet(array $dataStructure): array
|
||||
{
|
||||
if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
|
||||
throw new \RuntimeException(
|
||||
'Parsed data structure has both ROOT and sheets on top level. That is invalid.',
|
||||
1440676540
|
||||
);
|
||||
}
|
||||
if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
|
||||
$dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
|
||||
unset($dataStructure['ROOT']);
|
||||
}
|
||||
return $dataStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve FILE:EXT and EXT: for single sheets
|
||||
*/
|
||||
protected function resolveFileDirectives(array $dataStructure): array
|
||||
{
|
||||
if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
|
||||
foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
|
||||
if (!is_array($sheetStructure)) {
|
||||
if (str_starts_with(trim($sheetStructure), 'FILE:')) {
|
||||
$file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
|
||||
} else {
|
||||
$file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
|
||||
}
|
||||
if ($file && @is_file($file)) {
|
||||
$sheetStructure = GeneralUtility::xml2array((string)file_get_contents($file));
|
||||
}
|
||||
}
|
||||
$dataStructure['sheets'][$sheetName] = $sheetStructure;
|
||||
}
|
||||
}
|
||||
return $dataStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for invalid flex form structures, migrate and prepare single fields.
|
||||
* @throws InvalidDataStructureException
|
||||
*/
|
||||
private function checkMigratePrepareFlexTca(array $dataStructure): array
|
||||
{
|
||||
if (!is_array($dataStructure['sheets'] ?? null)) {
|
||||
return $dataStructure;
|
||||
}
|
||||
$newStructure = $dataStructure;
|
||||
foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
|
||||
if (!is_array($sheetStructure['ROOT']['el'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($sheetStructure['ROOT']['el'] as $sheetElementName => $sheetElementConfig) {
|
||||
if (!is_array($sheetElementConfig)) {
|
||||
continue;
|
||||
}
|
||||
if (($sheetElementConfig['type'] ?? null) === 'array' xor ($sheetElementConfig['section'] ?? null) === '1') {
|
||||
// Section element, but type=array without section=1 or vice versa is not ok
|
||||
throw new InvalidDataStructureException(
|
||||
'Broken data structure on field name ' . $sheetElementName . '. section without type or vice versa is not allowed',
|
||||
1440685208
|
||||
);
|
||||
}
|
||||
if (($sheetElementConfig['type'] ?? null) === 'array' && ($sheetElementConfig['section'] ?? null) === '1') {
|
||||
// Section element
|
||||
if (!is_array($sheetElementConfig['el'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($sheetElementConfig['el'] as $containerName => $containerConfig) {
|
||||
if (!is_array($containerConfig['el'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($containerConfig['el'] as $containerElementName => $containerElementConfig) {
|
||||
if (!is_array($containerElementConfig)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
// inline, file, group and category are always DB relations
|
||||
in_array($containerElementConfig['config']['type'] ?? [], ['inline', 'file', 'folder', 'group', 'category'], true)
|
||||
// MM is not allowed (usually type=select, otherwise the upper check should kick in)
|
||||
|| isset($containerElementConfig['config']['MM'])
|
||||
// foreign_table is not allowed (usually type=select, otherwise the upper check should kick in)
|
||||
|| isset($containerElementConfig['config']['foreign_table'])
|
||||
) {
|
||||
// Nesting types that use DB relations in container sections is not supported.
|
||||
throw new InvalidDataStructureException(
|
||||
'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
|
||||
. ' in section container "' . $containerName . '": Nesting elements that have database relations in flex form'
|
||||
. ' sections is not allowed.',
|
||||
1458745468
|
||||
);
|
||||
}
|
||||
if (($containerElementConfig['type'] ?? null) === 'array' && ($containerElementConfig['section'] ?? null) === '1') {
|
||||
// Nesting sections is not supported. Throw an exception if configured.
|
||||
throw new InvalidDataStructureException(
|
||||
'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
|
||||
. ' in section container "' . $containerName . '": Nesting sections in container elements'
|
||||
. ' sections is not allowed.',
|
||||
1458745712
|
||||
);
|
||||
}
|
||||
$containerElementConfig = $this->migrateFlexField($containerElementName, $containerElementConfig);
|
||||
$containerElementConfig = $this->prepareFlexField($containerElementName, $containerElementConfig);
|
||||
$newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName]['el'][$containerName]['el'][$containerElementName] = $containerElementConfig;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal element
|
||||
$sheetElementConfig = $this->migrateFlexField($sheetElementName, $sheetElementConfig);
|
||||
$sheetElementConfig = $this->prepareFlexField($sheetElementName, $sheetElementConfig);
|
||||
$newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName] = $sheetElementConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $newStructure;
|
||||
}
|
||||
|
||||
private function migrateFlexField(string $fieldName, array $fieldConfig): array
|
||||
{
|
||||
// TcaMigration of this field. Call the TcaMigration and log any deprecations.
|
||||
$dummyTca = [
|
||||
'dummyTable' => [
|
||||
'columns' => [
|
||||
$fieldName => $fieldConfig,
|
||||
],
|
||||
],
|
||||
];
|
||||
$tcaProcessingResult = $this->tcaMigration->migrate($dummyTca);
|
||||
// Messages are reset on each `migrate()` execution
|
||||
$messages = $tcaProcessingResult->getMessages();
|
||||
if (!empty($messages)) {
|
||||
$context = 'FlexFormTools did an on-the-fly migration of a flex form data structure. This is deprecated and will be removed.'
|
||||
. ' Merge the following changes into the flex form definition "' . $fieldName . '":';
|
||||
array_unshift($messages, $context);
|
||||
trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
|
||||
}
|
||||
return $tcaProcessingResult->getTca()['dummyTable']['columns'][$fieldName];
|
||||
}
|
||||
|
||||
private function prepareFlexField(string $fieldName, array $fieldConfig): array
|
||||
{
|
||||
$dummyTca = [
|
||||
'dummyTable' => [
|
||||
'columns' => [
|
||||
$fieldName => $fieldConfig,
|
||||
],
|
||||
],
|
||||
];
|
||||
$preparedTca = $this->tcaPreparation->prepare($dummyTca, true);
|
||||
return $preparedTca['dummyTable']['columns'][$fieldName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve data structure identifier from raw TCA configuration.
|
||||
*/
|
||||
private function getDataStructureIdentifierFromRawTca(array $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
|
||||
{
|
||||
// Check for record type specific configuration
|
||||
if (isset($schema['ctrl']['type'])) {
|
||||
$recordType = $row[$schema['ctrl']['type']] ?? '';
|
||||
if (isset($schema['types'][$recordType]) && ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $recordType, $fieldName)) !== []) {
|
||||
if ($fieldConfig['config']['type'] === 'flex' && $fieldConfig['config']['ds'] !== '') {
|
||||
$defaultIdentifier['dataStructureKey'] = $recordType;
|
||||
return $defaultIdentifier;
|
||||
}
|
||||
|
||||
throw new InvalidTcaException(
|
||||
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
|
||||
. ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
|
||||
1751796941
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to base field configuration
|
||||
$baseField = $schema['columns'][$fieldName]['config'] ?? [];
|
||||
if (($baseField['type'] ?? '') === 'flex' && ($baseField['ds'] ?? '') !== '') {
|
||||
$defaultIdentifier['dataStructureKey'] = 'default';
|
||||
return $defaultIdentifier;
|
||||
}
|
||||
|
||||
throw new InvalidTcaException(
|
||||
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
|
||||
. ' The field is either not configured as type="flex" or no valid data structure is defined.',
|
||||
1732198005
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve data structure identifier from TCA Schema.
|
||||
*/
|
||||
private function getDataStructureIdentifierFromTcaSchema(TcaSchema $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
|
||||
{
|
||||
if ($schema->getName() !== $tableName) {
|
||||
throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $tableName . ' from data structure identifier.', 1753182124);
|
||||
}
|
||||
|
||||
// Check for record type specific configuration
|
||||
if ($schema->supportsSubSchema()) {
|
||||
$recordType = (string)($row[$schema->getSubSchemaTypeInformation()->getFieldName()] ?? '');
|
||||
if ($recordType !== '' && $schema->hasSubSchema($recordType) && ($subSchema = $schema->getSubSchema($recordType))->hasField($fieldName)) {
|
||||
$flexField = $subSchema->getField($fieldName);
|
||||
if ($flexField instanceof FlexFormFieldType && $flexField->getDataStructure() !== '') {
|
||||
$defaultIdentifier['dataStructureKey'] = $recordType;
|
||||
return $defaultIdentifier;
|
||||
}
|
||||
|
||||
throw new InvalidTcaException(
|
||||
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
|
||||
. ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
|
||||
1751796940
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to base field
|
||||
$baseField = $schema->getField($fieldName);
|
||||
if ($baseField instanceof FlexFormFieldType && $baseField->getDataStructure() !== '') {
|
||||
$defaultIdentifier['dataStructureKey'] = 'default';
|
||||
return $defaultIdentifier;
|
||||
}
|
||||
|
||||
throw new InvalidTcaException(
|
||||
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
|
||||
. ' The field is either not configured as type="flex" or no valid data structure is defined.',
|
||||
1732198004
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve data structure from raw TCA configuration.
|
||||
*/
|
||||
private function resolveDataStructureFromRawTca(array $schema, string $fieldName, string $dataStructureKey): string
|
||||
{
|
||||
// Try record type specific configuration first
|
||||
if (isset($schema['ctrl']['type'], $schema['types'][$dataStructureKey])
|
||||
&& ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $dataStructureKey, $fieldName)) !== []
|
||||
&& ($fieldConfig['config']['type'] ?? '') === 'flex'
|
||||
&& is_string($fieldConfig['config']['ds'] ?? false)
|
||||
) {
|
||||
return $fieldConfig['config']['ds'];
|
||||
}
|
||||
|
||||
// Fall back to default configuration
|
||||
if ($dataStructureKey === 'default') {
|
||||
$baseField = $schema['columns'][$fieldName]['config'] ?? [];
|
||||
if (($baseField['type'] ?? '') === 'flex' && is_string($baseField['ds'] ?? false)) {
|
||||
return $baseField['ds'];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve data structure from TCA Schema.
|
||||
*/
|
||||
private function resolveDataStructureFromTcaSchema(TcaSchema $schema, string $table, string $field, string $dataStructureKey): string
|
||||
{
|
||||
if ($schema->getName() !== $table) {
|
||||
throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $table . ' from data structure identifier.', 1753182126);
|
||||
}
|
||||
|
||||
// Try record type specific configuration first
|
||||
if ($schema->supportsSubSchema()
|
||||
&& $schema->hasSubSchema($dataStructureKey)
|
||||
&& ($subSchema = $schema->getSubSchema($dataStructureKey))->hasField($field)
|
||||
&& ($flexField = $subSchema->getField($field)) instanceof FlexFormFieldType
|
||||
) {
|
||||
return $flexField->getDataStructure();
|
||||
}
|
||||
|
||||
// Fall back to default configuration
|
||||
if ($dataStructureKey === 'default' && ($flexField = $schema->getField($field)) instanceof FlexFormFieldType) {
|
||||
return $flexField->getDataStructure();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record type specific configuration, also already taking columnsOverrides into account.
|
||||
* In case the field is not defined for the record type, no configuration is returned.
|
||||
*/
|
||||
protected function getRecordTypeSpecificFieldConfig(array $tcaForTable, string $recordType, string $fieldName): array
|
||||
{
|
||||
$recordTypeConfig = $tcaForTable['types'][$recordType];
|
||||
$showItemArray = GeneralUtility::trimExplode(',', $recordTypeConfig['showitem'] ?? '', true);
|
||||
foreach ($showItemArray as $aShowItemFieldString) {
|
||||
[$name, , $paletteName] = GeneralUtility::trimExplode(';', $aShowItemFieldString . ';;;');
|
||||
if ($name === '--div--') {
|
||||
continue;
|
||||
}
|
||||
if ($name === '--palette--' && !empty($paletteName)) {
|
||||
if (!isset($tcaForTable['palettes'][$paletteName]['showitem'])) {
|
||||
continue;
|
||||
}
|
||||
$palettesArray = GeneralUtility::trimExplode(',', $tcaForTable['palettes'][$paletteName]['showitem']);
|
||||
foreach ($palettesArray as $aPalettesString) {
|
||||
[$name] = GeneralUtility::trimExplode(';', $aPalettesString . ';;');
|
||||
if ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
|
||||
return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
|
||||
}
|
||||
}
|
||||
} elseif ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
|
||||
return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a flexForm node recursively and takes care of sections etc.
|
||||
* Helper method of convertFlexFormContentToArray() and convertFlexFormContentToSheetsArray().
|
||||
*/
|
||||
private function walkFlexFormNode(mixed $nodeArray): mixed
|
||||
{
|
||||
if (!is_array($nodeArray)) {
|
||||
return $nodeArray;
|
||||
}
|
||||
$result = [];
|
||||
foreach ($nodeArray as $nodeKey => $nodeValue) {
|
||||
if ($nodeKey === 'vDEF') {
|
||||
return $nodeValue;
|
||||
}
|
||||
if (in_array($nodeKey, ['el', '_arrayContainer'])) {
|
||||
return $this->walkFlexFormNode($nodeValue);
|
||||
}
|
||||
if (($nodeKey[0] ?? '') === '_') {
|
||||
continue;
|
||||
}
|
||||
if (strpos((string)$nodeKey, '.')) {
|
||||
$nodeKeyParts = explode('.', $nodeKey);
|
||||
$currentNode = &$result;
|
||||
$nodeKeyPartsCount = count($nodeKeyParts);
|
||||
for ($i = 0; $i < $nodeKeyPartsCount - 1; $i++) {
|
||||
$currentNode = &$currentNode[$nodeKeyParts[$i]];
|
||||
}
|
||||
$newNode = [next($nodeKeyParts) => $nodeValue];
|
||||
$subVal = $this->walkFlexFormNode($newNode);
|
||||
$currentNode[key($subVal)] = current($subVal);
|
||||
} elseif (is_array($nodeValue)) {
|
||||
if (array_key_exists('vDEF', $nodeValue)) {
|
||||
$result[$nodeKey] = $nodeValue['vDEF'];
|
||||
} else {
|
||||
$result[$nodeKey] = $this->walkFlexFormNode($nodeValue);
|
||||
}
|
||||
} else {
|
||||
$result[$nodeKey] = $nodeValue;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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\Configuration\Loader\Exception;
|
||||
|
||||
class YamlFileLoadingException extends \RuntimeException {}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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\Configuration\Loader\Exception;
|
||||
|
||||
class YamlParseException extends \RuntimeException {}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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\Configuration\Loader\Exception;
|
||||
|
||||
class YamlPlaceholderException extends \RuntimeException {}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?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\Core\Configuration\Loader;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlFileLoadingException;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlParseException;
|
||||
use TYPO3\CMS\Core\Configuration\Processor\PlaceholderProcessorList;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* A YAML file loader that allows to load YAML files, based on the Symfony/Yaml component
|
||||
*
|
||||
* In addition to just load a YAML file, it adds some special functionality.
|
||||
*
|
||||
* - A special "imports" key in the YAML file allows to include other YAML files recursively.
|
||||
* The actual YAML file gets loaded after the import statements, which are interpreted first,
|
||||
* at the very beginning. Imports can be referenced with a relative path.
|
||||
*
|
||||
* - Merging configuration options of import files when having simple "lists" will add items to the list instead
|
||||
* of overwriting them.
|
||||
*
|
||||
* - Special placeholder values set via %optionA.suboptionB% replace the value with the named path of the configuration
|
||||
* The placeholders will act as a full replacement of this value.
|
||||
*
|
||||
* - Environment placeholder values set via %env(option)% will be replaced by env variables of the same name
|
||||
*/
|
||||
readonly class YamlFileLoader
|
||||
{
|
||||
// see also: EnvPlaceholderProcessor
|
||||
public const PATTERN_PARTS = '%[^(%]+?\([\'"]?([^(]*?)[\'"]?\)%|%([^%()]*?)%';
|
||||
public const PROCESS_PLACEHOLDERS = 0x01;
|
||||
public const PROCESS_IMPORTS = 0x02;
|
||||
public const ALLOW_EMPTY_FILE = 0x04;
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loads and parses a YAML file, and returns an array with the found data
|
||||
*
|
||||
* @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
|
||||
* @param int $flags Flags to configure behaviour of the loader: see public PROCESS_ constants above
|
||||
* @return array the configuration as array
|
||||
*/
|
||||
public function load(string $fileName, int $flags = self::PROCESS_PLACEHOLDERS | self::PROCESS_IMPORTS): array
|
||||
{
|
||||
return $this->loadAndParse($fileName, $flags, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method which does all the logic. Built so it can be re-used recursively.
|
||||
*
|
||||
* @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
|
||||
* @param string|null $currentFileName when called recursively
|
||||
* @return array the configuration as array
|
||||
*/
|
||||
protected function loadAndParse(string $fileName, int $flags, ?string $currentFileName): array
|
||||
{
|
||||
$sanitizedFileName = $this->getStreamlinedFileName($fileName, $currentFileName);
|
||||
$content = $this->getFileContents($sanitizedFileName);
|
||||
try {
|
||||
$content = Yaml::parse($content);
|
||||
} catch (ParseException $e) {
|
||||
throw new YamlParseException(
|
||||
'YAML file "' . $fileName . '" has syntax errors: ' . $e->getMessage(),
|
||||
1740817000,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
if ($content === null && $this->hasFlag($flags, self::ALLOW_EMPTY_FILE)) {
|
||||
$content = [];
|
||||
}
|
||||
|
||||
if (!is_array($content)) {
|
||||
throw new YamlParseException(
|
||||
'YAML file "' . $fileName . '" does not contain data.',
|
||||
1497332874
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->hasFlag($flags, self::PROCESS_IMPORTS)) {
|
||||
$content = $this->processImports($content, $flags, $sanitizedFileName);
|
||||
}
|
||||
if ($this->hasFlag($flags, self::PROCESS_PLACEHOLDERS)) {
|
||||
// Check for "%" placeholders
|
||||
$content = $this->processPlaceholders($content, $content);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put into a separate method to ease the pains with unit tests
|
||||
*
|
||||
* @return string the contents or empty string if file_get_contents fails
|
||||
*/
|
||||
protected function getFileContents(string $fileName): string
|
||||
{
|
||||
return is_readable($fileName) ? (string)file_get_contents($fileName) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the absolute file name, but if a different file name is given, it is built relative to that.
|
||||
*
|
||||
* @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
|
||||
* @param string|null $currentFileName when called recursively this contains the absolute file name of the file that included this file
|
||||
* @return string the contents of the file
|
||||
* @throws YamlFileLoadingException when the file was not accessible
|
||||
*/
|
||||
protected function getStreamlinedFileName(string $fileName, ?string $currentFileName): string
|
||||
{
|
||||
if (!empty($currentFileName)) {
|
||||
if (PathUtility::isExtensionPath($fileName) || PathUtility::isAbsolutePath($fileName)) {
|
||||
$streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
|
||||
} else {
|
||||
// Now this path is considered to be relative the current file name
|
||||
$streamlinedFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath(
|
||||
$currentFileName,
|
||||
$fileName
|
||||
);
|
||||
if (!GeneralUtility::isAllowedAbsPath($streamlinedFileName)) {
|
||||
throw new YamlFileLoadingException(
|
||||
'Referencing a file which is outside of TYPO3s main folder',
|
||||
1560319866
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
|
||||
}
|
||||
if (!$streamlinedFileName) {
|
||||
throw new YamlFileLoadingException('YAML File "' . $fileName . '" could not be loaded', 1485784246);
|
||||
}
|
||||
return $streamlinedFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the special "imports" key on the main level of a file,
|
||||
* which calls "load" recursively.
|
||||
*/
|
||||
protected function processImports(array $content, int $flags, ?string $fileName): array
|
||||
{
|
||||
if (isset($content['imports']) && is_array($content['imports'])) {
|
||||
// Reverse the order of imports to follow the order of the declarations, see #92100
|
||||
$content['imports'] = array_reverse($content['imports']);
|
||||
foreach ($content['imports'] as $import) {
|
||||
try {
|
||||
$import = $this->processPlaceholders($import, $content);
|
||||
$resource = $import['resource'];
|
||||
if ($import['glob'] ?? false) {
|
||||
$resource = $this->getStreamlinedFileName($resource, $fileName);
|
||||
foreach (array_reverse(glob($resource)) as $file) {
|
||||
$content = ArrayUtility::replaceAndAppendScalarValuesRecursive($this->loadAndParse($file, $flags, $fileName), $content);
|
||||
}
|
||||
} else {
|
||||
$importedContent = $this->loadAndParse($resource, $flags, $fileName);
|
||||
// override the imported content with the one from the current file
|
||||
$content = ArrayUtility::replaceAndAppendScalarValuesRecursive($importedContent, $content);
|
||||
}
|
||||
} catch (ParseException|YamlParseException|YamlFileLoadingException $exception) {
|
||||
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
unset($content['imports']);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function that gets called recursively to check for %...% placeholders
|
||||
* inside the array
|
||||
*
|
||||
* @param array $content the current sub-level content array
|
||||
* @param array $referenceArray the global configuration array
|
||||
* @return array the modified sub-level content array
|
||||
*/
|
||||
protected function processPlaceholders(array $content, array $referenceArray): array
|
||||
{
|
||||
foreach ($content as $k => $v) {
|
||||
if ($this->containsPlaceholder($k)) {
|
||||
$resolvedKey = $this->processPlaceholderLine($k, $referenceArray);
|
||||
if (isset($content[$resolvedKey])) {
|
||||
if ($k === $resolvedKey) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Unresolvable placeholder key "' . $k . '" could not be substituted.',
|
||||
1719672440
|
||||
);
|
||||
}
|
||||
throw new \UnexpectedValueException(
|
||||
'Placeholder key "' . $k . '" can not be substituted with "' . $resolvedKey . '" because key already exists',
|
||||
1719316250
|
||||
);
|
||||
}
|
||||
unset($content[$k]);
|
||||
$k = $resolvedKey;
|
||||
$content[$k] = $v;
|
||||
}
|
||||
if (is_array($v)) {
|
||||
$content[$k] = $this->processPlaceholders($v, $referenceArray);
|
||||
} elseif ($this->containsPlaceholder($v)) {
|
||||
$content[$k] = $this->processPlaceholderLine($v, $referenceArray);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function processPlaceholderLine(string $line, array $referenceArray): mixed
|
||||
{
|
||||
$parts = $this->getParts($line);
|
||||
foreach ($parts as $partKey => $part) {
|
||||
$result = $this->processSinglePlaceholder($partKey, $part, $referenceArray);
|
||||
// Replace whole content if placeholder is the only thing in this line
|
||||
if ($line === $partKey) {
|
||||
$line = $result;
|
||||
} elseif (is_string($result) || is_numeric($result)) {
|
||||
$line = str_replace($partKey, $result, $line);
|
||||
} else {
|
||||
throw new \UnexpectedValueException(
|
||||
'Placeholder can not be substituted if result is not string or numeric',
|
||||
1581502783
|
||||
);
|
||||
}
|
||||
if ($result !== $partKey && $this->containsPlaceholder($line)) {
|
||||
$line = $this->processPlaceholderLine($line, $referenceArray);
|
||||
}
|
||||
}
|
||||
return $line;
|
||||
}
|
||||
|
||||
protected function processSinglePlaceholder(string $placeholder, string $value, array $referenceArray): mixed
|
||||
{
|
||||
$processorList = GeneralUtility::makeInstance(
|
||||
PlaceholderProcessorList::class,
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['yamlLoader']['placeholderProcessors']
|
||||
);
|
||||
foreach ($processorList->compile() as $processor) {
|
||||
if ($processor->canProcess($placeholder, $referenceArray)) {
|
||||
try {
|
||||
$result = $processor->process($value, $referenceArray);
|
||||
} catch (\UnexpectedValueException) {
|
||||
$result = $placeholder;
|
||||
}
|
||||
if (is_array($result)) {
|
||||
$result = $this->processPlaceholders($result, $referenceArray);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $result ?? $placeholder;
|
||||
}
|
||||
|
||||
protected function getParts(string $placeholders): array
|
||||
{
|
||||
// find occurrences of placeholders like %some()% and %array.access%.
|
||||
// Only find the innermost ones, so we can nest them.
|
||||
preg_match_all(
|
||||
'/' . self::PATTERN_PARTS . '/',
|
||||
$placeholders,
|
||||
$parts,
|
||||
PREG_UNMATCHED_AS_NULL
|
||||
);
|
||||
$matches = array_filter(
|
||||
array_merge($parts[1], $parts[2])
|
||||
);
|
||||
return array_combine($parts[0], $matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds possible placeholders.
|
||||
* May find false positives for complexer structures, but they will be sorted later on.
|
||||
*/
|
||||
protected function containsPlaceholder(mixed $value): bool
|
||||
{
|
||||
return is_string($value) && substr_count($value, '%') >= 2;
|
||||
}
|
||||
|
||||
protected function hasFlag(int $flags, int $flag): bool
|
||||
{
|
||||
return ($flags & $flag) === $flag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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\Core\Configuration\Loader;
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlPlaceholderException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\String\StringFragmentPattern;
|
||||
use TYPO3\CMS\Core\Utility\String\StringFragmentSplitter;
|
||||
|
||||
/**
|
||||
* A guard for protecting YAML placeholders - keeps existing, but escalates on adding new placeholders
|
||||
*/
|
||||
class YamlPlaceholderGuard
|
||||
{
|
||||
protected StringFragmentSplitter $fragmentSplitter;
|
||||
|
||||
public function __construct(protected array $existingConfiguration)
|
||||
{
|
||||
$fragmentPattern = GeneralUtility::makeInstance(
|
||||
StringFragmentPattern::class,
|
||||
StringFragmentSplitter::TYPE_EXPRESSION,
|
||||
YamlFileLoader::PATTERN_PARTS
|
||||
);
|
||||
$this->fragmentSplitter = GeneralUtility::makeInstance(
|
||||
StringFragmentSplitter::class,
|
||||
$fragmentPattern
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies existing configuration.
|
||||
*/
|
||||
public function process(array $modified): array
|
||||
{
|
||||
return $this->protectPlaceholders($this->existingConfiguration, $modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects placeholders that have been introduced and handles* them.
|
||||
* (*) currently throws an exception, but could be purged or escaped as well
|
||||
*
|
||||
* @param array<string, mixed> $current
|
||||
* @param array<string, mixed> $modified
|
||||
* @param list<string> $steps configuration keys traversed so far
|
||||
* @return array<string, mixed> sanitized configuration (currently not used, exception thrown before)
|
||||
* @throws YamlPlaceholderException
|
||||
*/
|
||||
protected function protectPlaceholders(array $current, array $modified, array $steps = []): array
|
||||
{
|
||||
foreach ($modified as $key => $value) {
|
||||
$currentSteps = array_merge($steps, [$key]);
|
||||
if (is_array($value)) {
|
||||
$modified[$key] = $this->protectPlaceholders(
|
||||
$current[$key] ?? [],
|
||||
$value,
|
||||
$currentSteps
|
||||
);
|
||||
} elseif (is_string($value)) {
|
||||
$splitFlags = StringFragmentSplitter::FLAG_UNMATCHED_AS_NULL;
|
||||
$newFragments = $this->fragmentSplitter->split($value, $splitFlags);
|
||||
if (is_string($current[$key] ?? null)) {
|
||||
$currentFragments = $this->fragmentSplitter->split($current[$key] ?? '', $splitFlags);
|
||||
} else {
|
||||
$currentFragments = null;
|
||||
}
|
||||
// in case there are new fragments (at least one matching the pattern)
|
||||
if ($newFragments !== null) {
|
||||
// compares differences in `expression` fragments only
|
||||
$differences = $currentFragments === null
|
||||
? $newFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION)
|
||||
: $newFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION)
|
||||
->diff($currentFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION));
|
||||
if (count($differences) > 0) {
|
||||
throw new YamlPlaceholderException(
|
||||
sprintf(
|
||||
'Introducing placeholder%s %s for %s is not allowed',
|
||||
count($differences) !== 1 ? 's' : '',
|
||||
implode(', ', $differences->getFragments()),
|
||||
implode('.', $currentSteps)
|
||||
),
|
||||
1651690534
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $modified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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\Configuration\Processor\Placeholder;
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Processor\PlaceholderProcessorList;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Can process a string consisting of one or multiple %env(...)% YAML
|
||||
* placeholders, and replace them with their evaluated values.
|
||||
* This in contrast to the EnvVariableProcessor, which only evaluates
|
||||
* a single ENV value (without the placeholders), and is also utilized
|
||||
* for the actual expansion here by means of the PlaceholderProcessorList.
|
||||
*/
|
||||
class EnvPlaceholderProcessor
|
||||
{
|
||||
// see also YamlFileLoader
|
||||
public const PATTERN_PARTS = '%[^(%]+?\([\'"]?([^(]*?)[\'"]?\)%|%([^%()]*?)%';
|
||||
|
||||
protected array $processorList = [];
|
||||
|
||||
public function __construct(
|
||||
) {
|
||||
$processorList = GeneralUtility::makeInstance(
|
||||
PlaceholderProcessorList::class,
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['yamlLoader']['placeholderProcessors']
|
||||
);
|
||||
$this->processorList = $processorList->compile();
|
||||
}
|
||||
|
||||
public function canProcess(mixed $placeholder): bool
|
||||
{
|
||||
// only strings may be candidates for $placeholder substitution
|
||||
if (!is_string($placeholder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains($placeholder, '%env(');
|
||||
}
|
||||
|
||||
public function process(string $value): string
|
||||
{
|
||||
return $this->processPlaceholderLine($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The following methods are taken from YamlFileLoader, but adapted
|
||||
* for isolated usage and preventing circular dependencies.
|
||||
*/
|
||||
protected function processPlaceholderLine(string $line): string
|
||||
{
|
||||
$parts = $this->getParts($line);
|
||||
foreach ($parts as $partKey => $part) {
|
||||
$result = $this->processSinglePlaceholder($partKey, $part);
|
||||
// Replace whole content if placeholder is the only thing in this line
|
||||
if ($line === $partKey) {
|
||||
$line = $result;
|
||||
} elseif (is_string($result) || is_numeric($result)) {
|
||||
$line = str_replace($partKey, $result, $line);
|
||||
} else {
|
||||
throw new \UnexpectedValueException(
|
||||
'ENV Placeholder can not be substituted if result is not string or numeric',
|
||||
1770965068
|
||||
);
|
||||
}
|
||||
if ($result !== $partKey && $this->containsPlaceholder($line)) {
|
||||
$line = $this->processPlaceholderLine($line);
|
||||
}
|
||||
}
|
||||
return $line;
|
||||
}
|
||||
|
||||
protected function processSinglePlaceholder(string $placeholder, string $value): mixed
|
||||
{
|
||||
foreach ($this->processorList as $processor) {
|
||||
if ($processor->canProcess($placeholder, [])) {
|
||||
try {
|
||||
$result = $processor->process($value, []);
|
||||
} catch (\UnexpectedValueException) {
|
||||
$result = $placeholder;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $result ?? $placeholder;
|
||||
}
|
||||
|
||||
// These two methods are used just as in YamlFileLoader and
|
||||
// might be moved to utility classes; however for now they
|
||||
// are replicated to be able to be adapted.
|
||||
protected function getParts(string $placeholders): array
|
||||
{
|
||||
// find occurrences of placeholders like %some()% and %array.access%.
|
||||
// Only find the innermost ones, so we can nest them.
|
||||
preg_match_all(
|
||||
'/' . self::PATTERN_PARTS . '/',
|
||||
$placeholders,
|
||||
$parts,
|
||||
PREG_UNMATCHED_AS_NULL
|
||||
);
|
||||
$matches = array_filter(
|
||||
array_merge($parts[1], $parts[2])
|
||||
);
|
||||
return array_combine($parts[0], $matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds possible placeholders.
|
||||
* May find false positives for complexer structures, but they will be sorted later on.
|
||||
*/
|
||||
protected function containsPlaceholder(mixed $value): bool
|
||||
{
|
||||
return is_string($value) && substr_count($value, '%') >= 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Core\Configuration\Processor\Placeholder;
|
||||
|
||||
/**
|
||||
* Return value from environment variable
|
||||
*
|
||||
* Environment variables may only contain word characters and underscores (a-zA-Z0-9_)
|
||||
* to be compatible to shell environments.
|
||||
*/
|
||||
readonly class EnvVariableProcessor implements PlaceholderProcessorInterface
|
||||
{
|
||||
public function canProcess(string $placeholder, array $referenceArray): bool
|
||||
{
|
||||
return str_contains($placeholder, '%env(');
|
||||
}
|
||||
|
||||
public function process(string $value, array $referenceArray)
|
||||
{
|
||||
$envVar = getenv($value);
|
||||
if ($envVar === false) {
|
||||
throw new \UnexpectedValueException('Value not found', 1581501124);
|
||||
}
|
||||
return $envVar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Core\Configuration\Processor\Placeholder;
|
||||
|
||||
interface PlaceholderProcessorInterface
|
||||
{
|
||||
public function canProcess(string $placeholder, array $referenceArray): bool;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function process(string $value, array $referenceArray);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Core\Configuration\Processor\Placeholder;
|
||||
|
||||
/**
|
||||
* Returns the value for a placeholder as fetched from the referenceArray
|
||||
*/
|
||||
readonly class ValueFromReferenceArrayProcessor implements PlaceholderProcessorInterface
|
||||
{
|
||||
public function canProcess(string $placeholder, array $referenceArray): bool
|
||||
{
|
||||
return !str_contains($placeholder, '(');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for a placeholder as fetched from the referenceArray
|
||||
*
|
||||
* @param string $value the string to search for
|
||||
* @param array $referenceArray the main configuration array where to look up the data
|
||||
*
|
||||
* @return array|mixed|string
|
||||
*/
|
||||
public function process(string $value, array $referenceArray)
|
||||
{
|
||||
$parts = explode('.', $value);
|
||||
$referenceData = $referenceArray;
|
||||
foreach ($parts as $part) {
|
||||
if (isset($referenceData[$part])) {
|
||||
$referenceData = $referenceData[$part];
|
||||
} else {
|
||||
// return unsubstituted placeholder
|
||||
throw new \UnexpectedValueException('Value not found', 1581501216);
|
||||
}
|
||||
}
|
||||
return $referenceData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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\Core\Configuration\Processor;
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\PlaceholderProcessorInterface;
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Orders and returns given PlaceholderProcessors
|
||||
*/
|
||||
class PlaceholderProcessorList
|
||||
{
|
||||
/**
|
||||
* @var PlaceholderProcessorInterface[]
|
||||
*/
|
||||
protected $processors;
|
||||
|
||||
public function __construct($processorList = [])
|
||||
{
|
||||
$this->processors = $processorList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PlaceholderProcessorInterface[]
|
||||
*/
|
||||
public function compile(): array
|
||||
{
|
||||
$processors = [];
|
||||
$orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
|
||||
$orderedProcessors = $orderingService->orderByDependencies($this->processors, 'before', 'after');
|
||||
|
||||
foreach ($orderedProcessors as $processorClassName => $providerConfig) {
|
||||
if (isset($providerConfig['disabled']) && $providerConfig['disabled'] === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$processor = GeneralUtility::makeInstance($processorClassName);
|
||||
if (!$processor instanceof PlaceholderProcessorInterface) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Placeholder processor ' . $processorClassName . ' must implement PlaceholderProcessorInterface',
|
||||
1581343410
|
||||
);
|
||||
}
|
||||
$processors[] = $processor;
|
||||
}
|
||||
return $processors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Configuration\Event\AfterRichtextConfigurationPreparedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
|
||||
/**
|
||||
* Prepare richtext configuration. Used in DataHandler and FormEngine
|
||||
*
|
||||
* @internal Internal class for the time being - may change / vanish any time
|
||||
*/
|
||||
readonly class Richtext
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $runtimeCache,
|
||||
private YamlFileLoader $yamlFileLoader,
|
||||
private TypoScriptService $typoScriptService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This is an intermediate class / method to retrieve RTE
|
||||
* configuration until all core places use data providers to do that.
|
||||
*
|
||||
* @param string $table The table the field is in
|
||||
* @param string $field Field name
|
||||
* @param int $pid Real page id
|
||||
* @param string $recordType Record type value
|
||||
* @param array $tcaFieldConf ['config'] section of TCA field
|
||||
*/
|
||||
public function getConfiguration(string $table, string $field, int $pid, string $recordType, array $tcaFieldConf): array
|
||||
{
|
||||
// create instance of NodeFactory, ask for "text" element
|
||||
//
|
||||
// As soon as the Data handler starts using FormDataProviders, this class can vanish again, and the hack to
|
||||
// test for specific rich text instances can be dropped: Split the "TcaText" data provider into multiple parts, each
|
||||
// RTE should register and own data provider that does the transformation / configuration providing. This way,
|
||||
// the explicit check for different RTE classes is removed from core and "hooked in" by the RTE's.
|
||||
|
||||
// The main problem here is that all parameters that the processing needs is handed over to as TSconfig
|
||||
// "dotted array" syntax. We convert at least the processing information available under "processing"
|
||||
// together with pageTS, this way it can be overridden and understood in RteHtmlParser.
|
||||
// However, all other parts of the core will depend on the non-dotted syntax (coming from YAML directly)
|
||||
|
||||
$pageTs = $this->getPageTsConfiguration($table, $field, $pid, $recordType);
|
||||
|
||||
// determine which preset to use
|
||||
$pageTs['preset'] = $pageTs['fieldSpecificPreset'] ?? $tcaFieldConf['richtextConfiguration'] ?? $pageTs['generalPreset'] ?? 'default';
|
||||
unset($pageTs['fieldSpecificPreset']);
|
||||
unset($pageTs['generalPreset']);
|
||||
|
||||
// load configuration from preset
|
||||
$configuration = $this->loadConfigurationFromPreset($pageTs['preset']);
|
||||
|
||||
// overlay preset configuration with pageTs
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$configuration,
|
||||
$this->addFlattenedPageTsConfig($pageTs)
|
||||
);
|
||||
|
||||
// Handle "mode" / "transformation" config when overridden
|
||||
if (!isset($configuration['proc.']['mode']) && !isset($configuration['proc.']['overruleMode'])) {
|
||||
$configuration['proc.']['overruleMode'] = 'default';
|
||||
}
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new AfterRichtextConfigurationPreparedEvent($configuration));
|
||||
|
||||
return $event->getConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a configuration preset from an external resource (currently only YAML is supported).
|
||||
* This is the default behaviour and can be overridden by page TSconfig.
|
||||
*
|
||||
* @return array the parsed configuration
|
||||
*/
|
||||
protected function loadConfigurationFromPreset(string $presetName = ''): array
|
||||
{
|
||||
$configuration = [];
|
||||
if (!empty($presetName) && isset($GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'][$presetName])) {
|
||||
$identifier = 'richtext_' . $presetName;
|
||||
$configuration = $this->runtimeCache->get($identifier);
|
||||
|
||||
if ($configuration === false) {
|
||||
$configuration = $this->yamlFileLoader->load($GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'][$presetName]);
|
||||
// For future versions, you should however rely on the "processing" key and not the "proc" key.
|
||||
if (is_array($configuration['processing'] ?? null)) {
|
||||
$configuration['proc.'] = $this->convertPlainArrayToTypoScriptArray($configuration['processing']);
|
||||
}
|
||||
$this->runtimeCache->set($identifier, $configuration);
|
||||
}
|
||||
}
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return RTE section of page TS
|
||||
*
|
||||
* @param int $pid Page ts of given pid
|
||||
* @return array RTE section of pageTs of given pid
|
||||
*/
|
||||
protected function getRtePageTsConfigOfPid(int $pid): array
|
||||
{
|
||||
return BackendUtility::getPagesTSconfig($pid)['RTE.'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with Typoscript the old way (with dot)
|
||||
* Since the functionality in YAML is without the dots, but the new configuration is used without the dots
|
||||
* this functionality adds also an explicit = 1 to the arrays
|
||||
*
|
||||
* @param array $plainArray An array
|
||||
* @return array array with TypoScript as usual (with dot)
|
||||
*/
|
||||
protected function convertPlainArrayToTypoScriptArray(array $plainArray)
|
||||
{
|
||||
$typoScriptArray = [];
|
||||
foreach ($plainArray as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (!isset($typoScriptArray[$key])) {
|
||||
$typoScriptArray[$key] = 1;
|
||||
}
|
||||
$typoScriptArray[$key . '.'] = $this->convertPlainArrayToTypoScriptArray($value);
|
||||
} else {
|
||||
$typoScriptArray[$key] = $value ?? '';
|
||||
}
|
||||
}
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all PageTS.RTE options keys to configuration without dots
|
||||
*
|
||||
* We need to keep the dotted keys for backwards compatibility like ext:rtehtmlarea
|
||||
*
|
||||
* @param array $typoScriptArray TypoScriptArray
|
||||
* @return array array with config without dots added
|
||||
*/
|
||||
protected function addFlattenedPageTsConfig(array $typoScriptArray): array
|
||||
{
|
||||
foreach ($typoScriptArray as $key => $data) {
|
||||
if (!str_ends_with($key, '.')) {
|
||||
continue;
|
||||
}
|
||||
$typoScriptArray[substr($key, 0, -1)] = $this->typoScriptService->convertTypoScriptArrayToPlainArray($typoScriptArray[$key]);
|
||||
}
|
||||
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load PageTS configuration for the RTE
|
||||
*
|
||||
* Return RTE section of page TS, taking into account overloading via table, field and record type
|
||||
*
|
||||
* @param string $table The table the field is in
|
||||
* @param string $field Field name
|
||||
* @param int $pid Real page id
|
||||
* @param string $recordType Record type value
|
||||
*/
|
||||
protected function getPageTsConfiguration(string $table, string $field, int $pid, string $recordType): array
|
||||
{
|
||||
// Load page TSconfig configuration
|
||||
$fullPageTsConfig = $this->getRtePageTsConfigOfPid($pid);
|
||||
$defaultPageTsConfigOverrides = $fullPageTsConfig['default.'] ?? null;
|
||||
|
||||
$defaultPageTsConfigOverrides['generalPreset'] = $fullPageTsConfig['default.']['preset'] ?? null;
|
||||
|
||||
$fieldSpecificPageTsConfigOverrides = $fullPageTsConfig['config.'][$table . '.'][$field . '.'] ?? null;
|
||||
unset($fullPageTsConfig['default.'], $fullPageTsConfig['config.']);
|
||||
|
||||
// First use RTE.*
|
||||
$rtePageTsConfiguration = $fullPageTsConfig;
|
||||
|
||||
// Then overload with RTE.default.*
|
||||
if (is_array($defaultPageTsConfigOverrides)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($rtePageTsConfiguration, $defaultPageTsConfigOverrides);
|
||||
}
|
||||
|
||||
$rtePageTsConfiguration['fieldSpecificPreset'] = $fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.']['preset']
|
||||
?? $fieldSpecificPageTsConfigOverrides['preset'] ?? null;
|
||||
|
||||
// Then overload with RTE.config.tt_content.bodytext
|
||||
if (is_array($fieldSpecificPageTsConfigOverrides)) {
|
||||
$fieldSpecificPageTsConfigOverridesWithoutType = $fieldSpecificPageTsConfigOverrides;
|
||||
unset($fieldSpecificPageTsConfigOverridesWithoutType['types.']);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($rtePageTsConfiguration, $fieldSpecificPageTsConfigOverridesWithoutType);
|
||||
|
||||
// Then overload with RTE.config.tt_content.bodytext.types.textmedia
|
||||
if (
|
||||
$recordType
|
||||
&& isset($fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.'])
|
||||
&& is_array($fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.'])
|
||||
) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$rtePageTsConfiguration,
|
||||
$fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unset($rtePageTsConfiguration['preset']);
|
||||
|
||||
return $rtePageTsConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationChangedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteSettings;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteTSconfig;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteTypoScript;
|
||||
use TYPO3\CMS\Core\Site\Set\SetError;
|
||||
use TYPO3\CMS\Core\Site\Set\SetRegistry;
|
||||
use TYPO3\CMS\Core\Site\SiteSettingsFactory;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Responsibility: Handles the format of the configuration (currently yaml), and the location of the file system folder
|
||||
*
|
||||
* Reads all available site configuration options, and puts them into Site objects.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class SiteConfiguration
|
||||
{
|
||||
/**
|
||||
* Config yaml file name.
|
||||
*/
|
||||
private const string CONFIG_FILE_NAME = 'config.yaml';
|
||||
|
||||
/**
|
||||
* File naming containing TypoScript Setup.
|
||||
*/
|
||||
private const string TYPOSCRIPT_SETUP_FILE_NAME = 'setup.typoscript';
|
||||
|
||||
/**
|
||||
* File naming containing TypoScript Constants.
|
||||
*/
|
||||
private const string TYPOSCRIPT_CONSTANTS_FILE_NAME = 'constants.typoscript';
|
||||
|
||||
/**
|
||||
* File naming containing page TSconfig definitions
|
||||
*/
|
||||
private const string PAGE_TSCONFIG_FILE_NAME = 'page.tsconfig';
|
||||
|
||||
/**
|
||||
* YAML file name with all settings related to Content-Security-Policies.
|
||||
*/
|
||||
private const string CONTENT_SECURITY_FILE_NAME = 'csp.yaml';
|
||||
|
||||
/**
|
||||
* Identifier to store all configuration data in the core cache.
|
||||
*/
|
||||
private const string CACHE_IDENTIFIER = 'sites-configuration';
|
||||
|
||||
public function __construct(
|
||||
#[Autowire('%env(TYPO3:configPath)%/sites')]
|
||||
protected string $configPath,
|
||||
protected SiteSettingsFactory $siteSettingsFactory,
|
||||
protected SetRegistry $setRegistry,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
#[Autowire(service: 'cache.core')]
|
||||
protected PhpFrontend $cache,
|
||||
private readonly YamlFileLoader $yamlFileLoader,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected readonly FrontendInterface $runtimeCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Return all site objects which have been found in the filesystem.
|
||||
*
|
||||
* @return Site[]
|
||||
*/
|
||||
public function getAllExistingSites(bool $useCache = true): array
|
||||
{
|
||||
if ($useCache && $this->runtimeCache->has(self::CACHE_IDENTIFIER)) {
|
||||
return $this->runtimeCache->get(self::CACHE_IDENTIFIER);
|
||||
}
|
||||
return $this->resolveAllExistingSites($useCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all site objects which have been found in the filesystem.
|
||||
*
|
||||
* @return Site[]
|
||||
*/
|
||||
public function resolveAllExistingSites(bool $useCache = true): array
|
||||
{
|
||||
$sites = [];
|
||||
$siteConfiguration = $this->getAllSiteConfigurationFromFiles($useCache);
|
||||
foreach ($siteConfiguration as $identifier => $configuration) {
|
||||
// cast $identifier to string, as the identifier can potentially only consist of (int) digit numbers
|
||||
$identifier = (string)$identifier;
|
||||
$siteSettings = $this->siteSettingsFactory->getSettings($identifier, $configuration);
|
||||
$siteTypoScript = $this->getSiteTypoScript($identifier);
|
||||
$siteTSconfig = $this->getSiteTSconfig($identifier);
|
||||
$configuration['contentSecurityPolicies'] = $this->getContentSecurityPolicies($identifier);
|
||||
$configuration['routeEnhancers'] = ArrayUtility::replaceAndAppendScalarValuesRecursive(
|
||||
$this->getRouteEnhancersFromSets($configuration['dependencies'] ?? []),
|
||||
$configuration['routeEnhancers'] ?? []
|
||||
);
|
||||
|
||||
$rootPageId = (int)($configuration['rootPageId'] ?? 0);
|
||||
if ($rootPageId > 0) {
|
||||
$site = new Site($identifier, $rootPageId, $configuration, $siteSettings, $siteTypoScript, $siteTSconfig);
|
||||
$this->determineInvalidSets($site);
|
||||
$sites[$identifier] = $site;
|
||||
|
||||
}
|
||||
}
|
||||
$this->runtimeCache->set(self::CACHE_IDENTIFIER, $sites);
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all site objects which have been found in the filesystem containing settings only from the `config.yaml`
|
||||
* file ignoring values from the `settings.yaml` and `csp.yaml` file.
|
||||
*
|
||||
* @return Site[]
|
||||
* @internal Not part of public API. Used as intermediate solution until settings are handled by a dedicated GUI.
|
||||
*/
|
||||
public function resolveAllExistingSitesRaw(): array
|
||||
{
|
||||
$sites = [];
|
||||
$siteConfiguration = $this->getAllSiteConfigurationFromFiles(false);
|
||||
foreach ($siteConfiguration as $identifier => $configuration) {
|
||||
// cast $identifier to string, as the identifier can potentially only consist of (int) digit numbers
|
||||
$identifier = (string)$identifier;
|
||||
$inlineSettings = $configuration['settings'] ?? [];
|
||||
$siteSettings = SiteSettings::createFromSettingsTree($inlineSettings);
|
||||
$siteTypoScript = $this->getSiteTypoScript($identifier);
|
||||
|
||||
$rootPageId = (int)($configuration['rootPageId'] ?? 0);
|
||||
if ($rootPageId > 0) {
|
||||
$site = new Site($identifier, $rootPageId, $configuration, $siteSettings, $siteTypoScript);
|
||||
$this->determineInvalidSets($site);
|
||||
$sites[$identifier] = $site;
|
||||
}
|
||||
}
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of paths in which a site configuration is found.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getAllSiteConfigurationPaths(): array
|
||||
{
|
||||
$finder = new Finder();
|
||||
$paths = [];
|
||||
try {
|
||||
$finder->files()->depth(0)->name(self::CONFIG_FILE_NAME)->in($this->configPath . '/*');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$finder = [];
|
||||
}
|
||||
|
||||
foreach ($finder as $fileInfo) {
|
||||
$path = $fileInfo->getPath();
|
||||
$paths[basename($path)] = $path;
|
||||
}
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the site configuration from config files.
|
||||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
protected function getAllSiteConfigurationFromFiles(bool $useCache = true): array
|
||||
{
|
||||
// Check if the data is already cached
|
||||
$siteConfiguration = $useCache ? $this->cache->require(self::CACHE_IDENTIFIER) : false;
|
||||
if ($siteConfiguration !== false) {
|
||||
return $siteConfiguration;
|
||||
}
|
||||
$finder = new Finder();
|
||||
try {
|
||||
$finder->files()->depth(0)->name(self::CONFIG_FILE_NAME)->in($this->configPath . '/*');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Directory $this->configPath does not exist yet
|
||||
$finder = [];
|
||||
}
|
||||
$siteConfiguration = [];
|
||||
foreach ($finder as $fileInfo) {
|
||||
$configuration = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath((string)$fileInfo));
|
||||
$identifier = basename($fileInfo->getPath());
|
||||
$event = $this->eventDispatcher->dispatch(new SiteConfigurationLoadedEvent($identifier, $configuration));
|
||||
$siteConfiguration[$identifier] = $event->getConfiguration();
|
||||
}
|
||||
$this->cache->set(self::CACHE_IDENTIFIER, 'return ' . var_export($siteConfiguration, true) . ';');
|
||||
|
||||
return $siteConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plain configuration without additional settings.
|
||||
*
|
||||
* This method should only be used in case the original configuration as it exists in the file should be loaded,
|
||||
* for example for writing / editing configuration.
|
||||
*
|
||||
* All read related actions should be performed on the site entity.
|
||||
*
|
||||
* @param string $siteIdentifier
|
||||
*/
|
||||
public function load(string $siteIdentifier): array
|
||||
{
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::CONFIG_FILE_NAME;
|
||||
return $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS);
|
||||
}
|
||||
|
||||
protected function getSiteTypoScript(string $siteIdentifier): ?SiteTypoScript
|
||||
{
|
||||
$data = [
|
||||
'setup' => self::TYPOSCRIPT_SETUP_FILE_NAME,
|
||||
'constants' => self::TYPOSCRIPT_CONSTANTS_FILE_NAME,
|
||||
];
|
||||
$definitions = [];
|
||||
foreach ($data as $type => $fileName) {
|
||||
$path = $this->configPath . '/' . $siteIdentifier . '/' . $fileName;
|
||||
if (file_exists($path)) {
|
||||
$contents = @file_get_contents(GeneralUtility::fixWindowsFilePath($path));
|
||||
if ($contents !== false) {
|
||||
$definitions[$type] = $contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($definitions === []) {
|
||||
return null;
|
||||
}
|
||||
return new SiteTypoScript(...$definitions);
|
||||
}
|
||||
|
||||
protected function getSiteTSconfig(string $siteIdentifier): ?SiteTSconfig
|
||||
{
|
||||
$pageTSconfig = null;
|
||||
$path = $this->configPath . '/' . $siteIdentifier . '/' . self::PAGE_TSCONFIG_FILE_NAME;
|
||||
if (file_exists($path)) {
|
||||
$contents = @file_get_contents(GeneralUtility::fixWindowsFilePath($path));
|
||||
if ($contents !== false) {
|
||||
$pageTSconfig = $contents;
|
||||
}
|
||||
}
|
||||
if ($pageTSconfig === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SiteTSconfig(
|
||||
pageTSconfig: $pageTSconfig
|
||||
);
|
||||
}
|
||||
|
||||
protected function getContentSecurityPolicies(string $siteIdentifier): array
|
||||
{
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::CONTENT_SECURITY_FILE_NAME;
|
||||
if (file_exists($fileName)) {
|
||||
return $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route enhancers from site sets.
|
||||
*/
|
||||
protected function getRouteEnhancersFromSets(array $dependencies): array
|
||||
{
|
||||
$routeEnhancers = [];
|
||||
$sets = $this->setRegistry->getSets(...$dependencies);
|
||||
foreach ($sets as $set) {
|
||||
$routeEnhancers = ArrayUtility::replaceAndAppendScalarValuesRecursive(
|
||||
$routeEnhancers,
|
||||
$set->routeEnhancers
|
||||
);
|
||||
}
|
||||
return $routeEnhancers;
|
||||
}
|
||||
|
||||
protected function determineInvalidSets(Site $site): void
|
||||
{
|
||||
$site->invalidSets = array_filter(
|
||||
$this->setRegistry->getInvalidSets(),
|
||||
static fn($setName) => in_array($setName, $site->getSets(), true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
foreach ($site->getSets() as $set) {
|
||||
if (!$this->setRegistry->hasSet($set) && !isset($site->invalidSets[$set])) {
|
||||
$site->invalidSets[$set] = [
|
||||
'name' => $set,
|
||||
'error' => SetError::notFound,
|
||||
'context' => 'site:' . $site->getIdentifier(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[AsEventListener(event: SiteConfigurationChangedEvent::class)]
|
||||
public function siteConfigurationChanged(): void
|
||||
{
|
||||
$this->cache->remove(self::CACHE_IDENTIFIER);
|
||||
$this->runtimeCache->remove(self::CACHE_IDENTIFIER);
|
||||
}
|
||||
|
||||
#[AsEventListener('typo3-core/site-configuration')]
|
||||
public function warmupCaches(CacheWarmupEvent $event): void
|
||||
{
|
||||
if ($event->hasGroup('system')) {
|
||||
$this->getAllSiteConfigurationFromFiles(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?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\Configuration;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationChangedEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlPlaceholderException;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlPlaceholderGuard;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Writes Site objects into site configuration files.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class SiteWriter
|
||||
{
|
||||
/**
|
||||
* Config yaml file name.
|
||||
*/
|
||||
private const string CONFIG_FILE_NAME = 'config.yaml';
|
||||
|
||||
/**
|
||||
* YAML file name with all settings.
|
||||
*/
|
||||
private const string SETTINGS_FILE_NAME = 'settings.yaml';
|
||||
|
||||
public function __construct(
|
||||
protected readonly string $configPath,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly YamlFileLoader $yamlFileLoader,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a site configuration with one language "English" which is the de-facto default language for TYPO3 in general.
|
||||
*
|
||||
* @param string[] $dependencies Site set identifiers to add as dependencies
|
||||
* @throws SiteConfigurationWriteException
|
||||
*/
|
||||
public function createNewBasicSite(string $identifier, int $rootPageId, string $base, array $dependencies = []): void
|
||||
{
|
||||
// Create a default site configuration called "main" as best practice
|
||||
$configuration = [
|
||||
'rootPageId' => $rootPageId,
|
||||
'base' => $base,
|
||||
'languages' => [
|
||||
0 => [
|
||||
'title' => 'English',
|
||||
'enabled' => true,
|
||||
'languageId' => 0,
|
||||
'base' => '/',
|
||||
'locale' => 'en_US.UTF-8',
|
||||
'navigationTitle' => 'English',
|
||||
'flag' => 'us',
|
||||
],
|
||||
],
|
||||
'errorHandling' => [],
|
||||
'routes' => [],
|
||||
'dependencies' => $dependencies,
|
||||
];
|
||||
|
||||
$this->write($identifier, $configuration);
|
||||
}
|
||||
|
||||
public function writeSettings(string $siteIdentifier, array $settings): void
|
||||
{
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::SETTINGS_FILE_NAME;
|
||||
if ($settings === []) {
|
||||
if (!is_file($fileName)) {
|
||||
return;
|
||||
}
|
||||
$yamlFileContents = '# No site specific settings defined';
|
||||
} else {
|
||||
$yamlFileContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
|
||||
}
|
||||
if (!GeneralUtility::writeFile($fileName, $yamlFileContents, true)) {
|
||||
throw new SiteConfigurationWriteException('Unable to write site settings in sites/' . $siteIdentifier . '/' . self::SETTINGS_FILE_NAME, 1590487411);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a site configuration
|
||||
*
|
||||
* @param bool $protectPlaceholders whether to disallow introducing new placeholders
|
||||
* @todo enforce $protectPlaceholders with TYPO3 v13.0
|
||||
* @throws SiteConfigurationWriteException
|
||||
*/
|
||||
public function write(string $siteIdentifier, array $configuration, bool $protectPlaceholders = false): void
|
||||
{
|
||||
$folder = $this->configPath . '/' . $siteIdentifier;
|
||||
$fileName = $folder . '/' . self::CONFIG_FILE_NAME;
|
||||
$newConfiguration = $configuration;
|
||||
if (!file_exists($folder)) {
|
||||
GeneralUtility::mkdir_deep($folder);
|
||||
if ($protectPlaceholders && $newConfiguration !== []) {
|
||||
$newConfiguration = $this->protectPlaceholders([], $newConfiguration);
|
||||
}
|
||||
} elseif (file_exists($fileName)) {
|
||||
// load without any processing to have the unprocessed base to modify
|
||||
$newConfiguration = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName), 0);
|
||||
// load the processed configuration to diff changed values,
|
||||
// but don't process placeholders, because all properties that
|
||||
// were modified via GUI are unprocessed values as well
|
||||
$processed = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS);
|
||||
// find properties that were modified via GUI
|
||||
$newModified = array_replace_recursive(
|
||||
self::findRemoved($processed, $configuration),
|
||||
self::findModified($processed, $configuration)
|
||||
);
|
||||
if ($protectPlaceholders && $newModified !== []) {
|
||||
$newModified = $this->protectPlaceholders($newConfiguration, $newModified);
|
||||
}
|
||||
// change _only_ the modified keys, leave the original non-changed areas alone
|
||||
ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified);
|
||||
}
|
||||
$event = $this->eventDispatcher->dispatch(new SiteConfigurationBeforeWriteEvent($siteIdentifier, $newConfiguration));
|
||||
$newConfiguration = $this->sortConfiguration($event->getConfiguration());
|
||||
$yamlFileContents = Yaml::dump($newConfiguration, 99, 2);
|
||||
if (!GeneralUtility::writeFile($fileName, $yamlFileContents, true)) {
|
||||
throw new SiteConfigurationWriteException('Unable to write site configuration in sites/' . $siteIdentifier . '/' . self::CONFIG_FILE_NAME, 1590487011);
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($siteIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a site identifier (and moves the folder)
|
||||
*
|
||||
* @throws SiteConfigurationWriteException
|
||||
*/
|
||||
public function rename(string $currentIdentifier, string $newIdentifier): void
|
||||
{
|
||||
if (!rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier)) {
|
||||
throw new SiteConfigurationWriteException('Unable to rename folder sites/' . $currentIdentifier, 1522491300);
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($newIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the config.yaml file of a site configuration.
|
||||
* Also clears the cache.
|
||||
*
|
||||
* @throws SiteNotFoundException|SiteConfigurationWriteException
|
||||
*/
|
||||
public function delete(string $siteIdentifier): void
|
||||
{
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::CONFIG_FILE_NAME;
|
||||
if (!file_exists($fileName)) {
|
||||
throw new SiteNotFoundException('Site configuration file ' . self::CONFIG_FILE_NAME . ' within the site ' . $siteIdentifier . ' not found.', 1522866184);
|
||||
}
|
||||
if (!unlink($fileName)) {
|
||||
throw new SiteConfigurationWriteException('Unable to delete folder sites/' . $siteIdentifier, 1596462020);
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($siteIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects placeholders that have been introduced and handles* them.
|
||||
* (*) currently throws an exception, but could be purged or escaped as well
|
||||
*
|
||||
* @param array<string, mixed> $existingConfiguration
|
||||
* @param array<string, mixed> $modifiedConfiguration
|
||||
* @return array<string, mixed> sanitized configuration (currently not used, exception thrown before)
|
||||
* @throws SiteConfigurationWriteException
|
||||
*/
|
||||
protected function protectPlaceholders(array $existingConfiguration, array $modifiedConfiguration): array
|
||||
{
|
||||
try {
|
||||
return GeneralUtility::makeInstance(YamlPlaceholderGuard::class, $existingConfiguration)
|
||||
->process($modifiedConfiguration);
|
||||
} catch (YamlPlaceholderException $exception) {
|
||||
throw new SiteConfigurationWriteException($exception->getMessage(), 1670361271, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected function sortConfiguration(array $newConfiguration): array
|
||||
{
|
||||
ksort($newConfiguration);
|
||||
if (isset($newConfiguration['imports'])) {
|
||||
$imports = $newConfiguration['imports'];
|
||||
unset($newConfiguration['imports']);
|
||||
$newConfiguration['imports'] = $imports;
|
||||
}
|
||||
return $newConfiguration;
|
||||
}
|
||||
|
||||
protected static function findModified(array $currentConfiguration, array $newConfiguration): array
|
||||
{
|
||||
$differences = [];
|
||||
foreach ($newConfiguration as $key => $value) {
|
||||
if (!isset($currentConfiguration[$key]) || $currentConfiguration[$key] !== $value) {
|
||||
if (!isset($value) && isset($currentConfiguration[$key])) {
|
||||
$differences[$key] = '__UNSET';
|
||||
} elseif (isset($currentConfiguration[$key])
|
||||
&& is_array($value)
|
||||
&& is_array($currentConfiguration[$key])
|
||||
) {
|
||||
$differences[$key] = self::findModified($currentConfiguration[$key], $value);
|
||||
} else {
|
||||
$differences[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $differences;
|
||||
}
|
||||
|
||||
protected static function findRemoved(array $currentConfiguration, array $newConfiguration): array
|
||||
{
|
||||
$removed = [];
|
||||
foreach ($currentConfiguration as $key => $value) {
|
||||
if (!isset($newConfiguration[$key])) {
|
||||
$removed[$key] = '__UNSET';
|
||||
} elseif (isset($value) && is_array($value) && is_array($newConfiguration[$key])) {
|
||||
$removedInRecursion = self::findRemoved($value, $newConfiguration[$key]);
|
||||
if (!empty($removedInRecursion)) {
|
||||
$removed[$key] = $removedInRecursion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $removed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?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\Configuration\Tca;
|
||||
|
||||
/**
|
||||
* Automatically "enrich" TCA. This mainly adds "columns" definitions
|
||||
* based on "ctrl" settings. This is *not* for migration or preparation.
|
||||
*
|
||||
* This class is executed when building final TCA *after* "base"
|
||||
* files in "Configuration/TCA" have been loaded, and *before*
|
||||
* files in "Configuration/TCA/Overrides" are loaded.
|
||||
*
|
||||
* @internal Class and API may change any time.
|
||||
*/
|
||||
final readonly class TcaEnrichment
|
||||
{
|
||||
public function enrich(array $tca): array
|
||||
{
|
||||
$tca = $this->enrichDisabledField($tca);
|
||||
$tca = $this->enrichStarttimeField($tca);
|
||||
$tca = $this->enrichEndtimeField($tca);
|
||||
$tca = $this->enrichFeGroupField($tca);
|
||||
$tca = $this->enrichEditLockField($tca);
|
||||
$tca = $this->enrichDescriptionField($tca);
|
||||
$tca = $this->enrichLanguageField($tca);
|
||||
$tca = $this->setTransOrigPointerFieldInCtrl($tca);
|
||||
$tca = $this->enrichTransOrigPointerField($tca);
|
||||
$tca = $this->enrichTransOrigDiffSourceField($tca);
|
||||
$tca = $this->enrichTranslationSourceField($tca);
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichDisabledField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$disabledFieldName = $tableDefinition['ctrl']['enablecolumns']['disabled'] ?? null;
|
||||
if ($disabledFieldName && !is_array($tableDefinition['columns'][$disabledFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$disabledFieldName] = [
|
||||
'label' => 'core.db.general:enabled',
|
||||
'exclude' => true,
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
'renderType' => 'checkboxToggle',
|
||||
'default' => 0,
|
||||
'items' => [
|
||||
[
|
||||
'label' => '',
|
||||
'invertStateDisplay' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichStarttimeField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$starttimeFieldName = $tableDefinition['ctrl']['enablecolumns']['starttime'] ?? null;
|
||||
if ($starttimeFieldName && !is_array($tableDefinition['columns'][$starttimeFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$starttimeFieldName] = [
|
||||
'exclude' => true,
|
||||
'label' => 'core.db.general:starttime',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'default' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichEndtimeField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$endtimeFieldName = $tableDefinition['ctrl']['enablecolumns']['endtime'] ?? null;
|
||||
if ($endtimeFieldName && !is_array($tableDefinition['columns'][$endtimeFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$endtimeFieldName] = [
|
||||
'exclude' => true,
|
||||
'label' => 'core.db.general:endtime',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'default' => 0,
|
||||
'range' => [
|
||||
'upper' => mktime(0, 0, 0, 1, 1, 2106),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichFeGroupField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$feGroupFieldName = $tableDefinition['ctrl']['enablecolumns']['fe_group'] ?? null;
|
||||
if ($feGroupFieldName && !is_array($tableDefinition['columns'][$feGroupFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$feGroupFieldName] = [
|
||||
'exclude' => true,
|
||||
'label' => 'core.db.general:fe_group',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'size' => 5,
|
||||
'maxitems' => 20,
|
||||
'items' => [
|
||||
[
|
||||
'label' => 'core.db.general:fe_group.hide_at_login',
|
||||
'value' => -1,
|
||||
],
|
||||
[
|
||||
'label' => 'core.db.general:fe_group.any_login',
|
||||
'value' => -2,
|
||||
],
|
||||
[
|
||||
'label' => 'core.db.general:fe_group.usergroups',
|
||||
'value' => '--div--',
|
||||
],
|
||||
],
|
||||
'exclusiveKeys' => '-1,-2',
|
||||
'foreign_table' => 'fe_groups',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichEditLockField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$editLockFieldName = $tableDefinition['ctrl']['editlock'] ?? null;
|
||||
if ($editLockFieldName && !is_array($tableDefinition['columns'][$editLockFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$editLockFieldName] = [
|
||||
'displayCond' => 'HIDE_FOR_NON_ADMINS',
|
||||
'label' => 'core.db.general:editlock',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
'renderType' => 'checkboxToggle',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichDescriptionField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$descriptionFieldName = $tableDefinition['ctrl']['descriptionColumn'] ?? null;
|
||||
if ($descriptionFieldName && !is_array($tableDefinition['columns'][$descriptionFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$descriptionFieldName] = [
|
||||
'exclude' => true,
|
||||
'label' => 'core.db.general:description',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
'rows' => 5,
|
||||
'cols' => 30,
|
||||
'max' => 2000,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichLanguageField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$languageFieldName = $tableDefinition['ctrl']['languageField'] ?? null;
|
||||
if ($languageFieldName && !is_array($tableDefinition['columns'][$languageFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$languageFieldName] = [
|
||||
'exclude' => true,
|
||||
'label' => 'core.db.general:language',
|
||||
'config' => [
|
||||
'type' => 'language',
|
||||
],
|
||||
];
|
||||
}
|
||||
if ($languageFieldName && !is_array($tableDefinition['columns']['language_tag'] ?? null)) {
|
||||
$tca[$table]['columns']['language_tag'] = [
|
||||
'exclude' => true,
|
||||
'label' => 'Language Tag',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'size' => 10,
|
||||
'max' => 35,
|
||||
'eval' => 'trim',
|
||||
'default' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* When 'languageField' is set, 'transOrigPointerField' must be set as well.
|
||||
* We silently add 'transOrigPointerField' if that iss not the case.
|
||||
*
|
||||
* @todo: This obviously needs a consolidation in ctrl. We should have a single, probably
|
||||
* boolean ctrl toggle to make a table 'localization' aware, with core then handling
|
||||
* all internals. This will require streamlining the field names along the way, and
|
||||
* we can think about this as soon as sys_language_uid=-1 is gone.
|
||||
*/
|
||||
private function setTransOrigPointerFieldInCtrl(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
if (isset($tableDefinition['ctrl']['languageField']) && !isset($tableDefinition['ctrl']['transOrigPointerField'])) {
|
||||
$tca[$table]['ctrl']['transOrigPointerField'] = 'l10n_parent';
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichTransOrigPointerField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$transOrigPointerFieldName = $tableDefinition['ctrl']['transOrigPointerField'] ?? null;
|
||||
if ($transOrigPointerFieldName && !is_array($tableDefinition['columns'][$transOrigPointerFieldName] ?? null)) {
|
||||
$languageFieldName = $tableDefinition['ctrl']['languageField'];
|
||||
$tca[$table]['columns'][$transOrigPointerFieldName] = [
|
||||
'displayCond' => 'FIELD:' . $languageFieldName . ':>:0',
|
||||
'label' => 'core.db.general:l18n_parent',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'items' => [
|
||||
[
|
||||
'label' => '',
|
||||
'value' => 0,
|
||||
],
|
||||
],
|
||||
'foreign_table' => $table,
|
||||
'foreign_table_where' => 'AND {#' . $table . '}.{#pid}=###CURRENT_PID### AND {#' . $table . '}.{#' . $languageFieldName . '} IN (-1,0)',
|
||||
'default' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichTransOrigDiffSourceField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$transOrigDiffSourceFieldName = $tableDefinition['ctrl']['transOrigDiffSourceField'] ?? null;
|
||||
if ($transOrigDiffSourceFieldName && !is_array($tableDefinition['columns'][$transOrigDiffSourceFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$transOrigDiffSourceFieldName] = [
|
||||
'config' => [
|
||||
'type' => 'passthrough',
|
||||
'default' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichTranslationSourceField(array $tca): array
|
||||
{
|
||||
foreach ($tca as $table => $tableDefinition) {
|
||||
$translationSourceFieldName = $tableDefinition['ctrl']['translationSource'] ?? null;
|
||||
if ($translationSourceFieldName && !is_array($tableDefinition['columns'][$translationSourceFieldName] ?? null)) {
|
||||
$tca[$table]['columns'][$translationSourceFieldName] = [
|
||||
'config' => [
|
||||
'type' => 'passthrough',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Configuration\Tca;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
|
||||
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
|
||||
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
|
||||
/**
|
||||
* @internal Bootstrap related base TCA loading. Extensions must not use this.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class TcaFactory
|
||||
{
|
||||
public function __construct(
|
||||
private PackageManager $packageManager,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
#[Autowire(service: 'cache.core')]
|
||||
private PhpFrontend $codeCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The main production worker method.
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
$cacheData = $this->codeCache->require($this->getTcaCacheIdentifier());
|
||||
if ($cacheData) {
|
||||
$tca = $cacheData['tca'];
|
||||
} else {
|
||||
$tca = $this->create();
|
||||
$this->createBaseTcaCacheFile($tca);
|
||||
}
|
||||
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is (indirectly) used by extension manager when loading
|
||||
* extensions, by install tool bootstrap and cache warmup.
|
||||
*/
|
||||
public function create(): array
|
||||
{
|
||||
$tca = $this->loadConfigurationTcaFiles();
|
||||
$tca = $this->dispatchBeforeTcaOverridesEvent($tca);
|
||||
$tca = $this->enrichTca($tca);
|
||||
$tca = $this->loadConfigurationTcaOverridesFiles($tca);
|
||||
$tca = $this->migrateTca($tca);
|
||||
$tca = $this->prepareTca($tca);
|
||||
return $this->dispatchAfterTcaCompilationEvent($tca);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used by install tool LoadTcaService to check certain aspects of TCA
|
||||
*/
|
||||
public function createNotMigrated(): array
|
||||
{
|
||||
$tca = $this->loadConfigurationTcaFiles();
|
||||
$tca = $this->dispatchBeforeTcaOverridesEvent($tca);
|
||||
$tca = $this->enrichTca($tca);
|
||||
return $this->loadConfigurationTcaOverridesFiles($tca);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public since it's also used by CacheWarmupCommand
|
||||
*/
|
||||
public function createBaseTcaCacheFile(array $tca): void
|
||||
{
|
||||
$this->codeCache->set(
|
||||
$this->getTcaCacheIdentifier(),
|
||||
'return '
|
||||
. var_export(['tca' => $tca], true)
|
||||
. ';'
|
||||
);
|
||||
}
|
||||
|
||||
private function getTcaCacheIdentifier(): string
|
||||
{
|
||||
return (new PackageDependentCacheIdentifier($this->packageManager))->withPrefix('tca_base')->toString();
|
||||
}
|
||||
|
||||
private function loadConfigurationTcaFiles(): array
|
||||
{
|
||||
// To require TCA in a safe scoped environment avoiding local variable clashes.
|
||||
// Note: Return type 'mixed' is intended, otherwise broken TCA files with missing "return [];" statement would
|
||||
// emit a "return value must be of type array, int returned" PHP TypeError. This is mitigated by an array
|
||||
// check below.
|
||||
$scopedReturnRequire = static function (string $filename): mixed {
|
||||
return require $filename;
|
||||
};
|
||||
// First load "full table" files from Configuration/TCA
|
||||
$tca = [];
|
||||
$activePackages = $this->packageManager->getActivePackages();
|
||||
foreach ($activePackages as $package) {
|
||||
try {
|
||||
$finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA');
|
||||
} catch (\InvalidArgumentException) {
|
||||
// No such directory in this package
|
||||
continue;
|
||||
}
|
||||
foreach ($finder as $fileInfo) {
|
||||
$tcaOfTable = $scopedReturnRequire($fileInfo->getPathname());
|
||||
if (is_array($tcaOfTable)) {
|
||||
$tcaTableName = substr($fileInfo->getBasename(), 0, -4);
|
||||
$tca[$tcaTableName] = $tcaOfTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function enrichTca(array $tca): array
|
||||
{
|
||||
return (new TcaEnrichment())->enrich($tca);
|
||||
}
|
||||
|
||||
private function loadConfigurationTcaOverridesFiles(array $tca): array
|
||||
{
|
||||
// To require TCA Overrides in a safe scoped environment avoiding local variable clashes.
|
||||
$scopedRequire = static function (string $filename): void {
|
||||
require $filename;
|
||||
};
|
||||
// Execute override files from Configuration/TCA/Overrides
|
||||
$GLOBALS['TCA'] = $tca;
|
||||
$activePackages = $this->packageManager->getActivePackages();
|
||||
foreach ($activePackages as $package) {
|
||||
try {
|
||||
$finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA/Overrides');
|
||||
} catch (\InvalidArgumentException) {
|
||||
// No such directory in this package
|
||||
continue;
|
||||
}
|
||||
foreach ($finder as $fileInfo) {
|
||||
$scopedRequire($fileInfo->getPathname());
|
||||
}
|
||||
}
|
||||
$tca = $GLOBALS['TCA'];
|
||||
unset($GLOBALS['TCA']);
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function migrateTca(array $tca): array
|
||||
{
|
||||
// Call the TcaMigration and log any deprecations.
|
||||
$tcaMigration = new TcaMigration();
|
||||
$tcaProcessingResult = $tcaMigration->migrate($tca);
|
||||
$messages = $tcaProcessingResult->getMessages();
|
||||
if (!empty($messages)) {
|
||||
$context = 'Automatic TCA migration done during bootstrap. Please adapt TCA accordingly, these migrations'
|
||||
. ' will be removed. The backend module "Configuration -> TCA" shows the modified values.'
|
||||
. ' Please adapt these areas:';
|
||||
array_unshift($messages, $context);
|
||||
trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
|
||||
}
|
||||
return $tcaProcessingResult->getTca();
|
||||
}
|
||||
|
||||
private function prepareTca(array $tca): array
|
||||
{
|
||||
return (new TcaPreparation())->prepare($tca);
|
||||
}
|
||||
|
||||
private function dispatchBeforeTcaOverridesEvent($tca): array
|
||||
{
|
||||
return $this->eventDispatcher->dispatch(new BeforeTcaOverridesEvent($tca))->getTca();
|
||||
}
|
||||
|
||||
private function dispatchAfterTcaCompilationEvent($tca): array
|
||||
{
|
||||
$GLOBALS['TCA'] = $tca;
|
||||
$tca = $this->eventDispatcher->dispatch(new AfterTcaCompilationEvent($tca))->getTca();
|
||||
unset($GLOBALS['TCA']);
|
||||
return $tca;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,624 @@
|
||||
<?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\Configuration\Tca;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Prepare TCA. Used in bootstrap and Flex Form Data Structures,
|
||||
* executed *after* TcaMigration.
|
||||
* This is used to add *internal TCA details* needed by core.
|
||||
* For instance, type=category fields receive all the relation
|
||||
* details in order to work properly.
|
||||
*
|
||||
* @internal Class and API may change any time.
|
||||
*/
|
||||
readonly class TcaPreparation
|
||||
{
|
||||
/**
|
||||
* Prepare TCA
|
||||
*
|
||||
* This class is typically called within bootstrap with empty caches after all TCA
|
||||
* files from extensions have been loaded. The preparation is then applied and
|
||||
* the prepared result is cached.
|
||||
* For flex form TCA, this class is called dynamically if opening a record in the backend.
|
||||
*
|
||||
* See unit tests for details.
|
||||
*/
|
||||
public function prepare(array $tca, bool $isFlexForm = false): array
|
||||
{
|
||||
$tca = $this->configureCategoryRelations($tca, $isFlexForm);
|
||||
$tca = $this->configureFileReferences($tca, $isFlexForm);
|
||||
$tca = $this->configureEmailSoftReferences($tca);
|
||||
$tca = $this->configureLinkSoftReferences($tca);
|
||||
$tca = $this->configureSelectSingle($tca);
|
||||
$tca = $this->configureRelationshipToOne($tca);
|
||||
$tca = $this->addSystemFieldsToShowitemTypes($tca);
|
||||
$tca = $this->addIgnoredPageTypeRestrictionRecords($tca);
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares TCA configuration of type='category' fields.
|
||||
*
|
||||
* It adds some TCA config settings so category fields end up with similar
|
||||
* config as type='select' field, but in a more restricted way.
|
||||
* Some settings could also be set in TCA directly, but some fields
|
||||
* can not be overridden, e.g. foreign_table.
|
||||
*
|
||||
* This also sets necessary MM properties, in case relationship is
|
||||
* set to "manyToMany", which is the default. Note it is "oneToMany"
|
||||
* with flex forms, since flex forms do NOT support "manyToMany".
|
||||
*
|
||||
* Finally, all category fields with a "manyToMany" relationship are
|
||||
* added to the MM_oppositeUsage of sys_category "items".
|
||||
*
|
||||
* Important: Since this method defines a "foreign_table_where", this
|
||||
* must always be executed before prepareQuotingOfTableNamesAndColumnNames().
|
||||
*/
|
||||
protected function configureCategoryRelations(array $tca, bool $isFlexForm): array
|
||||
{
|
||||
foreach ($tca as $table => &$tableDefinition) {
|
||||
if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
|
||||
if (($fieldConfig['config']['type'] ?? '') !== 'category') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($fieldConfig['label'])) {
|
||||
$fieldConfig['label'] = 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
|
||||
}
|
||||
// Force foreign_table for type category
|
||||
$fieldConfig['config']['foreign_table'] = 'sys_category';
|
||||
// Initialize default column configuration and merge it with already defined
|
||||
$fieldConfig['config']['size'] ??= 20;
|
||||
$defaultTag = \Local\Multilanguage\Service\DefaultLanguageTagService::getTag();
|
||||
$fieldConfig['config']['foreign_table_where'] ??= " AND {#sys_category}.{#language_tag} IN ('" . $defaultTag . "', '')";
|
||||
if (empty($fieldConfig['config']['relationship'])) {
|
||||
// In case no relationship is given, set "manyToMany" for non flex form, but "oneToMany" with flex form.
|
||||
$fieldConfig['config']['relationship'] = $isFlexForm ? 'oneToMany' : 'manyToMany';
|
||||
}
|
||||
|
||||
// Sanitize 'relationship'
|
||||
if ($isFlexForm && !in_array($fieldConfig['config']['relationship'], ['oneToOne', 'oneToMany'], true)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'"relationship" must be one of "oneToOne" or "oneToMany", "manyToMany" is not supported as "relationship"'
|
||||
. ' for field ' . $fieldName . ' of type "category" in flexform.',
|
||||
1627640208
|
||||
);
|
||||
}
|
||||
if (!in_array($fieldConfig['config']['relationship'], ['oneToOne', 'oneToMany', 'manyToMany'], true)) {
|
||||
throw new \RuntimeException(
|
||||
$fieldName . ' of table ' . $table . ' is defined as type category with relationship "'
|
||||
. $fieldConfig['config']['relationship'] . '", but only "oneToOne", "oneToMany" and "manyToMany"'
|
||||
. ' are allowed.',
|
||||
1627898896
|
||||
);
|
||||
}
|
||||
|
||||
// Set the maxitems value (necessary for DataHandling and FormEngine)
|
||||
if ($fieldConfig['config']['relationship'] === 'oneToOne') {
|
||||
// In case relationship is set to "oneToOne", the database column for this
|
||||
// field will be an integer column. This means, only one uid can be stored.
|
||||
// Therefore, maxitems must be 1. Sanitize for flex form fields as well.
|
||||
if ((int)($fieldConfig['config']['maxitems'] ?? 0) > 1) {
|
||||
throw new \RuntimeException(
|
||||
$fieldName . ' of table ' . $table . ' is defined as type category with an oneToOne relationship. '
|
||||
. 'Therefore maxitems must be 1. Otherwise, use oneToMany or manyToMany as relationship instead.',
|
||||
1627335016
|
||||
);
|
||||
}
|
||||
$fieldConfig['config']['maxitems'] = 1;
|
||||
} elseif (!($fieldConfig['config']['maxitems'] ?? false)) {
|
||||
// In case maxitems is not set or set to 0, set the default value "99999"
|
||||
$fieldConfig['config']['maxitems'] = 99999;
|
||||
} elseif ($fieldConfig['config']['relationship'] === 'oneToMany'
|
||||
&& (int)($fieldConfig['config']['maxitems'] ?? 0) === 1
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
$fieldName . ' of table ' . $table . ' is defined as type category with a ' . $fieldConfig['config']['relationship']
|
||||
. ' relationship. Therefore, maxitems can not be set to 1. Use oneToOne as relationship instead.',
|
||||
1627335017
|
||||
);
|
||||
}
|
||||
|
||||
// Add the default value if not set
|
||||
if (!isset($fieldConfig['config']['default'])
|
||||
&& $fieldConfig['config']['relationship'] !== 'oneToMany'
|
||||
) {
|
||||
// @todo: This is db wise not accurate: A oneToOne relation without a relation being assigned,
|
||||
// @todo: should have NULL as value, not 0, since 0 looks like a uid, but it isn't.
|
||||
// @todo: The field should be nullable and DH should handle that.
|
||||
$fieldConfig['config']['default'] = 0;
|
||||
}
|
||||
|
||||
// Add MM related properties in case relationship is set to "manyToMany".
|
||||
// This will not be done for the sys_category table itself. Not relevant with flex forms.
|
||||
if ($fieldConfig['config']['relationship'] === 'manyToMany' && $table !== 'sys_category') {
|
||||
// Note these settings are hard coded here and can't be overridden.
|
||||
$fieldConfig['config'] = array_replace_recursive($fieldConfig['config'], [
|
||||
'MM' => 'sys_category_record_mm',
|
||||
'MM_opposite_field' => 'items',
|
||||
'MM_match_fields' => [
|
||||
'tablenames' => $table,
|
||||
'fieldname' => $fieldName,
|
||||
],
|
||||
]);
|
||||
// Register opposite references for the foreign side of a category relation
|
||||
if (empty($tca['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$table])) {
|
||||
$tca['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$table] = [];
|
||||
}
|
||||
if (!in_array($fieldName, $tca['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$table], true)) {
|
||||
$tca['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$table][] = $fieldName;
|
||||
}
|
||||
// Take specific value of exclude flag into account
|
||||
if (!isset($fieldConfig['exclude'])) {
|
||||
$fieldConfig['exclude'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
protected function configureFileReferences(array $tca, bool $isFlexForm): array
|
||||
{
|
||||
foreach ($tca as $table => &$tableDefinition) {
|
||||
if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
|
||||
if (($fieldConfig['config']['type'] ?? '') === 'file') {
|
||||
// Set static values for this type. Most of them are not needed due to the
|
||||
// dedicated TCA type. However a lot of underlying code in DataHandler and
|
||||
// friends relies on those keys, especially "foreign_table" and "foreign_selector".
|
||||
// @todo Check which of those values can be removed since only used by FormEngine
|
||||
$fieldConfig['config'] = array_replace_recursive($fieldConfig['config'], [
|
||||
'foreign_table' => 'sys_file_reference',
|
||||
'foreign_field' => 'uid_foreign',
|
||||
'foreign_sortby' => 'sorting_foreign',
|
||||
'foreign_table_field' => 'tablenames',
|
||||
'foreign_label' => 'uid_local',
|
||||
'foreign_selector' => 'uid_local',
|
||||
]);
|
||||
if (!isset($fieldConfig['config']['foreign_match_fields']['fieldname'])) {
|
||||
$fieldConfig['config']['foreign_match_fields']['fieldname'] = $fieldName;
|
||||
}
|
||||
if (!$isFlexForm) {
|
||||
$fieldConfig['config']['foreign_match_fields']['tablenames'] = $table;
|
||||
}
|
||||
$fieldConfig['config'] = $this->configureAllowedDisallowedFileExtensions($fieldConfig['config']);
|
||||
}
|
||||
if (is_array($fieldConfig['config']['overrideChildTca'] ?? null)) {
|
||||
$fieldConfig['config']['overrideChildTca'] = $this->configureAllowedDisallowedInOverrideChildTca($fieldConfig['config']['overrideChildTca']);
|
||||
}
|
||||
}
|
||||
unset($fieldConfig);
|
||||
if (is_array($tableDefinition['types'] ?? null)) {
|
||||
foreach ($tableDefinition['types'] as &$typeConfig) {
|
||||
if (!isset($typeConfig['columnsOverrides']) || !is_array($typeConfig['columnsOverrides'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($typeConfig['columnsOverrides'] as &$columnsOverridesConfig) {
|
||||
if (!isset($columnsOverridesConfig['config']) || !is_array($columnsOverridesConfig['config'])) {
|
||||
continue;
|
||||
}
|
||||
$columnsOverridesConfig['config'] = $this->configureAllowedDisallowedFileExtensions($columnsOverridesConfig['config']);
|
||||
if (is_array($columnsOverridesConfig['config']['overrideChildTca'] ?? null)) {
|
||||
$columnsOverridesConfig['config']['overrideChildTca'] = $this->configureAllowedDisallowedInOverrideChildTca($columnsOverridesConfig['config']['overrideChildTca']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* configureFileReferences() helper
|
||||
*/
|
||||
protected function configureAllowedDisallowedInOverrideChildTca(array $overrideChildTcaConfig): array
|
||||
{
|
||||
if (is_array($overrideChildTcaConfig['columns'] ?? null)) {
|
||||
foreach ($overrideChildTcaConfig['columns'] as &$overrideChildTcaColumnConfig) {
|
||||
if (!isset($overrideChildTcaColumnConfig['config'])) {
|
||||
continue;
|
||||
}
|
||||
$overrideChildTcaColumnConfig['config'] = $this->configureAllowedDisallowedFileExtensions($overrideChildTcaColumnConfig['config']);
|
||||
}
|
||||
unset($overrideChildTcaColumnConfig);
|
||||
}
|
||||
if (is_array($overrideChildTcaConfig['types'] ?? null)) {
|
||||
foreach ($overrideChildTcaConfig['types'] as &$overrideChildTcaTypeConfig) {
|
||||
if (!isset($overrideChildTcaTypeConfig['config'])) {
|
||||
continue;
|
||||
}
|
||||
$overrideChildTcaTypeConfig['config'] = $this->configureAllowedDisallowedFileExtensions($overrideChildTcaTypeConfig['config']);
|
||||
}
|
||||
}
|
||||
return $overrideChildTcaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* configureFileReferences() helper
|
||||
*/
|
||||
protected function configureAllowedDisallowedFileExtensions(array $config): array
|
||||
{
|
||||
if (!empty($allowed = ($config['allowed'] ?? null))) {
|
||||
$config['allowed'] = $this->prepareFileExtensions($allowed);
|
||||
}
|
||||
if (!empty($disallowed = ($config['disallowed'] ?? null))) {
|
||||
$config['disallowed'] = $this->prepareFileExtensions($disallowed);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* configureFileReferences() helper: Ensures format, replaces placeholders and remove duplicates
|
||||
*/
|
||||
protected function prepareFileExtensions(mixed $fileExtensions): string
|
||||
{
|
||||
if (is_array($fileExtensions)) {
|
||||
$fileExtensions = implode(',', $fileExtensions);
|
||||
} else {
|
||||
$fileExtensions = (string)$fileExtensions;
|
||||
}
|
||||
// Replace placeholders with the corresponding $GLOBALS value for now
|
||||
if (preg_match_all('/common-(image|text|media)-types/', $fileExtensions, $matches)) {
|
||||
foreach ($matches[1] as $key => $type) {
|
||||
$fileExtensions = str_replace(
|
||||
$matches[0][$key],
|
||||
$GLOBALS['TYPO3_CONF_VARS'][$type === 'image' ? 'GFX' : 'SYS'][$type . 'file_ext'] ?? '',
|
||||
$fileExtensions
|
||||
);
|
||||
}
|
||||
}
|
||||
return StringUtility::uniqueList($fileExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "'softref' = 'email[subst]'" to all 'type' = 'email' column fields.
|
||||
*/
|
||||
protected function configureEmailSoftReferences(array $tca): array
|
||||
{
|
||||
foreach ($tca as &$tableDefinition) {
|
||||
if (!is_array($tableDefinition['columns'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as &$fieldConfig) {
|
||||
if (($fieldConfig['config']['type'] ?? null) === 'email') {
|
||||
// Hard set/override: 'softref' is not listed as property for type=email at all,
|
||||
// there is little need to have this configurable.
|
||||
$fieldConfig['config']['softref'] = 'email[subst]';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "'softref' = 'typolink'" to all 'type' = 'link' column fields.
|
||||
*/
|
||||
protected function configureLinkSoftReferences(array $tca): array
|
||||
{
|
||||
foreach ($tca as &$tableDefinition) {
|
||||
if (!is_array($tableDefinition['columns'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as &$fieldConfig) {
|
||||
if (($fieldConfig['config']['type'] ?? null) === 'link') {
|
||||
// Hard set/override: 'softref' is not listed as property for type=link at all,
|
||||
// there is little need to have this configurable.
|
||||
$fieldConfig['config']['softref'] = 'typolink';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "'relationship' for TCA type "select" fields, having "selectSingle" set as renderType and are
|
||||
* pointing to a "foreign_table". Depending on further configuration, this will set the "relationship"
|
||||
* to either "manyToMany" (in case "MM" is set) or to "manyToOne".
|
||||
* Already defined "relationship" is not overwritten!
|
||||
*
|
||||
* This is mainly done to prevent checks on the renderType, which should be avoided.
|
||||
*/
|
||||
protected function configureSelectSingle(array $tca): array
|
||||
{
|
||||
foreach ($tca as &$tableDefinition) {
|
||||
if (!is_array($tableDefinition['columns'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as &$fieldConfig) {
|
||||
if (($fieldConfig['config']['type'] ?? null) !== 'select'
|
||||
|| ($fieldConfig['config']['renderType'] ?? null) !== 'selectSingle'
|
||||
|| !isset($fieldConfig['config']['foreign_table'])
|
||||
|| isset($fieldConfig['config']['relationship'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($fieldConfig['config']['MM'])) {
|
||||
$fieldConfig['config']['relationship'] = 'manyToMany';
|
||||
} else {
|
||||
$fieldConfig['config']['relationship'] = 'manyToOne';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "'maxitems' => 1" to all relation type column fields with 'relationship' set to 'oneToOne' or 'manyToOne'.
|
||||
*/
|
||||
protected function configureRelationshipToOne(array $tca): array
|
||||
{
|
||||
foreach ($tca as &$tableDefinition) {
|
||||
if (!is_array($tableDefinition['columns'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tableDefinition['columns'] as &$fieldConfig) {
|
||||
$type = $fieldConfig['config']['type'] ?? null;
|
||||
if (in_array($type, ['select', 'inline', 'group', 'folder', 'file'], true)
|
||||
&& in_array($fieldConfig['config']['relationship'] ?? null, ['oneToOne', 'manyToOne'], true)
|
||||
) {
|
||||
// Hard set/override: 'maxitems' to 1, since relationship [x]ToOne - as the name suggests -
|
||||
// only allows a single item to be selected.
|
||||
$fieldConfig['config']['maxitems'] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all system fields (CType, colPos, hidden etc.) are automatically added
|
||||
* to the showitem list of all CTypes in tt_content. As custom CTypes might have added
|
||||
* the fields, the respective fields also need to be removed first.
|
||||
*/
|
||||
protected function addSystemFieldsToShowitemTypes(array $tca): array
|
||||
{
|
||||
// @todo Only deal with this for tt_content in v13, as other parts might be too intrusive
|
||||
// might change in v14
|
||||
if (!isset($tca['tt_content'])) {
|
||||
return $tca;
|
||||
}
|
||||
// Only proceed in case the record type field is defined
|
||||
$typeField = (string)($tca['tt_content']['ctrl']['type'] ?? '');
|
||||
if ($typeField === '') {
|
||||
return $tca;
|
||||
}
|
||||
// Build list of values (fields and palettes) which should be removed
|
||||
// from custom palettes, because they will be added automatically.
|
||||
$listOfValuesToRemove = [
|
||||
'--div--;core.form.tabs:general',
|
||||
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general',
|
||||
'--div--;core.form.tabs:language',
|
||||
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language',
|
||||
'--div--;core.form.tabs:access',
|
||||
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access',
|
||||
'--div--;core.form.tabs:notes',
|
||||
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes',
|
||||
'--palette--;;general',
|
||||
'--palette--;;language',
|
||||
'--palette--;;access',
|
||||
'--palette--;;hidden',
|
||||
'colPos',
|
||||
];
|
||||
$listOfValuesToRemove[] = $typeField;
|
||||
if (($languageField = (string)($tca['tt_content']['ctrl']['languageField'] ?? '')) !== '') {
|
||||
$listOfValuesToRemove[] = $languageField;
|
||||
}
|
||||
if (($transOrigPointerField = (string)($tca['tt_content']['ctrl']['transOrigPointerField'] ?? '')) !== '') {
|
||||
$listOfValuesToRemove[] = $transOrigPointerField;
|
||||
}
|
||||
$enablecolumns = $tca['tt_content']['ctrl']['enablecolumns'] ?? [];
|
||||
foreach ($enablecolumns as $fieldName) {
|
||||
$listOfValuesToRemove[] = $fieldName;
|
||||
}
|
||||
if (($editlock = (string)($tca['tt_content']['ctrl']['editlock'] ?? '')) !== '') {
|
||||
$listOfValuesToRemove[] = $editlock;
|
||||
}
|
||||
if (($descriptionColumn = (string)($tca['tt_content']['ctrl']['descriptionColumn'] ?? '')) !== '') {
|
||||
$listOfValuesToRemove[] = $descriptionColumn;
|
||||
}
|
||||
|
||||
// Remove any system field from custom palettes
|
||||
foreach ($tca['tt_content']['palettes'] as $paletteName => &$paletteConfig) {
|
||||
if (in_array($paletteName, ['general', 'language', 'access', 'hidden'], true)) {
|
||||
continue;
|
||||
}
|
||||
$showItemSplitted = GeneralUtility::trimExplode(',', $paletteConfig['showitem'], true);
|
||||
$paletteConfig['showitem'] = implode(',', array_diff($this->removeCustomFieldLabels($showItemSplitted, $listOfValuesToRemove), $listOfValuesToRemove));
|
||||
}
|
||||
unset($paletteConfig);
|
||||
|
||||
// Process the content types
|
||||
foreach ($tca['tt_content']['types'] as $type => $typeInformation) {
|
||||
// Remove any of the special fields from the content type's current showitem
|
||||
$showItemSplitted = GeneralUtility::trimExplode(',', $typeInformation['showitem'] ?? '', true);
|
||||
$showItemFiltered = array_diff($this->removeCustomFieldLabels($showItemSplitted, $listOfValuesToRemove), $listOfValuesToRemove);
|
||||
|
||||
// Extract all fields of the extended tab to add it at the end
|
||||
[$showItemList, $extendedParts] = $this->extractExtendedParts($showItemFiltered);
|
||||
|
||||
// Add record type field (usually "CType") and colPos either using the "general" palette
|
||||
// or manually, in case the palette does not exist or does not contain the fields.
|
||||
$generalPaletteItems = $this->removeCustomFieldLabels(GeneralUtility::trimExplode(',', $tca['tt_content']['palettes']['general']['showitem'] ?? '', true), $listOfValuesToRemove);
|
||||
if (in_array($typeField, $generalPaletteItems, true) && in_array('colPos', $generalPaletteItems, true)) {
|
||||
$showItemParts = ['--palette--;;general'];
|
||||
} else {
|
||||
$showItemParts = [
|
||||
$typeField,
|
||||
'colPos',
|
||||
];
|
||||
}
|
||||
|
||||
// Because FormEngine will add the general tab automatically, we will not do this here
|
||||
// However, if the first item in the $showItemList is actually a tab (--div--), we need to
|
||||
// add if before the "first fields"
|
||||
if (str_starts_with($showItemList[0] ?? '', '--div--')) {
|
||||
array_unshift($showItemParts, $showItemList[0]);
|
||||
unset($showItemList[0]);
|
||||
}
|
||||
$showItemParts = array_merge($showItemParts, $showItemList);
|
||||
|
||||
// Add language field either using the "language" palette or manually,
|
||||
// in case the palette does not exist or does not contain the field.
|
||||
if ($languageField !== '') {
|
||||
$showItemParts[] = '--div--;core.form.tabs:language';
|
||||
$languagePaletteItems = $this->removeCustomFieldLabels(GeneralUtility::trimExplode(',', $tca['tt_content']['palettes']['language']['showitem'] ?? '', true), $listOfValuesToRemove);
|
||||
if (in_array($languageField, $languagePaletteItems, true)
|
||||
&& ($transOrigPointerField === '' || in_array($transOrigPointerField, $languagePaletteItems, true))
|
||||
) {
|
||||
$showItemParts[] = '--palette--;;language';
|
||||
} else {
|
||||
$showItemParts[] = $languageField;
|
||||
if ($transOrigPointerField) {
|
||||
$showItemParts[] = $transOrigPointerField;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enable fields either using the "hidden" amd "access" palettes or
|
||||
// manually, in case the palettes do not exist or do not contain the fields.
|
||||
if ($enablecolumns !== [] || $editlock !== '') {
|
||||
$showItemParts[] = '--div--;core.form.tabs:access';
|
||||
if (isset($enablecolumns['disabled'])) {
|
||||
$hiddenPaletteParts = $this->removeCustomFieldLabels(GeneralUtility::trimExplode(',', $tca['tt_content']['palettes']['hidden']['showitem'] ?? '', true), $listOfValuesToRemove);
|
||||
if (in_array($enablecolumns['disabled'], $hiddenPaletteParts, true)) {
|
||||
$showItemParts[] = '--palette--;;hidden';
|
||||
} else {
|
||||
$showItemParts[] = $enablecolumns['disabled'];
|
||||
}
|
||||
}
|
||||
if ((isset($enablecolumns['starttime']) || isset($enablecolumns['endtime']) || isset($enablecolumns['fe_group']) || $editlock)) {
|
||||
$accessPaletteParts = $this->removeCustomFieldLabels(GeneralUtility::trimExplode(',', $tca['tt_content']['palettes']['access']['showitem'] ?? '', true), $listOfValuesToRemove);
|
||||
if ((!isset($enablecolumns['starttime']) || in_array($enablecolumns['starttime'], $accessPaletteParts, true))
|
||||
&& (!isset($enablecolumns['endtime']) || in_array($enablecolumns['endtime'], $accessPaletteParts, true))
|
||||
&& (!isset($enablecolumns['fe_group']) || in_array($enablecolumns['fe_group'], $accessPaletteParts, true))
|
||||
&& (!$editlock || in_array($editlock, $accessPaletteParts, true))
|
||||
) {
|
||||
$showItemParts[] = '--palette--;;access';
|
||||
} else {
|
||||
if (isset($enablecolumns['starttime'])) {
|
||||
$showItemParts[] = $enablecolumns['starttime'];
|
||||
}
|
||||
if (isset($enablecolumns['endtime'])) {
|
||||
$showItemParts[] = $enablecolumns['endtime'];
|
||||
}
|
||||
if (isset($enablecolumns['fe_group'])) {
|
||||
$showItemParts[] = $enablecolumns['fe_group'];
|
||||
}
|
||||
if ($editlock) {
|
||||
$showItemParts[] = $editlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add description column if defined
|
||||
if ($descriptionColumn !== '') {
|
||||
$showItemParts[] = '--div--;core.form.tabs:notes,' . $descriptionColumn;
|
||||
}
|
||||
|
||||
// Add extended tab at the end - if it exists
|
||||
$showItemParts = array_merge($showItemParts, $extendedParts);
|
||||
|
||||
// Merge parts together
|
||||
$tca['tt_content']['types'][$type]['showitem'] = trim(implode(',', $showItemParts), ',');
|
||||
}
|
||||
return $tca;
|
||||
}
|
||||
|
||||
private function extractExtendedParts(array $showItemFiltered): array
|
||||
{
|
||||
$extendedParts = [];
|
||||
$addFields = false;
|
||||
foreach ($showItemFiltered as $key => $part) {
|
||||
if ($part === '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended' || $part === '--div--;core.form.tabs:extended') {
|
||||
$extendedParts[] = $part;
|
||||
$addFields = true;
|
||||
unset($showItemFiltered[$key]);
|
||||
} elseif ($addFields) {
|
||||
if (str_starts_with($part, '--div--')) {
|
||||
break;
|
||||
}
|
||||
$extendedParts[] = $part;
|
||||
unset($showItemFiltered[$key]);
|
||||
}
|
||||
}
|
||||
return [$showItemFiltered, $extendedParts];
|
||||
}
|
||||
|
||||
private function removeCustomFieldLabels(array $showitemParts, array $fieldList): array
|
||||
{
|
||||
if ($fieldList === []) {
|
||||
return $showitemParts;
|
||||
}
|
||||
foreach ($showitemParts as &$showItem) {
|
||||
// Tabs keep their labels
|
||||
if (str_starts_with($showItem, '--div--')) {
|
||||
continue;
|
||||
}
|
||||
// Remove label of palette
|
||||
if (str_starts_with($showItem, '--palette--')) {
|
||||
$parts = GeneralUtility::trimExplode(';', $showItem, true, 3);
|
||||
// Palette without label, continue.
|
||||
if (count($parts) !== 3) {
|
||||
continue;
|
||||
}
|
||||
$paletteName = '--palette--;;' . $parts[2];
|
||||
if (in_array($paletteName, $fieldList, true)) {
|
||||
$showItem = $paletteName;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// This must be a field.
|
||||
$parts = GeneralUtility::trimExplode(';', $showItem, true, 2);
|
||||
$fieldName = $parts[0];
|
||||
// Just keep the first part => the fieldname in case field is defined in the $fieldList
|
||||
if (in_array($fieldName, $fieldList, true)) {
|
||||
$showItem = $fieldName;
|
||||
}
|
||||
}
|
||||
return $showitemParts;
|
||||
}
|
||||
|
||||
protected function addIgnoredPageTypeRestrictionRecords(array $tca): array
|
||||
{
|
||||
$allowedRecordTypes = [];
|
||||
foreach ($tca as $table => $configuration) {
|
||||
if ($configuration['ctrl']['security']['ignorePageTypeRestriction'] ?? false) {
|
||||
$allowedRecordTypes[] = $table;
|
||||
}
|
||||
}
|
||||
if ($allowedRecordTypes === []) {
|
||||
return $tca;
|
||||
}
|
||||
$mergedAllowedRecords = array_merge(
|
||||
$tca['pages']['ctrl']['defaultAllowedRecordTypes'] ?? [],
|
||||
$allowedRecordTypes
|
||||
);
|
||||
$tca['pages']['ctrl']['defaultAllowedRecordTypes'] = array_unique($mergedAllowedRecords);
|
||||
return $tca;
|
||||
}
|
||||
}
|
||||
@@ -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\Configuration\Tca;
|
||||
|
||||
/**
|
||||
* @internal internal data object - only to be used within TYPO3 Core
|
||||
*/
|
||||
final readonly class TcaProcessingResult
|
||||
{
|
||||
public function __construct(
|
||||
private array $tca,
|
||||
/** Accumulate messages, occurred on TCA processing, e.g. by TcaMigration. */
|
||||
private array $messages = []
|
||||
) {}
|
||||
|
||||
public function getTca(): array
|
||||
{
|
||||
return $this->tca;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMessages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
public function withTca(array $tca): TcaProcessingResult
|
||||
{
|
||||
return new self($tca, $this->messages);
|
||||
}
|
||||
|
||||
public function withAdditionalMessages(string ...$messages): TcaProcessingResult
|
||||
{
|
||||
return new self($this->tca, array_merge($this->messages, $messages));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user