TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+101
View File
@@ -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;
}
}
+21
View File
@@ -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;
}
}
+80
View File
@@ -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
+377
View File
@@ -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
);
}
}
}