TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Migrates backend user language from "default" to "en".
|
||||
*
|
||||
* @since 14.2
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('backendUserLanguageMigration')]
|
||||
final readonly class BackendUserLanguageMigration implements UpgradeWizardInterface
|
||||
{
|
||||
private const string TABLE_NAME = 'be_users';
|
||||
|
||||
public function __construct(
|
||||
private ConnectionPool $connectionPool
|
||||
) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate backend user language from "default" to "en"';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
$count = $this->getRecordsToUpdateCount();
|
||||
return sprintf(
|
||||
'The language key "default" for backend users has been replaced with "en". '
|
||||
. 'This wizard migrates %d backend user record(s) to use "en" instead of "default".',
|
||||
$count
|
||||
);
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return $this->getRecordsToUpdateCount() > 0;
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
|
||||
$connection->update(
|
||||
self::TABLE_NAME,
|
||||
['lang' => 'en'],
|
||||
['lang' => 'default']
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getRecordsToUpdateCount(): int
|
||||
{
|
||||
$queryBuilder = $this->getPreparedQueryBuilder();
|
||||
return (int)$queryBuilder
|
||||
->count('uid')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'lang',
|
||||
$queryBuilder->createNamedParameter('default')
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
}
|
||||
|
||||
private function getPreparedQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Is this upgradeWizard chatty aka does it need to output things?
|
||||
*/
|
||||
interface ChattyInterface
|
||||
{
|
||||
/**
|
||||
* Setter injection for output into upgrade wizards
|
||||
*/
|
||||
public function setOutput(OutputInterface $output): void;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
/**
|
||||
* Use if upgrade wizard needs confirmation
|
||||
*/
|
||||
interface ConfirmableInterface
|
||||
{
|
||||
/**
|
||||
* Return a confirmation message instance
|
||||
*/
|
||||
public function getConfirmation(): Confirmation;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
readonly class Confirmation
|
||||
{
|
||||
public function __construct(
|
||||
protected string $title,
|
||||
protected string $message,
|
||||
protected bool $defaultValue = false,
|
||||
protected string $confirm = 'Yes, execute',
|
||||
protected string $deny = 'No, do not execute',
|
||||
protected bool $required = false
|
||||
) {}
|
||||
|
||||
public function getConfirm(): string
|
||||
{
|
||||
return $this->confirm;
|
||||
}
|
||||
|
||||
public function getDeny(): string
|
||||
{
|
||||
return $this->deny;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getDefaultValue(): bool
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Upgrades\RowUpdater\RowUpdaterInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This is a generic updater to migrate content of TCA rows.
|
||||
*
|
||||
* Multiple classes implementing interface "RowUpdaterInterface" can be
|
||||
* registered here, each for a specific update purpose.
|
||||
*
|
||||
* The updater fetches each row of all TCA registered tables and
|
||||
* visits the client classes who may modify the row content.
|
||||
*
|
||||
* The updater remembers for each class if it run through, so the updater
|
||||
* will be shown again if a new updater class is registered that has not
|
||||
* been run yet.
|
||||
*
|
||||
* A start position pointer is stored in the registry that is updated during
|
||||
* the run process, so if for instance the PHP process runs into a timeout,
|
||||
* the job can restart at the position it stopped.
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('databaseRowsUpdateWizard')]
|
||||
class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
|
||||
{
|
||||
/**
|
||||
* @var array Single classes that may update rows
|
||||
* @todo No RowUpdater remaining allowing us to move the registration to attribute/interface based locator.
|
||||
*/
|
||||
protected $rowUpdater = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAvailableRowUpdater(): array
|
||||
{
|
||||
return $this->rowUpdater;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Title of this updater
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Execute database migrations on single rows';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Longer description of this updater
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
|
||||
$description = 'Row updaters that have not been executed:';
|
||||
foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
|
||||
$rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
|
||||
if (!$rowUpdater instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484066647
|
||||
);
|
||||
}
|
||||
$description .= LF . $rowUpdater->getTitle();
|
||||
}
|
||||
return $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if at least one row updater is not marked done
|
||||
*/
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return !empty($this->getRowUpdatersToExecute());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] All new fields and tables must exist
|
||||
*/
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the configuration update.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\ConnectionException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
|
||||
// If rows from the target table that is updated and the sys_registry table are on the
|
||||
// same connection, the row update statement and sys_registry position update will be
|
||||
// handled in a transaction to have an atomic operation in case of errors during execution.
|
||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
|
||||
|
||||
/** @var RowUpdaterInterface[] $rowUpdaterInstances */
|
||||
$rowUpdaterInstances = [];
|
||||
// Single row updater instances are created only once for this method giving
|
||||
// them a chance to set up local properties during hasPotentialUpdateForTable()
|
||||
// and using that in updateTableRow()
|
||||
foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
|
||||
$rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
|
||||
if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484071612
|
||||
);
|
||||
}
|
||||
$rowUpdaterInstances[] = $rowUpdaterInstance;
|
||||
}
|
||||
|
||||
// Scope of the row updater is to update all rows that have TCA,
|
||||
// our list of tables is just the list of loaded TCA tables.
|
||||
/** @var string[] $listOfAllTables */
|
||||
$listOfAllTables = array_keys($GLOBALS['TCA']);
|
||||
|
||||
// In case the PHP ended for whatever reason, fetch the last position from registry
|
||||
// and throw away all tables before that start point.
|
||||
sort($listOfAllTables);
|
||||
reset($listOfAllTables);
|
||||
$firstTable = current($listOfAllTables) ?: '';
|
||||
$startPosition = $this->getStartPosition($firstTable);
|
||||
foreach ($listOfAllTables as $key => $table) {
|
||||
if ($table === $startPosition['table']) {
|
||||
break;
|
||||
}
|
||||
unset($listOfAllTables[$key]);
|
||||
}
|
||||
|
||||
// Ask each row updater if it potentially has field updates for rows of a table
|
||||
$tableToUpdaterList = [];
|
||||
foreach ($listOfAllTables as $table) {
|
||||
foreach ($rowUpdaterInstances as $updater) {
|
||||
if ($updater->hasPotentialUpdateForTable($table)) {
|
||||
$tableToUpdaterList[$table] ??= [];
|
||||
$tableToUpdaterList[$table][] = $updater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all rows of all tables that have potential row updaters attached,
|
||||
// feed each single row to each updater and finally update each row in database if
|
||||
// a row updater changed a fields
|
||||
foreach ($tableToUpdaterList as $table => $updaters) {
|
||||
/** @var RowUpdaterInterface[] $updaters */
|
||||
$connectionForTable = $connectionPool->getConnectionForTable($table);
|
||||
$queryBuilder = $connectionPool->getQueryBuilderForTable($table);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->select('*')
|
||||
->from($table)
|
||||
->orderBy('uid');
|
||||
if ($table === $startPosition['table']) {
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
|
||||
);
|
||||
}
|
||||
$statement = $queryBuilder->executeQuery();
|
||||
$rowCountWithoutUpdate = 0;
|
||||
while ($row = $statement->fetchAssociative()) {
|
||||
$rowBefore = $row;
|
||||
foreach ($updaters as $updater) {
|
||||
$row = $updater->updateTableRow($table, $row);
|
||||
}
|
||||
$updatedFields = array_diff_assoc($row, $rowBefore);
|
||||
if (empty($updatedFields)) {
|
||||
// Updaters changed no field of that row
|
||||
$rowCountWithoutUpdate++;
|
||||
if ($rowCountWithoutUpdate >= 200) {
|
||||
// Update startPosition if there were many rows without data change
|
||||
$startPosition = [
|
||||
'table' => $table,
|
||||
'uid' => $row['uid'],
|
||||
];
|
||||
$registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
|
||||
$rowCountWithoutUpdate = 0;
|
||||
}
|
||||
} else {
|
||||
$rowCountWithoutUpdate = 0;
|
||||
$startPosition = [
|
||||
'table' => $table,
|
||||
'uid' => $rowBefore['uid'],
|
||||
];
|
||||
if ($connectionForSysRegistry === $connectionForTable) {
|
||||
// Target table and sys_registry table are on the same connection, use a transaction
|
||||
$connectionForTable->beginTransaction();
|
||||
try {
|
||||
$this->updateOrDeleteRow(
|
||||
$connectionForTable,
|
||||
$connectionForTable,
|
||||
$table,
|
||||
(int)$rowBefore['uid'],
|
||||
$updatedFields,
|
||||
$startPosition
|
||||
);
|
||||
$connectionForTable->commit();
|
||||
} catch (\Exception $up) {
|
||||
$connectionForTable->rollBack();
|
||||
throw $up;
|
||||
}
|
||||
} else {
|
||||
// Different connections for table and sys_registry.
|
||||
// So, execute two distinct queries and hope for the best.
|
||||
$this->updateOrDeleteRow(
|
||||
$connectionForTable,
|
||||
$connectionForSysRegistry,
|
||||
$table,
|
||||
(int)$rowBefore['uid'],
|
||||
$updatedFields,
|
||||
$startPosition
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ready with updates, remove position information from sys_registry
|
||||
$registry->remove('installUpdateRows', 'rowUpdatePosition');
|
||||
// Mark row updaters that were executed as done
|
||||
foreach ($rowUpdaterInstances as $updater) {
|
||||
$this->setRowUpdaterExecuted($updater);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of class names that are not yet marked as done.
|
||||
*
|
||||
* @return array Class names
|
||||
*/
|
||||
protected function getRowUpdatersToExecute(): array
|
||||
{
|
||||
$doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
return array_diff($this->rowUpdater, $doneRowUpdater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single updater as done
|
||||
*/
|
||||
protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
$doneRowUpdater[] = get_class($updater);
|
||||
$registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with table / uid combination that specifies the start position the
|
||||
* update row process should start with.
|
||||
*
|
||||
* @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
|
||||
* @return array New start position
|
||||
*/
|
||||
protected function getStartPosition(string $firstTable): array
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
|
||||
if (empty($startPosition)) {
|
||||
$startPosition = [
|
||||
'table' => $firstTable,
|
||||
'uid' => 0,
|
||||
];
|
||||
$registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
|
||||
}
|
||||
return $startPosition;
|
||||
}
|
||||
|
||||
protected function updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
|
||||
{
|
||||
$deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
|
||||
if ($deleteField === null && isset($updatedFields['deleted']) && $updatedFields['deleted'] === 1) {
|
||||
$connectionForTable->delete(
|
||||
$table,
|
||||
[
|
||||
'uid' => $uid,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$connectionForTable->update(
|
||||
$table,
|
||||
$updatedFields,
|
||||
[
|
||||
'uid' => $uid,
|
||||
]
|
||||
);
|
||||
}
|
||||
$connectionForSysRegistry->update(
|
||||
'sys_registry',
|
||||
[
|
||||
'entry_value' => serialize($startPosition),
|
||||
],
|
||||
[
|
||||
'entry_namespace' => 'installUpdateRows',
|
||||
'entry_key' => 'rowUpdatePosition',
|
||||
],
|
||||
[
|
||||
// Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
|
||||
'entry_value' => Connection::PARAM_LOB,
|
||||
'entry_namespace' => Connection::PARAM_STR,
|
||||
'entry_key' => Connection::PARAM_STR,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Service\DatabaseUpgradeWizardsService;
|
||||
|
||||
/**
|
||||
* Prerequisite for upgrade wizards to ensure the database is up-to-date
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final class DatabaseUpdatedPrerequisite implements PrerequisiteInterface, ChattyInterface
|
||||
{
|
||||
private OutputInterface $output;
|
||||
|
||||
public function __construct(
|
||||
private readonly DatabaseUpgradeWizardsService $databaseUpgradeWizardsService,
|
||||
) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Database Up-to-Date';
|
||||
}
|
||||
|
||||
public function ensure(): bool
|
||||
{
|
||||
$adds = $this->databaseUpgradeWizardsService->getBlockingDatabaseAdds();
|
||||
// Nothing to add, early return
|
||||
if ($adds === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->output->writeln('Performing ' . count($adds) . ' database operations.');
|
||||
// remove potentially empty error messages
|
||||
$errorMessages = array_filter($this->databaseUpgradeWizardsService->addMissingTablesAndFields());
|
||||
|
||||
return $errorMessages === [];
|
||||
}
|
||||
|
||||
public function isFulfilled(): bool
|
||||
{
|
||||
$adds = $this->databaseUpgradeWizardsService->getBlockingDatabaseAdds();
|
||||
return count($adds) === 0;
|
||||
}
|
||||
|
||||
public function setOutput(OutputInterface $output): void
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Upgrades\DatabaseUpdatedPrerequisite as CoreDatabaseUpdatedPrerequisite;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface as CoreUpgradeWizardInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Migrate extension data import registry keys from path-based to extension key-based format
|
||||
*
|
||||
* This wizard updates sys_registry entries that were stored with file paths to use
|
||||
* extension keys as prefix instead, making them independent of file path changes.
|
||||
*
|
||||
* @since 14.0
|
||||
* @internal This class is only meant to be used within `EXT:core` and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('migrateExtensionDataImportRegistryKeys')]
|
||||
readonly class MigrateExtensionDataImportRegistryKeysUpdate implements CoreUpgradeWizardInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate extension data import registry keys';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Updates sys_registry entries for extension data imports from path-based keys to extension key-based keys. '
|
||||
. 'This makes the registry entries independent of file path changes and follows the new format introduced '
|
||||
. 'in the extension data import system.';
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('sys_registry');
|
||||
|
||||
// Get all extensionDataImport registry entries
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder
|
||||
->select('entry_key', 'entry_value')
|
||||
->from('sys_registry')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'entry_namespace',
|
||||
$queryBuilder->createNamedParameter('extensionDataImport')
|
||||
)
|
||||
)
|
||||
->orderBy('uid')
|
||||
->executeQuery();
|
||||
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$oldKey = $row['entry_key'];
|
||||
$value = $row['entry_value'];
|
||||
|
||||
if ($oldKey === '') {
|
||||
continue;
|
||||
}
|
||||
// Skip entries that already use the new format (contain ":")
|
||||
if (str_contains($oldKey, ':') && !str_starts_with($oldKey, 'EXT:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newKey = $this->convertPathToExtensionKey($oldKey);
|
||||
if ($newKey !== null && $newKey !== $oldKey) {
|
||||
// Set the new key with the same value
|
||||
$registry->set('extensionDataImport', $newKey, unserialize($value, ['allowed_classes' => false]));
|
||||
// Remove the old key
|
||||
$registry->remove('extensionDataImport', $oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('sys_registry');
|
||||
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$count = $queryBuilder
|
||||
->count('*')
|
||||
->from('sys_registry')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'entry_namespace',
|
||||
$queryBuilder->createNamedParameter('extensionDataImport')
|
||||
),
|
||||
$queryBuilder->expr()->notLike(
|
||||
'entry_key',
|
||||
$queryBuilder->createNamedParameter('%:%')
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
CoreDatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a path-based registry key to an extension key-based format
|
||||
*/
|
||||
protected function convertPathToExtensionKey(string $pathKey): ?string
|
||||
{
|
||||
// Pattern 1: typo3conf/ext/... or typo3/sysext/...
|
||||
if (preg_match('#^(?:typo3conf/ext|typo3/sysext)/([^/]+)/(.+)$#', $pathKey, $matches)) {
|
||||
$extensionKey = $matches[1];
|
||||
$filePart = $matches[2];
|
||||
return $extensionKey . ':' . $filePart;
|
||||
}
|
||||
|
||||
// Pattern 2: EXT:extension_name/...
|
||||
if (preg_match('#^EXT:([^/]+)/(.+)$#', $pathKey, $matches)) {
|
||||
$extensionKey = $matches[1];
|
||||
$filePart = $matches[2];
|
||||
return $extensionKey . ':' . $filePart;
|
||||
}
|
||||
|
||||
// Pattern 3: composer-based mode (vendor/...), in this case we take the last path part before
|
||||
// "Initialisation/Files" or "ext_tables_static+adt.sql"
|
||||
$pathSegments = GeneralUtility::revExplode('/', $pathKey, 2);
|
||||
if (count($pathSegments) !== 2) {
|
||||
return null;
|
||||
}
|
||||
if ($pathSegments[1] === 'Files' || $pathSegments[1] === 'dataImported') {
|
||||
$pathSegments[0] = GeneralUtility::revExplode('/', $pathSegments[0], 2)[0];
|
||||
$pathSegments[1] = 'Initialisation/' . $pathSegments[1];
|
||||
}
|
||||
if (preg_match('#^([^/]+)/(.+)$#', $pathSegments[0], $matches) && in_array($pathSegments[1], ['Initialisation/Files', 'Initialisation/dataImported', 'ext_tables_static+adt.sql'])) {
|
||||
$extensionKey = (GeneralUtility::revExplode('/', $matches[0], 2)[1] ?? '');
|
||||
$extensionKey = str_replace('-', '_', $extensionKey); // Normalize dashes to underscores
|
||||
if (str_starts_with($extensionKey, 'cms_')) {
|
||||
$extensionKey = substr($extensionKey, strlen('cms_'));
|
||||
}
|
||||
$filePart = $pathSegments[1];
|
||||
return $extensionKey . ':' . $filePart;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Migrates `pages.url` field values for `pages.doktype = 3 (Link)` to
|
||||
* TypoLink notation suitable for the `pages.link` field and displays
|
||||
* failed pages uid.
|
||||
*
|
||||
* @since 14.0
|
||||
* @internal This class is only meant to be used within EXT:core and is not part of the TYPO3 Core API.
|
||||
* @todo Remove in 16.0 as breaking change.
|
||||
*/
|
||||
#[UpgradeWizard('pageDoktypeLinkMigration')]
|
||||
class PageDoktypeLinkMigration implements UpgradeWizardInterface, ChattyInterface
|
||||
{
|
||||
protected ?OutputInterface $output = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
public function setOutput(OutputInterface $output): void
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate field "pages.url" to "pages.link" for pages of type Link.';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrates "pages.url" to "pages.link", preserving former behaviour of the page type "External Link".';
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
$tableSchema = $this->getPagesTableSchema();
|
||||
return $tableSchema !== null
|
||||
&& $tableSchema->hasColumn('url')
|
||||
&& $tableSchema->hasColumn('target')
|
||||
&& $tableSchema->hasColumn('link')
|
||||
&& $this->hasRecordsToUpdate();
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
if (!$this->updateNecessary()) {
|
||||
return true;
|
||||
}
|
||||
$connection = $this->connectionPool->getConnectionForTable('pages');
|
||||
$migratableItemsQueryBuilder = $connection->createQueryBuilder();
|
||||
$result = $migratableItemsQueryBuilder
|
||||
->select('uid', 'url', 'target', 'deleted')
|
||||
->from('pages')
|
||||
->where(
|
||||
$migratableItemsQueryBuilder->expr()->and(
|
||||
$migratableItemsQueryBuilder->expr()->eq('link', $migratableItemsQueryBuilder->createNamedParameter('')),
|
||||
$migratableItemsQueryBuilder->expr()->neq('url', $migratableItemsQueryBuilder->createNamedParameter('')),
|
||||
$migratableItemsQueryBuilder->expr()->eq('doktype', $migratableItemsQueryBuilder->createNamedParameter(PageRepository::DOKTYPE_LINK, ParameterType::INTEGER)),
|
||||
),
|
||||
)
|
||||
->executeQuery();
|
||||
$scheme = $GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultScheme'] ?? 'http';
|
||||
$failedMigrations = [];
|
||||
try {
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$url = $this->migrateExternalUrlToTypoLink($row['url'], $row['target'], $scheme);
|
||||
if ($url === '') {
|
||||
if ($row['deleted'] !== 1) {
|
||||
$failedMigrations[] = $row['uid'];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$updateQueryBuilder = $connection->createQueryBuilder();
|
||||
$updateQueryBuilder->getRestrictions()->removeAll();
|
||||
$expression = $updateQueryBuilder->expr();
|
||||
$updateQueryBuilder->update('pages')
|
||||
->set('link', $url)
|
||||
// Empty url field to flag already migrated record.
|
||||
->set('url', '')
|
||||
// Empty target field
|
||||
->set('target', '')
|
||||
->where($expression->eq('uid', ($row['uid'])))
|
||||
->executeStatement();
|
||||
}
|
||||
} finally {
|
||||
// Ensure to buffer is freed in case any exception occurred to avoid follow issues.
|
||||
$result->free();
|
||||
}
|
||||
if ($failedMigrations !== []) {
|
||||
$this->output?->writeln(sprintf(
|
||||
'The following pages of type "Link" could not be migrated: %s',
|
||||
implode(', ', $failedMigrations),
|
||||
));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasRecordsToUpdate(): bool
|
||||
{
|
||||
$connection = $this->connectionPool->getConnectionForTable('pages');
|
||||
$migratableItemsQueryBuilder = $connection->createQueryBuilder();
|
||||
return (bool)$migratableItemsQueryBuilder
|
||||
->count('*')
|
||||
->from('pages')
|
||||
->where(
|
||||
$migratableItemsQueryBuilder->expr()->and(
|
||||
$migratableItemsQueryBuilder->expr()->eq('link', $migratableItemsQueryBuilder->createNamedParameter('')),
|
||||
$migratableItemsQueryBuilder->expr()->neq('url', $migratableItemsQueryBuilder->createNamedParameter('')),
|
||||
$migratableItemsQueryBuilder->expr()->eq('doktype', $migratableItemsQueryBuilder->createNamedParameter(PageRepository::DOKTYPE_LINK, ParameterType::INTEGER)),
|
||||
),
|
||||
)->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
protected function migrateExternalUrlToTypoLink(string $urlString, string $target, string $sitePrefix): string
|
||||
{
|
||||
$urlTargetSuffix = $target !== '' ? ' ' . $target : '';
|
||||
if ($urlString === '') {
|
||||
return '';
|
||||
}
|
||||
// Old ExternalUrl field allowed to simply define query parameters appended to the current page,
|
||||
// which TypoScript still supports. Simply keep/copy the option.
|
||||
if (str_starts_with($urlString, '?')) {
|
||||
return $urlString . $urlTargetSuffix;
|
||||
}
|
||||
$parsedUrl = parse_url($urlString);
|
||||
if (str_starts_with($urlString, 'mailto:')) {
|
||||
if (GeneralUtility::validEmail(substr($urlString, 7))) {
|
||||
// valid mailto: URL
|
||||
return $urlString . $urlTargetSuffix;
|
||||
}
|
||||
return '';
|
||||
|
||||
}
|
||||
if (!($parsedUrl['scheme'] ?? false)) {
|
||||
if (GeneralUtility::validEmail($urlString)) {
|
||||
// Email Address without mailto prefix
|
||||
return 'mailto:' . $urlString;
|
||||
}
|
||||
if (str_starts_with($urlString, '/')) {
|
||||
// Relative Url on site base
|
||||
return $urlString . $urlTargetSuffix;
|
||||
}
|
||||
// domain without https prefix
|
||||
$urlString = $sitePrefix . '://' . $urlString;
|
||||
}
|
||||
if (!GeneralUtility::isValidUrl($urlString)) {
|
||||
return '';
|
||||
}
|
||||
// Reject any non-http(s) schemes (mailto: already handled above)
|
||||
// This rejects javascript: ftp: and other potentially harmful prefixes
|
||||
$scheme = strtolower((string)(parse_url($urlString, PHP_URL_SCHEME) ?? ''));
|
||||
if ($scheme !== '' && $scheme !== 'http' && $scheme !== 'https') {
|
||||
return '';
|
||||
}
|
||||
return $urlString . $urlTargetSuffix;
|
||||
}
|
||||
|
||||
protected function getPagesTableSchema(): ?Table
|
||||
{
|
||||
$schemaManager = $this->connectionPool->getConnectionForTable('pages')->createSchemaManager();
|
||||
if (!$schemaManager->tablesExist(['pages'])) {
|
||||
return null;
|
||||
}
|
||||
return $schemaManager->introspectTable('pages');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Collection of prerequisites used internally in upgrade wizard commands.
|
||||
*
|
||||
* @internal for use in upgrade wizard command only and not part of public API.
|
||||
*/
|
||||
final class PrerequisiteCollection implements \IteratorAggregate
|
||||
{
|
||||
private \ArrayObject $prerequisites;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->prerequisites = new \ArrayObject();
|
||||
}
|
||||
|
||||
public function add(string $prerequisiteClass): void
|
||||
{
|
||||
if (
|
||||
!($this->prerequisites[$prerequisiteClass] ?? false)
|
||||
&& is_a($prerequisiteClass, PrerequisiteInterface::class, true)
|
||||
) {
|
||||
$this->prerequisites[$prerequisiteClass] = GeneralUtility::makeInstance(
|
||||
$prerequisiteClass
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
return $this->prerequisites;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
/**
|
||||
* UpgradeWizard Prerequisites
|
||||
*/
|
||||
interface PrerequisiteInterface
|
||||
{
|
||||
/**
|
||||
* Get speaking name of this prerequisite
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Ensure this prerequisite is fulfilled
|
||||
*
|
||||
* Gets called if "isFulfilled" returns false
|
||||
* and should ensure the prerequisite
|
||||
*
|
||||
* Returns true on success, false on error
|
||||
*
|
||||
* @see isFulfilled
|
||||
*/
|
||||
public function ensure(): bool;
|
||||
|
||||
/**
|
||||
* Is this prerequisite met?
|
||||
*
|
||||
* Checks whether this prerequisite is fulfilled. If it is not,
|
||||
* ensure should be called to fulfill it.
|
||||
*
|
||||
* @see ensure
|
||||
*/
|
||||
public function isFulfilled(): bool;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Upgrades;
|
||||
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Command\ProgressListener\ReferenceIndexProgressListener;
|
||||
use TYPO3\CMS\Core\Database\ReferenceIndex;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* ReferenceIndex Prerequisite
|
||||
*
|
||||
* Defines that the reference index needs to be up-to-date before an upgrade wizard may be run
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final class ReferenceIndexUpdatedPrerequisite implements PrerequisiteInterface, ChattyInterface
|
||||
{
|
||||
private OutputInterface $output;
|
||||
|
||||
public function __construct(
|
||||
private readonly ReferenceIndex $referenceIndex,
|
||||
) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Reference Index Up-to-Date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reference index
|
||||
*/
|
||||
public function ensure(): bool
|
||||
{
|
||||
$this->output->writeln('Reference Index is being updated');
|
||||
$progressListener = GeneralUtility::makeInstance(ReferenceIndexProgressListener::class);
|
||||
$progressListener->initialize(new SymfonyStyle(new ArrayInput([]), $this->output));
|
||||
$result = $this->referenceIndex->updateIndex(false, $progressListener);
|
||||
return empty($result['errors']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there are reference index updates to be done
|
||||
*/
|
||||
public function isFulfilled(): bool
|
||||
{
|
||||
$result = $this->referenceIndex->updateIndex(true);
|
||||
return empty($result['errors']);
|
||||
}
|
||||
|
||||
public function setOutput(OutputInterface $output): void
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
}
|
||||
@@ -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\Upgrades;
|
||||
|
||||
/**
|
||||
* Use if wizard may be run multiple times (and should not be disabled after one run)
|
||||
*
|
||||
* Semantic/Marker interface only
|
||||
*/
|
||||
interface RepeatableInterface {}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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\Upgrades\RowUpdater;
|
||||
|
||||
/**
|
||||
* Interface each single row updater must implement.
|
||||
*/
|
||||
interface RowUpdaterInterface
|
||||
{
|
||||
/**
|
||||
* Get a description of this single row updater
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Return true if this row updater may have updates for given table rows.
|
||||
*
|
||||
* @param string $tableName Given table
|
||||
*/
|
||||
public function hasPotentialUpdateForTable(string $tableName): bool;
|
||||
|
||||
/**
|
||||
* Update a single row from a table.
|
||||
*
|
||||
* @param string $tableName Given table
|
||||
* @param array $row Given row
|
||||
* @return array Potentially modified row
|
||||
*/
|
||||
public function updateTableRow(string $tableName, array $row): array;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
/**
|
||||
* Interface UpgradeWizardInterface
|
||||
*/
|
||||
interface UpgradeWizardInterface
|
||||
{
|
||||
/**
|
||||
* Return the speaking name of this wizard
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Return the description for this wizard
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Execute the update
|
||||
*
|
||||
* Called when a wizard reports that an update is necessary
|
||||
*/
|
||||
public function executeUpdate(): bool;
|
||||
|
||||
/**
|
||||
* Is an update necessary?
|
||||
*
|
||||
* Is used to determine whether a wizard needs to be run.
|
||||
* Check if data for migration exists.
|
||||
*/
|
||||
public function updateNecessary(): bool;
|
||||
|
||||
/**
|
||||
* Returns an array of class names of Prerequisite classes
|
||||
*
|
||||
* This way a wizard can define dependencies like "database up-to-date" or
|
||||
* "reference index updated"
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPrerequisites(): array;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
/**
|
||||
* Registry for upgrade wizards. The registry receives all services, tagged with "install.upgradewizard".
|
||||
* The tagging of upgrade wizards is automatically done based on the PHP Attribute UpgradeWizard.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class UpgradeWizardRegistry
|
||||
{
|
||||
public function __construct(
|
||||
#[AutowireLocator(services: 'install.upgradewizard', indexAttribute: 'identifier')]
|
||||
private ServiceLocator $upgradeWizards
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Whether a registered upgrade wizard exists for the given identifier
|
||||
*/
|
||||
public function hasUpgradeWizard(string $identifier): bool
|
||||
{
|
||||
return $this->upgradeWizards->has($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered upgrade wizard by identifier
|
||||
*/
|
||||
public function getUpgradeWizard(string $identifier): UpgradeWizardInterface
|
||||
{
|
||||
if (!$this->hasUpgradeWizard($identifier)) {
|
||||
throw new \UnexpectedValueException('Upgrade wizard with identifier ' . $identifier . ' is not registered.', 1673964964);
|
||||
}
|
||||
|
||||
return $this->upgradeWizards->get($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered upgrade wizards
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUpgradeWizards(): array
|
||||
{
|
||||
return $this->upgradeWizards->getProvidedServices();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?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\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @since 14.0
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('userPermissionsForRenamedModulesMigration')]
|
||||
class UserPermissionsForRenamedModulesMigration implements UpgradeWizardInterface
|
||||
{
|
||||
protected array $tables = [
|
||||
'be_groups' => 'groupMods',
|
||||
'be_users' => 'userMods',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array <string, string> an array with the old module identifier as key and the new one as value
|
||||
*/
|
||||
protected array $moduleRenaming = [
|
||||
'web_list' => 'records',
|
||||
'web_info' => 'content_status',
|
||||
'workspaces_admin' => 'workspaces_publish',
|
||||
'site_redirects' => 'redirects',
|
||||
'web_linkvalidator' => 'linkvalidator_checklinks',
|
||||
];
|
||||
|
||||
/**
|
||||
* Modules that require a parent module to be accessible.
|
||||
*
|
||||
* @var array<string, string> Key: the new module identifier, Value: required parent module
|
||||
*/
|
||||
protected array $requiredParentModules = [
|
||||
'redirects' => 'link_management',
|
||||
'linkvalidator_checklinks' => 'link_management',
|
||||
];
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate module permissions';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrate permissions for renamed modules in user and group module permissions. '
|
||||
. 'Also adds required parent modules when a module has been moved to a new location in the module hierarchy.';
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return $this->migrate(true);
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
return $this->migrate(false);
|
||||
}
|
||||
|
||||
private function migrate(bool $dryRun): bool
|
||||
{
|
||||
$migrated = false;
|
||||
foreach ($this->tables as $table => $field) {
|
||||
$queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable($table);
|
||||
$connection = $this->getConnectionPool()->getConnectionForTable($table);
|
||||
$queryBuilder->select('uid', $field)
|
||||
->from($table)
|
||||
->executeQuery();
|
||||
|
||||
foreach ($queryBuilder->fetchAllAssociative() as $record) {
|
||||
$originalModules = (string)($record[$field] ?? '');
|
||||
$modules = explode(',', $originalModules);
|
||||
$parentModulesToAdd = [];
|
||||
$updatedModules = array_map(function ($module) use (&$parentModulesToAdd) {
|
||||
$trimmedModule = trim($module);
|
||||
if (isset($this->moduleRenaming[$trimmedModule])) {
|
||||
$newModule = $this->moduleRenaming[$trimmedModule];
|
||||
if (isset($this->requiredParentModules[$newModule])) {
|
||||
$parentModulesToAdd[] = $this->requiredParentModules[$newModule];
|
||||
}
|
||||
return $newModule;
|
||||
}
|
||||
return $module;
|
||||
}, $modules);
|
||||
|
||||
$trimmedModules = array_filter(array_map('trim', $updatedModules));
|
||||
$deduplicatedModules = array_unique($trimmedModules);
|
||||
|
||||
// Check if existing modules (already renamed in a previous migration) need parent modules
|
||||
// This handles the case where the wizard ran in v14.0 before $requiredParentModules existed
|
||||
foreach ($deduplicatedModules as $module) {
|
||||
if (isset($this->requiredParentModules[$module])) {
|
||||
$parentModulesToAdd[] = $this->requiredParentModules[$module];
|
||||
}
|
||||
}
|
||||
|
||||
// Add parent modules if not already present
|
||||
foreach (array_unique($parentModulesToAdd) as $parentModule) {
|
||||
if (!in_array($parentModule, $deduplicatedModules, true)) {
|
||||
$deduplicatedModules[] = $parentModule;
|
||||
}
|
||||
}
|
||||
|
||||
$newModules = implode(',', $deduplicatedModules);
|
||||
if ($originalModules !== $newModules) {
|
||||
if ($dryRun) {
|
||||
return true;
|
||||
}
|
||||
$migrated = $connection->update(
|
||||
$table,
|
||||
[
|
||||
$field => $newModules,
|
||||
],
|
||||
['uid' => (int)$record['uid']]
|
||||
) > 0 || $migrated;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
protected function getConnectionPool(): ConnectionPool
|
||||
{
|
||||
return GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user