TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<?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\Service\Archive;
|
||||
|
||||
use TYPO3\CMS\Core\Exception\Archive\ExtractException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service that handles zip creation and extraction
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class ZipService
|
||||
{
|
||||
/**
|
||||
* Extracts the zip archive to a given directory. This method makes sure a file cannot be placed outside the directory.
|
||||
*
|
||||
* @throws ExtractException
|
||||
*/
|
||||
public function extract(string $fileName, string $directory): bool
|
||||
{
|
||||
$this->assertDirectoryIsWritable($directory);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
$state = $zip->open($fileName);
|
||||
if ($state !== true) {
|
||||
throw new ExtractException(
|
||||
sprintf('Unable to open zip file %s, error code %d', $fileName, $state),
|
||||
1565709712
|
||||
);
|
||||
}
|
||||
|
||||
$result = $zip->extractTo($directory);
|
||||
$zip->close();
|
||||
if ($result) {
|
||||
GeneralUtility::fixPermissions(rtrim($directory, '/'), true);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ExtractException
|
||||
*/
|
||||
public function verify(string $fileName): bool
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
$state = $zip->open($fileName);
|
||||
if ($state !== true) {
|
||||
throw new ExtractException(
|
||||
sprintf('Unable to open zip file %s, error code %d', $fileName, $state),
|
||||
1565709713
|
||||
);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = str_replace('\\', '/', (string)$zip->getNameIndex($i));
|
||||
if (preg_match('#/(?:\.{2,})+#', $entryName) // Contains any traversal sequence starting with a slash, e.g. /../, /.., /.../
|
||||
|| preg_match('#^(?:\.{2,})+/#', $entryName) // Starts with a traversal sequence, e.g. ../, .../
|
||||
) {
|
||||
throw new ExtractException(
|
||||
sprintf('Suspicious sequence in zip file %s: %s', $fileName, $entryName),
|
||||
1565709714
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function assertDirectoryIsWritable(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Directory %s does not exist', $directory),
|
||||
1565773005
|
||||
);
|
||||
}
|
||||
if (!is_writable($directory)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Directory %s is not writable', $directory),
|
||||
1565773006
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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\Service;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
|
||||
use TYPO3\CMS\Core\Database\Schema\SqlReader;
|
||||
|
||||
/**
|
||||
* Service class executing database tasks for upgrade wizards
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class DatabaseUpgradeWizardsService
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionPool $connectionPool,
|
||||
private SqlReader $sqlReader,
|
||||
private SchemaMigrator $schemaMigrator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a list of tables, single columns and indexes to add.
|
||||
*
|
||||
* @return array{
|
||||
* tables?: list<array{table: string}>,
|
||||
* columns?: list<array{table: string, field: string}>,
|
||||
* indexes?: list<array{table: string, index: string}>
|
||||
* }
|
||||
*/
|
||||
public function getBlockingDatabaseAdds(): array
|
||||
{
|
||||
$databaseDefinitions = $this->sqlReader->getCreateTableStatementArray($this->sqlReader->getTablesDefinitionString());
|
||||
$databaseDifferences = $this->schemaMigrator->getSchemaDiffs($databaseDefinitions);
|
||||
$adds = [];
|
||||
foreach ($databaseDifferences as $schemaDiff) {
|
||||
foreach ($schemaDiff->getCreatedTables() as $newTable) {
|
||||
/** @var Table $newTable */
|
||||
if (!is_array($adds['tables'] ?? false)) {
|
||||
$adds['tables'] = [];
|
||||
}
|
||||
$adds['tables'][] = [
|
||||
'table' => $newTable->getName(),
|
||||
];
|
||||
}
|
||||
foreach ($schemaDiff->getAlteredTables() as $changedTable) {
|
||||
foreach ($changedTable->getAddedColumns() as $addedColumn) {
|
||||
/** @var Column $addedColumn */
|
||||
if (!is_array($adds['columns'] ?? false)) {
|
||||
$adds['columns'] = [];
|
||||
}
|
||||
$adds['columns'][] = [
|
||||
'table' => $changedTable->getOldTable()->getName(),
|
||||
'field' => $addedColumn->getName(),
|
||||
];
|
||||
}
|
||||
foreach ($changedTable->getAddedIndexes() as $addedIndex) {
|
||||
/** $var Index $addedIndex */
|
||||
if (!is_array($adds['indexes'] ?? false)) {
|
||||
$adds['indexes'] = [];
|
||||
}
|
||||
$adds['indexes'][] = [
|
||||
'table' => $changedTable->getOldTable()->getName(),
|
||||
'index' => $addedIndex->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $adds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing tables, indexes and fields to DB.
|
||||
*
|
||||
* @return array<string, string> Every sql statement as key with empty string or error message as value
|
||||
*/
|
||||
public function addMissingTablesAndFields(): array
|
||||
{
|
||||
$databaseDefinitions = $this->sqlReader->getCreateTableStatementArray($this->sqlReader->getTablesDefinitionString());
|
||||
return $this->schemaMigrator->install($databaseDefinitions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if DB main charset on mysql is utf8
|
||||
*
|
||||
* @return bool True if charset is ok
|
||||
*/
|
||||
public function isDatabaseCharsetUtf8(): bool
|
||||
{
|
||||
$connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
$isDefaultConnectionMysql = $platform instanceof DoctrineMariaDBPlatform || $platform instanceof DoctrineMySQLPlatform;
|
||||
if (!$isDefaultConnectionMysql) {
|
||||
// Not tested on non mysql
|
||||
$charsetOk = true;
|
||||
} else {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
|
||||
->from('information_schema.SCHEMATA')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'SCHEMA_NAME',
|
||||
$queryBuilder->createNamedParameter($connection->getDatabase())
|
||||
)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
// check if database charset is utf-8, also allows utf8mb3 and utf8mb4
|
||||
$charsetOk = str_starts_with($charset, 'utf8');
|
||||
}
|
||||
return $charsetOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default connection MySQL database charset to utf8.
|
||||
* Should be called only *if* default database connection is actually MySQL
|
||||
*/
|
||||
public function setDatabaseCharsetUtf8()
|
||||
{
|
||||
$connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
$sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
|
||||
$connection->executeStatement($sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
<?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\Service;
|
||||
|
||||
/**
|
||||
* This class provides functionality to build
|
||||
* an ordered list from a set of dependencies.
|
||||
*
|
||||
* We use an adjacency matrix for the dependency graph (DAG)
|
||||
*
|
||||
* Example structure of the DAG is:
|
||||
* A => (A => FALSE, B => TRUE, C => FALSE)
|
||||
* B => (A => FALSE, B => FALSE, C => FALSE)
|
||||
* C => (A => TRUE, B => FALSE, C => FALSE)
|
||||
*
|
||||
* A depends on B, C depends on A, B is independent
|
||||
*/
|
||||
readonly class DependencyOrderingService
|
||||
{
|
||||
/**
|
||||
* Order items by specified dependencies before/after
|
||||
*
|
||||
* The dependencies of an items are specified as:
|
||||
* 'someItemKey' => [
|
||||
* 'before' => ['someItemKeyA', 'someItemKeyB']
|
||||
* 'after' => ['someItemKeyC']
|
||||
* ]
|
||||
*
|
||||
* If your items use different keys for specifying the relations, you can define the appropriate keys
|
||||
* by setting the $beforeKey and $afterKey parameters accordingly.
|
||||
*
|
||||
* @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore'
|
||||
* @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter'
|
||||
*/
|
||||
public function orderByDependencies(array $items, string $beforeKey = 'before', string $afterKey = 'after'): array
|
||||
{
|
||||
$graph = $this->buildDependencyGraph($items, $beforeKey, $afterKey);
|
||||
$sortedItems = [];
|
||||
foreach ($this->calculateOrder($graph) as $id) {
|
||||
if (isset($items[$id])) {
|
||||
$sortedItems[$id] = $items[$id];
|
||||
}
|
||||
}
|
||||
return $sortedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the dependency graph for the given dependencies
|
||||
*
|
||||
* The dependencies have to specified in the following structure:
|
||||
*
|
||||
* ```
|
||||
* $dependencies = [
|
||||
* 'someKey' => [
|
||||
* 'before' => ['someKeyA', 'someKeyB']
|
||||
* 'after' => ['someKeyC']
|
||||
* ]
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* We interpret a dependency like
|
||||
*
|
||||
* ```
|
||||
* 'A' => [
|
||||
* 'before' => ['B'],
|
||||
* 'after' => ['C', 'D']
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* as
|
||||
* - A depends on C
|
||||
* - A depends on D
|
||||
* - B depends on A
|
||||
*
|
||||
* @param array $dependencies
|
||||
* @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore'
|
||||
* @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter'
|
||||
* @return array<array-key, array<array-key, bool>> The dependency graph
|
||||
*/
|
||||
public function buildDependencyGraph(array $dependencies, string $beforeKey = 'before', string $afterKey = 'after'): array
|
||||
{
|
||||
$dependencies = $this->prepareDependencies($dependencies, $beforeKey, $afterKey);
|
||||
|
||||
$identifiers = array_keys($dependencies);
|
||||
sort($identifiers);
|
||||
// $dependencyGraph is the adjacency matrix as two-dimensional array initialized to FALSE (empty graph)
|
||||
/** @var array<array-key, array<array-key, bool>> $dependencyGraph */
|
||||
$dependencyGraph = array_fill_keys($identifiers, array_fill_keys($identifiers, false));
|
||||
|
||||
foreach ($identifiers as $id) {
|
||||
foreach ($dependencies[$id][$beforeKey] as $beforeId) {
|
||||
$dependencyGraph[$beforeId][$id] = true;
|
||||
}
|
||||
foreach ($dependencies[$id][$afterKey] as $afterId) {
|
||||
$dependencyGraph[$id][$afterId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @internal PackageManager
|
||||
// this is a dirty special case for suggestion handling of packages
|
||||
// see \TYPO3\CMS\Core\Package\PackageManager::convertConfigurationForGraph for details
|
||||
// DO NOT use this for any other case
|
||||
foreach ($identifiers as $id) {
|
||||
if (isset($dependencies[$id]['after-resilient'])) {
|
||||
foreach ($dependencies[$id]['after-resilient'] as $afterId) {
|
||||
$reverseDependencies = $this->findPathInGraph($dependencyGraph, $afterId, $id);
|
||||
if (empty($reverseDependencies)) {
|
||||
$dependencyGraph[$id][$afterId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dependencyGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an ordered list for a dependencyGraph
|
||||
*
|
||||
* @param bool[][] $dependencyGraph
|
||||
* @return mixed[] Sorted array of keys of $dependencies
|
||||
*/
|
||||
public function calculateOrder(array $dependencyGraph): array
|
||||
{
|
||||
$rootIds = array_flip($this->findRootIds($dependencyGraph));
|
||||
|
||||
// Add number of dependencies for each root node
|
||||
foreach ($rootIds as $id => &$dependencies) {
|
||||
$dependencies = count(array_filter($dependencyGraph[$id]));
|
||||
}
|
||||
unset($dependencies);
|
||||
|
||||
// This will contain our final result in reverse order,
|
||||
// meaning a result of [A, B, C] equals "A after B after C"
|
||||
$sortedIds = [];
|
||||
|
||||
// Walk through the graph, level by level
|
||||
while (!empty($rootIds)) {
|
||||
ksort($rootIds);
|
||||
// We take those with fewer dependencies first, to have them at the end of the list in the final result.
|
||||
$minimum = PHP_INT_MAX;
|
||||
$currentId = 0;
|
||||
foreach ($rootIds as $id => $count) {
|
||||
if ($count <= $minimum) {
|
||||
$minimum = $count;
|
||||
$currentId = $id;
|
||||
}
|
||||
}
|
||||
unset($rootIds[$currentId]);
|
||||
|
||||
$sortedIds[] = $currentId;
|
||||
|
||||
// Process the dependencies of the current node
|
||||
foreach (array_filter($dependencyGraph[$currentId] ?? []) as $dependingId => $_) {
|
||||
// Remove the edge to this dependency
|
||||
$dependencyGraph[$currentId][$dependingId] = false;
|
||||
if (!$this->getIncomingEdgeCount($dependencyGraph, (string)$dependingId)) {
|
||||
// We found a new root, lets add it to the list
|
||||
$rootIds[$dependingId] = count(array_filter($dependencyGraph[$dependingId] ?? []));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for remaining edges in the graph
|
||||
$cycles = [];
|
||||
array_walk($dependencyGraph, static function ($dependencies, $fromId) use (&$cycles) {
|
||||
array_walk($dependencies, static function ($dependency, $toId) use (&$cycles, $fromId) {
|
||||
if ($dependency) {
|
||||
$cycles[] = $fromId . '->' . $toId;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!empty($cycles)) {
|
||||
throw new \UnexpectedValueException('Your dependencies have cycles. That will not work out. Cycles found: ' . implode(', ', $cycles), 1381960494);
|
||||
}
|
||||
|
||||
// We now built a list of dependencies
|
||||
// Reverse the list to get the correct sorting order
|
||||
return array_reverse($sortedIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of incoming edges in the dependency graph for given identifier
|
||||
*/
|
||||
protected function getIncomingEdgeCount(array $dependencyGraph, string $identifier): int
|
||||
{
|
||||
$incomingEdgeCount = 0;
|
||||
foreach ($dependencyGraph as $dependencies) {
|
||||
if ($dependencies[$identifier] ?? []) {
|
||||
$incomingEdgeCount++;
|
||||
}
|
||||
}
|
||||
return $incomingEdgeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all root nodes of a graph
|
||||
*
|
||||
* Root nodes are those, where nothing else depends on (they can be the last in the loading order).
|
||||
* If there are no dependencies at all, all nodes are root nodes.
|
||||
*
|
||||
* @param bool[][] $dependencyGraph
|
||||
* @return array List of identifiers which are root nodes
|
||||
*/
|
||||
public function findRootIds(array $dependencyGraph): array
|
||||
{
|
||||
// Filter nodes with no incoming edge (aka root nodes)
|
||||
$rootIds = [];
|
||||
foreach ($dependencyGraph as $id => $_) {
|
||||
if (!$this->getIncomingEdgeCount($dependencyGraph, (string)$id)) {
|
||||
$rootIds[] = $id;
|
||||
}
|
||||
}
|
||||
return $rootIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any path in the graph from given start node to destination node
|
||||
*
|
||||
* @param array $graph Directed graph
|
||||
* @param string $from Start node
|
||||
* @param string $to Destination node
|
||||
* @return array Nodes of the found path; empty if no path is found
|
||||
*/
|
||||
protected function findPathInGraph(array $graph, string $from, string $to): array
|
||||
{
|
||||
foreach (array_filter($graph[$from] ?? []) as $node => $_) {
|
||||
if ($node === $to) {
|
||||
return [$from, $to];
|
||||
}
|
||||
$subPath = $this->findPathInGraph($graph, $node, $to);
|
||||
if (!empty($subPath)) {
|
||||
array_unshift($subPath, $from);
|
||||
return $subPath;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare dependencies
|
||||
*
|
||||
* Ensure that all discovered identifiers are added to the dependency list
|
||||
* so we can reliably use the identifiers to build the matrix.
|
||||
* Additionally, fix all invalid or missing before/after arrays
|
||||
*
|
||||
* @param array $dependencies
|
||||
* @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore'
|
||||
* @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter'
|
||||
* @return array Prepared dependencies
|
||||
*/
|
||||
protected function prepareDependencies(array $dependencies, string $beforeKey = 'before', string $afterKey = 'after'): array
|
||||
{
|
||||
$preparedDependencies = [];
|
||||
foreach ($dependencies as $id => $dependency) {
|
||||
foreach ([$beforeKey, $afterKey] as $relation) {
|
||||
if (!isset($dependency[$relation]) || !is_array($dependency[$relation])) {
|
||||
$dependency[$relation] = [];
|
||||
}
|
||||
// add all missing, but referenced identifiers to the $dependency list
|
||||
foreach ($dependency[$relation] as $dependingId) {
|
||||
if (!isset($dependencies[$dependingId]) && !isset($preparedDependencies[$dependingId])) {
|
||||
$preparedDependencies[$dependingId] = [
|
||||
$beforeKey => [],
|
||||
$afterKey => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
$preparedDependencies[$id] = $dependency;
|
||||
}
|
||||
return $preparedDependencies;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Service;
|
||||
|
||||
/**
|
||||
* A service exception
|
||||
*/
|
||||
class Exception extends \TYPO3\CMS\Core\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\Service\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Service\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if the silent configuration updater changed configuration
|
||||
*/
|
||||
class ConfigurationChangedException extends Exception {}
|
||||
@@ -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\Service\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when the SilentConfigurationUpgrade cannot make changes to the settings.php.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SilentConfigurationUpgradeReadonlyException extends Exception
|
||||
{
|
||||
protected $message = 'The SilentConfigurationUpgradeService needs to make changes to the settings.php but that file is read-only. '
|
||||
. 'Please (temporarily) clear the read-only status and open the install tool or run the UpgradeWizards command on CLI (typo3 upgrade:run). '
|
||||
. 'Once the SilentConfigurationUpgrade has been run, you may restrict writing to the settings.php again.';
|
||||
|
||||
public function __construct(int $code = 0, ?\Throwable $throwable = null)
|
||||
{
|
||||
parent::__construct($this->message, $code, $throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
<?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\Service;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Helper functionality for subparts and marker substitution
|
||||
* ###MYMARKER###
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class MarkerBasedTemplateService
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.assets')]
|
||||
protected FrontendInterface $hashCache,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected FrontendInterface $runtimeCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the first subpart encapsulated in the marker, $marker
|
||||
* (possibly present in $content as a HTML comment)
|
||||
*
|
||||
* @param string $content Content with subpart wrapped in fx. "###CONTENT_PART###" inside.
|
||||
* @param string $marker Marker string, eg. "###CONTENT_PART###
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSubpart($content, $marker)
|
||||
{
|
||||
$start = strpos($content, $marker);
|
||||
if ($start === false) {
|
||||
return '';
|
||||
}
|
||||
$start += strlen($marker);
|
||||
$stop = strpos($content, $marker, $start);
|
||||
// Q: What shall get returned if no stop marker is given
|
||||
// Everything till the end or nothing?
|
||||
if ($stop === false) {
|
||||
return '';
|
||||
}
|
||||
$content = substr($content, $start, $stop - $start);
|
||||
$matches = [];
|
||||
if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $content, $matches) === 1) {
|
||||
return $matches[2];
|
||||
}
|
||||
// Resetting $matches
|
||||
$matches = [];
|
||||
if (preg_match('/(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $content, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
// Resetting $matches
|
||||
$matches = [];
|
||||
if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $content, $matches) === 1) {
|
||||
return $matches[2];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes a subpart in $content with the content of $subpartContent.
|
||||
*
|
||||
* @param string $content Content with subpart wrapped in fx. "###CONTENT_PART###" inside.
|
||||
* @param string $marker Marker string, eg. "###CONTENT_PART###
|
||||
* @param string|array $subpartContent If $subpartContent happens to be an array, it's [0] and [1] elements are wrapped around the content of the subpart (fetched by getSubpart())
|
||||
* @param bool $recursive If $recursive is set, the function calls itself with the content set to the remaining part of the content after the second marker. This means that proceeding subparts are ALSO substituted!
|
||||
* @param bool $keepMarker If set, the marker around the subpart is not removed, but kept in the output
|
||||
*
|
||||
* @return string Processed input content
|
||||
*/
|
||||
public function substituteSubpart($content, $marker, $subpartContent, $recursive = true, $keepMarker = false)
|
||||
{
|
||||
$start = strpos($content, $marker);
|
||||
if ($start === false) {
|
||||
return $content;
|
||||
}
|
||||
$startAM = $start + strlen($marker);
|
||||
$stop = strpos($content, $marker, $startAM);
|
||||
if ($stop === false) {
|
||||
return $content;
|
||||
}
|
||||
$stopAM = $stop + strlen($marker);
|
||||
$before = substr($content, 0, $start);
|
||||
$after = substr($content, $stopAM);
|
||||
$between = substr($content, $startAM, $stop - $startAM);
|
||||
if ($recursive) {
|
||||
$after = $this->substituteSubpart($after, $marker, $subpartContent, $recursive, $keepMarker);
|
||||
}
|
||||
if ($keepMarker) {
|
||||
$matches = [];
|
||||
if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
|
||||
$before .= $marker . $matches[1];
|
||||
$between = $matches[2];
|
||||
$after = $matches[3] . $marker . $after;
|
||||
} elseif (preg_match('/^(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
|
||||
$before .= $marker;
|
||||
$between = $matches[1];
|
||||
$after = $matches[2] . $marker . $after;
|
||||
} elseif (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $between, $matches) === 1) {
|
||||
$before .= $marker . $matches[1];
|
||||
$between = $matches[2];
|
||||
$after = $marker . $after;
|
||||
} else {
|
||||
$before .= $marker;
|
||||
$after = $marker . $after;
|
||||
}
|
||||
} else {
|
||||
$matches = [];
|
||||
if (preg_match('/^(.*)\\<\\!\\-\\-[^\\>]*$/s', $before, $matches) === 1) {
|
||||
$before = $matches[1];
|
||||
}
|
||||
if (is_array($subpartContent)) {
|
||||
$matches = [];
|
||||
if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
|
||||
$between = $matches[2];
|
||||
} elseif (preg_match('/^(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
|
||||
$between = $matches[1];
|
||||
} elseif (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $between, $matches) === 1) {
|
||||
$between = $matches[2];
|
||||
}
|
||||
}
|
||||
$matches = [];
|
||||
// resetting $matches
|
||||
if (preg_match('/^[^\\<]*\\-\\-\\>(.*)$/s', $after, $matches) === 1) {
|
||||
$after = $matches[1];
|
||||
}
|
||||
}
|
||||
if (is_array($subpartContent)) {
|
||||
$between = $subpartContent[0] . $between . $subpartContent[1];
|
||||
} else {
|
||||
$between = $subpartContent;
|
||||
}
|
||||
|
||||
return $before . $between . $after;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes multiple subparts at once
|
||||
*
|
||||
* @param string $content The content stream, typically HTML template content.
|
||||
* @param array $subpartsContent The array of key/value pairs being subpart/content values used in the substitution. For each element in this array the function will substitute a subpart in the content stream with the content.
|
||||
*
|
||||
* @return string The processed HTML content string.
|
||||
*/
|
||||
public function substituteSubpartArray($content, array $subpartsContent)
|
||||
{
|
||||
foreach ($subpartsContent as $subpartMarker => $subpartContent) {
|
||||
$content = $this->substituteSubpart($content, $subpartMarker, $subpartContent);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes a marker string in the input content
|
||||
* (by a simple str_replace())
|
||||
*
|
||||
* @param string $content The content stream, typically HTML template content.
|
||||
* @param string $marker The marker string, typically on the form "###[the marker string]###
|
||||
* @param mixed $markContent The content to insert instead of the marker string found.
|
||||
*
|
||||
* @return string The processed HTML content string.
|
||||
* @see substituteSubpart()
|
||||
*/
|
||||
public function substituteMarker($content, $marker, $markContent)
|
||||
{
|
||||
return str_replace($marker, $markContent, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the input $markContentArray array and for each key the marker
|
||||
* by the same name (possibly wrapped and in upper case) will be
|
||||
* substituted with the keys value in the array. This is very useful if you
|
||||
* have a data-record to substitute in some content. In particular when you
|
||||
* use the $wrap and $uppercase values to pre-process the markers. Eg. a
|
||||
* key name like "myfield" could effectively be represented by the marker
|
||||
* "###MYFIELD###" if the wrap value was "###|###" and the $uppercase
|
||||
* boolean TRUE.
|
||||
*
|
||||
* @param string $content The content stream, typically HTML template content.
|
||||
* @param array|null $markContentArray The array of key/value pairs being marker/content values used in the substitution. For each element in this array the function will substitute a marker in the content stream with the content.
|
||||
* @param string $wrap A wrap value - [part 1] | [part 2] - for the markers before substitution
|
||||
* @param bool $uppercase If set, all marker string substitution is done with upper-case markers.
|
||||
* @param bool $deleteUnused If set, all unused marker are deleted.
|
||||
*
|
||||
* @return string The processed output stream
|
||||
* @see substituteMarker()
|
||||
* @see substituteMarkerInObject()
|
||||
*/
|
||||
public function substituteMarkerArray($content, $markContentArray, $wrap = '', $uppercase = false, $deleteUnused = false)
|
||||
{
|
||||
if (is_array($markContentArray)) {
|
||||
$wrapArr = GeneralUtility::trimExplode('|', $wrap);
|
||||
$search = [];
|
||||
$replace = [];
|
||||
foreach ($markContentArray as $marker => $markContent) {
|
||||
if ($uppercase) {
|
||||
// use strtr instead of strtoupper to avoid locale problems with Turkish
|
||||
$marker = strtr($marker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
if (isset($wrapArr[0], $wrapArr[1])) {
|
||||
$marker = $wrapArr[0] . $marker . $wrapArr[1];
|
||||
}
|
||||
$search[] = $marker;
|
||||
$replace[] = $markContent;
|
||||
}
|
||||
$content = str_replace($search, $replace, $content);
|
||||
unset($search, $replace);
|
||||
if ($deleteUnused) {
|
||||
if (empty($wrap)) {
|
||||
$wrapArr = ['###', '###'];
|
||||
}
|
||||
$content = preg_replace('/' . preg_quote($wrapArr[0], '/') . '([A-Z0-9_|\\-]*)' . preg_quote($wrapArr[1], '/') . '/is', '', $content);
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all markers and subparts in a template with the content provided in the structured array.
|
||||
*
|
||||
* The array is built like the template with its markers and subparts. Keys represent the marker name and the values the
|
||||
* content.
|
||||
* If the value is not an array the key will be treated as a single marker.
|
||||
* If the value is an array the key will be treated as a subpart marker.
|
||||
* Repeated subpart contents are of course elements in the array, so every subpart value must contain an array with its
|
||||
* markers.
|
||||
*
|
||||
* ```
|
||||
* $markersAndSubparts = array (
|
||||
* '###SINGLEMARKER1###' => 'value 1',
|
||||
* '###SUBPARTMARKER1###' => array(
|
||||
* 0 => array(
|
||||
* '###SINGLEMARKER2###' => 'value 2',
|
||||
* ),
|
||||
* 1 => array(
|
||||
* '###SINGLEMARKER2###' => 'value 3',
|
||||
* )
|
||||
* ),
|
||||
* '###SUBPARTMARKER2###' => array(
|
||||
* ),
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* Subparts can be nested, so below the 'SINGLEMARKER2' it is possible to have another subpart marker with an array as the
|
||||
* value, which in its turn contains the elements of the sub-subparts.
|
||||
* Empty arrays for Subparts will cause the subtemplate to be cleared.
|
||||
*
|
||||
* @param string $content The content stream, typically HTML template content.
|
||||
* @param array $markersAndSubparts The array of single markers and subpart contents.
|
||||
* @param string $wrap A wrap value - [part1] | [part2] - for the markers before substitution.
|
||||
* @param bool $uppercase If set, all marker string substitution is done with upper-case markers.
|
||||
* @param bool $deleteUnused If set, all unused single markers are deleted.
|
||||
*
|
||||
* @return string The processed output stream
|
||||
*/
|
||||
public function substituteMarkerAndSubpartArrayRecursive($content, array $markersAndSubparts, $wrap = '', $uppercase = false, $deleteUnused = false)
|
||||
{
|
||||
$wraps = GeneralUtility::trimExplode('|', $wrap);
|
||||
$singleItems = [];
|
||||
$compoundItems = [];
|
||||
// Split markers and subparts into separate arrays
|
||||
foreach ($markersAndSubparts as $markerName => $markerContent) {
|
||||
if (is_array($markerContent)) {
|
||||
$compoundItems[] = $markerName;
|
||||
} else {
|
||||
$singleItems[$markerName] = $markerContent;
|
||||
}
|
||||
}
|
||||
$subTemplates = [];
|
||||
$subpartSubstitutes = [];
|
||||
// Build a cache for the sub template
|
||||
foreach ($compoundItems as $subpartMarker) {
|
||||
if ($uppercase) {
|
||||
// Use strtr instead of strtoupper to avoid locale problems with Turkish
|
||||
$subpartMarker = strtr($subpartMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
if (isset($wraps[0], $wraps[1])) {
|
||||
$subpartMarker = $wraps[0] . $subpartMarker . $wraps[1];
|
||||
}
|
||||
$subTemplates[$subpartMarker] = $this->getSubpart($content, $subpartMarker);
|
||||
}
|
||||
// Replace the subpart contents recursively
|
||||
foreach ($compoundItems as $subpartMarker) {
|
||||
$completeMarker = $subpartMarker;
|
||||
if ($uppercase) {
|
||||
// use strtr instead of strtoupper to avoid locale problems with Turkish
|
||||
$completeMarker = strtr($completeMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
if (isset($wraps[0], $wraps[1])) {
|
||||
$completeMarker = $wraps[0] . $completeMarker . $wraps[1];
|
||||
}
|
||||
if (!empty($markersAndSubparts[$subpartMarker])) {
|
||||
$subpartSubstitutes[$completeMarker] = '';
|
||||
foreach ($markersAndSubparts[$subpartMarker] as $partialMarkersAndSubparts) {
|
||||
$subpartSubstitutes[$completeMarker] .= $this->substituteMarkerAndSubpartArrayRecursive(
|
||||
$subTemplates[$completeMarker],
|
||||
$partialMarkersAndSubparts,
|
||||
$wrap,
|
||||
$uppercase,
|
||||
$deleteUnused
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$subpartSubstitutes[$completeMarker] = '';
|
||||
}
|
||||
}
|
||||
// Substitute the single markers and subparts
|
||||
$result = $this->substituteSubpartArray($content, $subpartSubstitutes);
|
||||
$result = $this->substituteMarkerArray($result, $singleItems, $wrap, $uppercase, $deleteUnused);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi substitution function with caching.
|
||||
*
|
||||
* This function should be a one-stop substitution function for working
|
||||
* with HTML-template. It does not substitute by str_replace but by
|
||||
* splitting. This secures that the value inserted does not themselves
|
||||
* contain markers or subparts.
|
||||
*
|
||||
* Note that the "caching" won't cache the content of the substitution,
|
||||
* but only the splitting of the template in various parts. So if you
|
||||
* want only one cache-entry per template, make sure you always pass the
|
||||
* exact same set of marker/subpart keys. Else you will be flooding the
|
||||
* user's cache table.
|
||||
*
|
||||
* This function takes three kinds of substitutions in one:
|
||||
* $markContentArray is a regular marker-array where the 'keys' are
|
||||
* substituted in $content with their values
|
||||
*
|
||||
* $subpartContentArray works exactly like markContentArray only is whole
|
||||
* subparts substituted and not only a single marker.
|
||||
*
|
||||
* $wrappedSubpartContentArray is an array of arrays with 0/1 keys where
|
||||
* the subparts pointed to by the main key is wrapped with the 0/1 value
|
||||
* alternating.
|
||||
*
|
||||
* @param string $content The content stream, typically HTML template content.
|
||||
* @param array $markContentArray Regular marker-array where the 'keys' are substituted in $content with their values
|
||||
* @param array $subpartContentArray Exactly like markContentArray only is whole subparts substituted and not only a single marker.
|
||||
* @param array $wrappedSubpartContentArray An array of arrays with 0/1 keys where the subparts pointed to by the main key is wrapped with the 0/1 value alternating.
|
||||
* @return string The output content stream
|
||||
* @see substituteSubpart()
|
||||
* @see substituteMarker()
|
||||
* @see substituteMarkerInObject()
|
||||
*/
|
||||
public function substituteMarkerArrayCached($content, ?array $markContentArray = null, ?array $subpartContentArray = null, ?array $wrappedSubpartContentArray = null)
|
||||
{
|
||||
// If not arrays then set them
|
||||
if ($markContentArray === null) {
|
||||
// Plain markers
|
||||
$markContentArray = [];
|
||||
}
|
||||
if ($subpartContentArray === null) {
|
||||
// Subparts being directly substituted
|
||||
$subpartContentArray = [];
|
||||
}
|
||||
if ($wrappedSubpartContentArray === null) {
|
||||
// Subparts being wrapped
|
||||
$wrappedSubpartContentArray = [];
|
||||
}
|
||||
// Finding keys and check hash:
|
||||
$sPkeys = array_keys($subpartContentArray);
|
||||
$wPkeys = array_keys($wrappedSubpartContentArray);
|
||||
$keysToReplace = array_merge(array_keys($markContentArray), $sPkeys, $wPkeys);
|
||||
if (empty($keysToReplace)) {
|
||||
return $content;
|
||||
}
|
||||
asort($keysToReplace);
|
||||
$storeKey = md5('substituteMarkerArrayCached_storeKey:' . serialize([$content, $keysToReplace]));
|
||||
$fromCache = $this->runtimeCache->get($storeKey);
|
||||
if ($fromCache) {
|
||||
$storeArr = $fromCache;
|
||||
} else {
|
||||
$storeArrDat = $this->hashCache->get($storeKey);
|
||||
if (is_array($storeArrDat)) {
|
||||
$storeArr = $storeArrDat;
|
||||
// Setting the data in the first level cache
|
||||
$this->runtimeCache->set($storeKey, $storeArr);
|
||||
} else {
|
||||
// Finding subparts and substituting them with the subpart as a marker
|
||||
foreach ($sPkeys as $sPK) {
|
||||
$content = $this->substituteSubpart($content, $sPK, $sPK);
|
||||
}
|
||||
// Finding subparts and wrapping them with markers
|
||||
foreach ($wPkeys as $wPK) {
|
||||
$content = $this->substituteSubpart($content, $wPK, [
|
||||
$wPK,
|
||||
$wPK,
|
||||
]);
|
||||
}
|
||||
|
||||
$storeArr = [];
|
||||
// search all markers in the content
|
||||
$result = preg_match_all('/###([^#](?:[^#]*+|#{1,2}[^#])+)###/', $content, $markersInContent);
|
||||
if ($result !== false && !empty($markersInContent[1])) {
|
||||
$keysToReplaceFlipped = array_flip($keysToReplace);
|
||||
$regexKeys = [];
|
||||
$wrappedKeys = [];
|
||||
// Traverse keys and quote them for reg ex.
|
||||
foreach ($markersInContent[1] as $key) {
|
||||
if (isset($keysToReplaceFlipped['###' . $key . '###'])) {
|
||||
$regexKeys[] = preg_quote($key, '/');
|
||||
$wrappedKeys[] = '###' . $key . '###';
|
||||
}
|
||||
}
|
||||
$regex = '/###(?:' . implode('|', $regexKeys) . ')###/';
|
||||
$storeArr['c'] = preg_split($regex, $content); // contains all content parts around markers
|
||||
$storeArr['k'] = $wrappedKeys; // contains all markers incl. ###
|
||||
// Setting the data inside the second-level cache
|
||||
$this->runtimeCache->set($storeKey, $storeArr);
|
||||
// Storing the cached data permanently
|
||||
$this->hashCache->set($storeKey, $storeArr, ['substMarkArrayCached'], 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($storeArr['k']) && is_array($storeArr['k'])) {
|
||||
// Substitution/Merging:
|
||||
// Merging content types together, resetting
|
||||
$valueArr = array_merge($markContentArray, $subpartContentArray, $wrappedSubpartContentArray);
|
||||
$wSCA_reg = [];
|
||||
$content = '';
|
||||
// Traversing the keyList array and merging the static and dynamic content
|
||||
foreach ($storeArr['k'] as $n => $keyN) {
|
||||
// add content before marker
|
||||
$content .= $storeArr['c'][$n];
|
||||
if (!is_array($valueArr[$keyN])) {
|
||||
// fetch marker replacement from $markContentArray or $subpartContentArray
|
||||
$content .= $valueArr[$keyN];
|
||||
} else {
|
||||
if (!isset($wSCA_reg[$keyN])) {
|
||||
$wSCA_reg[$keyN] = 0;
|
||||
}
|
||||
// fetch marker replacement from $wrappedSubpartContentArray
|
||||
$content .= $valueArr[$keyN][$wSCA_reg[$keyN] % 2];
|
||||
$wSCA_reg[$keyN]++;
|
||||
}
|
||||
}
|
||||
// add remaining content
|
||||
$content .= $storeArr['c'][count($storeArr['k'])];
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute marker array in an array of values
|
||||
*
|
||||
* @param mixed $tree If string, then it just calls substituteMarkerArray. If array(and even multi-dim) then for each key/value pair the marker array will be substituted (by calling this function recursively)
|
||||
* @param array $markContentArray The array of key/value pairs being marker/content values used in the substitution. For each element in this array the function will substitute a marker in the content string/array values.
|
||||
* @return mixed The processed input variable.
|
||||
* @see substituteMarker()
|
||||
*/
|
||||
public function substituteMarkerInObject(&$tree, array $markContentArray)
|
||||
{
|
||||
if (is_array($tree)) {
|
||||
foreach ($tree as $key => $value) {
|
||||
$this->substituteMarkerInObject($tree[$key], $markContentArray);
|
||||
}
|
||||
} else {
|
||||
$tree = $this->substituteMarkerArray($tree, $markContentArray);
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds elements to the input $markContentArray based on the values from
|
||||
* the fields from $fieldList found in $row
|
||||
*
|
||||
* @param array $markContentArray Array with key/values being marker-strings/substitution values.
|
||||
* @param array $row An array with keys found in the $fieldList (typically a record) which values should be moved to the $markContentArray
|
||||
* @param string $fieldList A list of fields from the $row array to add to the $markContentArray array. If empty all fields from $row will be added (unless they are integers)
|
||||
* @param bool $nl2br If set, all values added to $markContentArray will be nl2br()'ed
|
||||
* @param string $prefix Prefix string to the fieldname before it is added as a key in the $markContentArray. Notice that the keys added to the $markContentArray always start and end with "###
|
||||
* @param bool $htmlSpecialCharsValue If set, all values are passed through htmlspecialchars() - RECOMMENDED to avoid most obvious XSS and maintain XHTML compliance.
|
||||
* @param bool $respectXhtml if set, and $nl2br is set, then the new lines are added with <br /> instead of <br>
|
||||
* @return array The modified $markContentArray
|
||||
*/
|
||||
public function fillInMarkerArray(array $markContentArray, array $row, $fieldList = '', $nl2br = true, $prefix = 'FIELD_', $htmlSpecialCharsValue = false, $respectXhtml = false)
|
||||
{
|
||||
if ($fieldList) {
|
||||
$fArr = GeneralUtility::trimExplode(',', $fieldList, true);
|
||||
foreach ($fArr as $field) {
|
||||
$markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($row[$field], $respectXhtml) : $row[$field];
|
||||
}
|
||||
} else {
|
||||
foreach ($row as $field => $value) {
|
||||
if (!MathUtility::canBeInterpretedAsInteger($field)) {
|
||||
if ($htmlSpecialCharsValue) {
|
||||
$value = htmlspecialchars($value);
|
||||
}
|
||||
$markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($value, $respectXhtml) : $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $markContentArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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\Service;
|
||||
|
||||
/**
|
||||
* Class with helper functions for clearing the PHP opcache.
|
||||
* It auto-detects the opcache system and invalidates/resets it.
|
||||
* https://forge.typo3.org/issues/55252
|
||||
* Supported opcaches are: OPcache >= 7.0 (PHP 5.5)
|
||||
*/
|
||||
readonly class OpcodeCacheService
|
||||
{
|
||||
/**
|
||||
* Returns all supported and active opcaches
|
||||
*
|
||||
* @return array Array filled with supported and active opcaches
|
||||
*/
|
||||
public function getAllActive(): array
|
||||
{
|
||||
$supportedCaches = [
|
||||
'OPcache' => [
|
||||
'active' => extension_loaded('Zend OPcache') && ini_get('opcache.enable') === '1',
|
||||
'version' => phpversion('Zend OPcache'),
|
||||
'warning' => self::isClearable() ? false : 'Either opcache_invalidate or opcache_reset are disabled in this installation. Clearing will not work.',
|
||||
'clearCallback' => static function ($fileAbsPath) {
|
||||
if (self::isClearable()) {
|
||||
if ($fileAbsPath !== null) {
|
||||
opcache_invalidate($fileAbsPath);
|
||||
} else {
|
||||
opcache_reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
$activeCaches = [];
|
||||
foreach ($supportedCaches as $opcodeCache => $properties) {
|
||||
if ($properties['active']) {
|
||||
$activeCaches[$opcodeCache] = $properties;
|
||||
}
|
||||
}
|
||||
return $activeCaches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a file from an opcache, if one exists.
|
||||
*
|
||||
* @param string|null $fileAbsPath The file as absolute path to be cleared or NULL to clear completely.
|
||||
*/
|
||||
public function clearAllActive(?string $fileAbsPath = null): void
|
||||
{
|
||||
foreach ($this->getAllActive() as $properties) {
|
||||
$callback = $properties['clearCallback'];
|
||||
$callback($fileAbsPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function isClearable(): bool
|
||||
{
|
||||
$disabled = explode(',', (string)ini_get('disable_functions'));
|
||||
return function_exists('opcache_invalidate')
|
||||
&& function_exists('opcache_reset')
|
||||
&& !(in_array('opcache_invalidate', $disabled, true) || in_array('opcache_reset', $disabled, true));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,377 @@
|
||||
<?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\Service;
|
||||
|
||||
use Symfony\Component\Console\Output\Output;
|
||||
use Symfony\Component\Console\Output\StreamOutput;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Upgrades\ChattyInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\ConfirmableInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\RepeatableInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\RowUpdater\RowUpdaterInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardRegistry;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service class helps to manage upgrade wizards.
|
||||
*
|
||||
* @internal This class is only meant to be used within `EXT:core` and `EXT:install` and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final class UpgradeWizardsService
|
||||
{
|
||||
private StreamOutput $output;
|
||||
|
||||
public function __construct(
|
||||
private readonly UpgradeWizardRegistry $upgradeWizardRegistry,
|
||||
private readonly Registry $registry,
|
||||
) {
|
||||
$fileName = 'php://temp';
|
||||
if (($stream = fopen($fileName, 'wb')) === false) {
|
||||
throw new \RuntimeException('Unable to open stream "' . $fileName . '"', 1598341765);
|
||||
}
|
||||
$this->output = new StreamOutput($stream, Output::VERBOSITY_NORMAL, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array List of wizards marked as done in registry
|
||||
*/
|
||||
public function listOfWizardsDone(): array
|
||||
{
|
||||
$wizardsDoneInRegistry = [];
|
||||
foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $serviceName) {
|
||||
if ($this->registry->get('installUpdate', $serviceName, false)) {
|
||||
$wizardsDoneInRegistry[] = [
|
||||
'class' => $serviceName,
|
||||
'identifier' => $identifier,
|
||||
// @todo fetching the service to get the title should be improved
|
||||
'title' => $this->upgradeWizardRegistry->getUpgradeWizard($identifier)->getTitle(),
|
||||
];
|
||||
}
|
||||
}
|
||||
return $wizardsDoneInRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array List of row updaters marked as done in registry
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function listOfRowUpdatersDone(): array
|
||||
{
|
||||
$rowUpdatersDoneClassNames = $this->registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
$rowUpdatersDone = [];
|
||||
foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
|
||||
// Silently skip non-existing DatabaseRowsUpdateWizards
|
||||
if (!class_exists($rowUpdaterClassName)) {
|
||||
continue;
|
||||
}
|
||||
$rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
|
||||
if (!$rowUpdater instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484152906
|
||||
);
|
||||
}
|
||||
$rowUpdatersDone[] = [
|
||||
'class' => $rowUpdaterClassName,
|
||||
'identifier' => $rowUpdaterClassName,
|
||||
'title' => $rowUpdater->getTitle(),
|
||||
];
|
||||
}
|
||||
return $rowUpdatersDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark one wizard as undone. This can be a "casual" wizard
|
||||
* or a single "row updater".
|
||||
*
|
||||
* @param string $identifier Wizard or RowUpdater identifier
|
||||
* @return bool True if wizard has been marked as undone
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function markWizardUndone(string $identifier): bool
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$aWizardHasBeenMarkedUndone = false;
|
||||
foreach ($this->listOfWizardsDone() as $wizard) {
|
||||
if ($wizard['identifier'] === $identifier) {
|
||||
$aWizardHasBeenMarkedUndone = true;
|
||||
$this->registry->set('installUpdate', $wizard['class'], 0);
|
||||
}
|
||||
}
|
||||
if (!$aWizardHasBeenMarkedUndone) {
|
||||
$rowUpdatersDoneList = $this->listOfRowUpdatersDone();
|
||||
$registryArray = $this->registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
foreach ($rowUpdatersDoneList as $rowUpdater) {
|
||||
if ($rowUpdater['identifier'] === $identifier) {
|
||||
$aWizardHasBeenMarkedUndone = true;
|
||||
foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
|
||||
if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
|
||||
unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $aWizardHasBeenMarkedUndone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of registered upgrade wizards not marked done.
|
||||
*
|
||||
* @return array List of upgrade wizards in correct order with detail information
|
||||
*/
|
||||
public function getUpgradeWizardsList(): array
|
||||
{
|
||||
$wizards = [];
|
||||
foreach (array_keys($this->upgradeWizardRegistry->getUpgradeWizards()) as $identifier) {
|
||||
if ($this->isWizardDone($identifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wizards[] = $this->getWizardInformationByIdentifier($identifier);
|
||||
}
|
||||
return $wizards;
|
||||
}
|
||||
|
||||
public function getWizardInformationByIdentifier(string $identifier): array
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
if (is_subclass_of($identifier, RowUpdaterInterface::class)) {
|
||||
return [
|
||||
'class' => $identifier,
|
||||
'identifier' => $identifier,
|
||||
'title' => $identifier,
|
||||
'shouldRenderWizard' => false,
|
||||
'explanation' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
|
||||
|
||||
if ($wizard instanceof ChattyInterface) {
|
||||
$wizard->setOutput($this->output);
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $wizard::class,
|
||||
'identifier' => $identifier,
|
||||
'title' => $wizard->getTitle(),
|
||||
'shouldRenderWizard' => $wizard->updateNecessary(),
|
||||
'explanation' => $wizard->getDescription(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the "get user input" step of a wizard
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getWizardUserInput(string $identifier): array
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
|
||||
$wizardHtml = '';
|
||||
if ($wizard instanceof ConfirmableInterface) {
|
||||
$markup = [];
|
||||
$radioAttributes = [
|
||||
'type' => 'radio',
|
||||
'class' => 'btn-check',
|
||||
'name' => 'install[values][' . $identifier . '][install]',
|
||||
'value' => '0',
|
||||
];
|
||||
$markup[] = '<div class="panel panel-danger">';
|
||||
$markup[] = ' <div class="panel-heading">';
|
||||
$markup[] = htmlspecialchars($wizard->getConfirmation()->getTitle());
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' <div class="panel-body">';
|
||||
$markup[] = ' <p>' . nl2br(htmlspecialchars($wizard->getConfirmation()->getMessage())) . '</p>';
|
||||
$markup[] = ' <div class="btn-group">';
|
||||
if (!$wizard->getConfirmation()->isRequired()) {
|
||||
$denyChecked = $wizard->getConfirmation()->getDefaultValue() === false ? ' checked' : '';
|
||||
$markup[] = ' <input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . $denyChecked . ' id="upgrade-wizard-deny">';
|
||||
$markup[] = ' <label class="btn btn-default" for="upgrade-wizard-deny">' . $wizard->getConfirmation()->getDeny() . '</label>';
|
||||
}
|
||||
$radioAttributes['value'] = '1';
|
||||
$confirmChecked = $wizard->getConfirmation()->getDefaultValue() === true ? ' checked' : '';
|
||||
$markup[] = ' <input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . $confirmChecked . ' id="upgrade-wizard-confirm">';
|
||||
$markup[] = ' <label class="btn btn-default" for="upgrade-wizard-confirm">' . $wizard->getConfirmation()->getConfirm() . '</label>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = '</div>';
|
||||
$wizardHtml = implode('', $markup);
|
||||
}
|
||||
|
||||
$result = [
|
||||
'identifier' => $identifier,
|
||||
'title' => $wizard->getTitle(),
|
||||
'description' => $wizard->getDescription(),
|
||||
'wizardHtml' => $wizardHtml,
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single update wizard
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function executeWizard(string $identifier, array $values): FlashMessageQueue
|
||||
{
|
||||
$performResult = false;
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
|
||||
|
||||
if ($wizard instanceof ChattyInterface) {
|
||||
$wizard->setOutput($this->output);
|
||||
}
|
||||
$messages = new FlashMessageQueue('install');
|
||||
|
||||
if ($wizard instanceof ConfirmableInterface) {
|
||||
// value is set in request but is empty
|
||||
$isSetButEmpty = isset($values[$identifier]['install']) && empty($values[$identifier]['install']);
|
||||
$checkValue = (int)$values[$identifier]['install'];
|
||||
|
||||
if ($checkValue === 1) {
|
||||
// confirmation = yes, we do the update
|
||||
$performResult = $wizard->executeUpdate();
|
||||
} elseif ($wizard->getConfirmation()->isRequired()) {
|
||||
// confirmation = no, but is required, we do *not* the update and fail
|
||||
$performResult = false;
|
||||
} elseif ($isSetButEmpty) {
|
||||
// confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
|
||||
$this->output->writeln('No changes applied, marking wizard as done.');
|
||||
// confirmation was set to "no"
|
||||
$performResult = true;
|
||||
}
|
||||
} else {
|
||||
// confirmation yes or non-confirmable
|
||||
$performResult = $wizard->executeUpdate();
|
||||
}
|
||||
|
||||
$stream = $this->output->getStream();
|
||||
rewind($stream);
|
||||
if ($performResult) {
|
||||
if (!$wizard instanceof RepeatableInterface) {
|
||||
// mark wizard as done if it's not repeatable and was successful
|
||||
$this->markWizardAsDone($wizard);
|
||||
}
|
||||
$messages->enqueue(
|
||||
new FlashMessage(
|
||||
(string)stream_get_contents($stream),
|
||||
'Update successful'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$messages->enqueue(
|
||||
new FlashMessage(
|
||||
(string)stream_get_contents($stream),
|
||||
'Update failed!',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
)
|
||||
);
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks some wizard as being "seen" so that it not shown again.
|
||||
* Writes the info in system/settings.php
|
||||
*/
|
||||
public function markWizardAsDone(UpgradeWizardInterface $upgradeWizard): void
|
||||
{
|
||||
$this->registry->set('installUpdate', $upgradeWizard::class, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this wizard has been "done" before
|
||||
*
|
||||
* @return bool TRUE if wizard has been done before, FALSE otherwise
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function isWizardDone(string $identifier): bool
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
return (bool)$this->registry->get(
|
||||
'installUpdate',
|
||||
$this->upgradeWizardRegistry->getUpgradeWizard($identifier)::class,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to catch \UnexpectedValueException for backwards compatibility reasons
|
||||
*/
|
||||
public function getUpgradeWizard(string $identifier): ?UpgradeWizardInterface
|
||||
{
|
||||
try {
|
||||
return $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
|
||||
} catch (\UnexpectedValueException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUpgradeWizardIdentifiers(): array
|
||||
{
|
||||
return array_keys($this->upgradeWizardRegistry->getUpgradeWizards());
|
||||
}
|
||||
|
||||
public function getNonRepeatableUpgradeWizards(): array
|
||||
{
|
||||
$nonRepeatableUpgradeWizards = [];
|
||||
foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $updateClassName) {
|
||||
if (!in_array(RepeatableInterface::class, class_implements($updateClassName) ?: [], true)) {
|
||||
$nonRepeatableUpgradeWizards[$identifier] = $updateClassName;
|
||||
}
|
||||
}
|
||||
return $nonRepeatableUpgradeWizards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate identifier exists in upgrade wizard list
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function assertIdentifierIsValid(string $identifier): void
|
||||
{
|
||||
if ($identifier === '') {
|
||||
throw new \RuntimeException('Empty upgrade wizard identifier given', 1650579934);
|
||||
}
|
||||
if (!is_subclass_of($identifier, RowUpdaterInterface::class)
|
||||
&& !$this->upgradeWizardRegistry->hasUpgradeWizard($identifier)
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
'The upgrade wizard identifier "' . $identifier . '" must either be registered as upgrade wizard or it must implement TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface',
|
||||
1650546252
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user