Files
gutachten_t3/typo3/sysext/install/Classes/Updates/MigratePagesLanguageOverlayUpdate.php
T
2026-10-19 08:18:39 +02:00

360 lines
14 KiB
PHP

<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* 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!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\LoadTcaService;
/**
* Merge pages_language_overlay rows into pages table
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigratePagesLanguageOverlayUpdate implements UpgradeWizardInterface, ChattyInterface
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'pagesLanguageOverlay';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate content from pages_language_overlay to pages';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The table pages_language_overlay will be removed to align the translation '
. 'handling for pages with the rest of the core. This wizard transfers all data to the pages '
. 'table by creating new entries and linking them to the l10n parent. This might take a while, '
. 'because max. (amount of pages) x (active languages) new entries need be created.';
}
/**
* Checks whether updates are required.
*
* @return bool Whether an update is required (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
// Check if the database table even exists
if ($this->checkIfWizardIsRequired()) {
return true;
}
return false;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Additional output if there are columns with mm config
*
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
/**
* Performs the update.
*
* @return bool Whether everything went smoothly or not
*/
public function executeUpdate(): bool
{
// Warn for TCA relation configurations which are not migrated.
if (isset($GLOBALS['TCA']['pages_language_overlay']['columns'])
&& is_array($GLOBALS['TCA']['pages_language_overlay']['columns'])
) {
foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) {
if (isset($fieldConfiguration['config']['MM'])) {
$this->output->writeln('The pages_language_overlay field ' . $fieldName
. ' with its MM relation configuration can not be migrated'
. ' automatically. Existing data relations to this field have'
. ' to be migrated manually.');
}
}
}
// Ensure pages_language_overlay is still available in TCA
GeneralUtility::makeInstance(LoadTcaService::class)->loadExtensionTablesWithoutMigration();
$this->mergePagesLanguageOverlayIntoPages();
$this->updateInlineRelations();
$this->updateSysHistoryRelations();
$this->enableFeatureFlag();
return true;
}
/**
* 1. Fetches ALL pages_language_overlay (= translations) records
* 2. Fetches the given page record (= original language) for each translation
* 3. Populates the values from the original language IF the field in the translation record is NOT SET (empty is fine)
* 4. Adds proper fields for the translations which is
* - l10n_parent = UID of the original-language-record
* - pid = PID of the original-language-record (please note: THIS IS DIFFERENT THAN IN pages_language_overlay)
* - l10n_source = UID of the original-language-record (only this is supported currently)
*/
protected function mergePagesLanguageOverlayIntoPages()
{
$overlayQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages_language_overlay');
$overlayQueryBuilder->getRestrictions()->removeAll();
$overlayRecords = $overlayQueryBuilder
->select('*')
->from('pages_language_overlay')
->execute();
$pagesConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
$pagesColumns = $pagesConnection->getSchemaManager()->listTableDetails('pages')->getColumns();
$pagesColumnTypes = [];
foreach ($pagesColumns as $pageColumn) {
$pagesColumnTypes[$pageColumn->getName()] = $pageColumn->getType()->getBindingType();
}
while ($overlayRecord = $overlayRecords->fetch()) {
// Early continue if record has been migrated before
if ($this->isOverlayRecordMigratedAlready((int)$overlayRecord['uid'])) {
continue;
}
$values = [];
$originalPageId = (int)$overlayRecord['pid'];
$page = $this->fetchDefaultLanguagePageRecord($originalPageId);
if (!empty($page)) {
foreach ($pagesColumns as $pageColumn) {
$name = $pageColumn->getName();
if (isset($overlayRecord[$name])) {
$values[$name] = $overlayRecord[$name];
} elseif (isset($page[$name])) {
$values[$name] = $page[$name];
}
}
$values['pid'] = $page['pid'];
$values['l10n_parent'] = $originalPageId;
$values['l10n_source'] = $originalPageId;
$values['legacy_overlay_uid'] = $overlayRecord['uid'];
unset($values['uid']);
$pagesConnection->insert(
'pages',
$values,
$pagesColumnTypes
);
}
}
}
/**
* Inline relations with foreign_field, foreign_table, foreign_table_field on
* pages_language_overlay TCA get their existing relations updated to new
* uid and pages table.
*/
protected function updateInlineRelations()
{
if (isset($GLOBALS['TCA']['pages_language_overlay']['columns']) && is_array($GLOBALS['TCA']['pages_language_overlay']['columns'])) {
foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) {
// Migrate any 1:n relations
if ($fieldConfiguration['config']['type'] === 'inline'
&& !empty($fieldConfiguration['config']['foreign_field'])
&& !empty($fieldConfiguration['config']['foreign_table'])
&& !empty($fieldConfiguration['config']['foreign_table_field'])
) {
$foreignTable = trim($fieldConfiguration['config']['foreign_table']);
$foreignField = trim($fieldConfiguration['config']['foreign_field']);
$foreignTableField = trim($fieldConfiguration['config']['foreign_table_field']);
$translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$translatedPagesQueryBuilder->getRestrictions()->removeAll();
$translatedPagesRows = $translatedPagesQueryBuilder
->select('uid', 'legacy_overlay_uid')
->from('pages')
->where(
$translatedPagesQueryBuilder->expr()->gt(
'l10n_parent',
$translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
)
)
->execute();
while ($translatedPageRow = $translatedPagesRows->fetch()) {
$foreignTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
$foreignTableQueryBuilder->getRestrictions()->removeAll();
$foreignTableQueryBuilder
->update($foreignTable)
->set($foreignField, $translatedPageRow['uid'])
->set($foreignTableField, 'pages')
->where(
$foreignTableQueryBuilder->expr()->eq(
$foreignField,
$foreignTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT)
),
$foreignTableQueryBuilder->expr()->eq(
$foreignTableField,
$foreignTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR)
)
)
->execute();
}
}
}
}
}
/**
* Update recuid and tablename of sys_history table to pages and new uid
* for all pages_language_overlay rows
*/
protected function updateSysHistoryRelations()
{
$translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$translatedPagesQueryBuilder->getRestrictions()->removeAll();
$translatedPagesRows = $translatedPagesQueryBuilder
->select('uid', 'legacy_overlay_uid')
->from('pages')
->where(
$translatedPagesQueryBuilder->expr()->gt(
'l10n_parent',
$translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
)
)
->execute();
while ($translatedPageRow = $translatedPagesRows->fetch()) {
$historyTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
$historyTableQueryBuilder->getRestrictions()->removeAll();
$historyTableQueryBuilder
->update('sys_history')
->set('tablename', 'pages')
->set('recuid', $translatedPageRow['uid'])
->where(
$historyTableQueryBuilder->expr()->eq(
'recuid',
$historyTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT)
),
$historyTableQueryBuilder->expr()->eq(
'tablename',
$historyTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR)
)
)
->execute();
}
}
/**
* Fetches a certain page
*
* @param int $pageId
* @return array
*/
protected function fetchDefaultLanguagePageRecord(int $pageId): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$page = $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
)
)
->execute()
->fetch();
return $page ?: [];
}
/**
* Verify if a single overlay record has been migrated to pages already
* by checking the db field legacy_overlay_uid for the orig uid
*
* @param int $overlayUid
* @return bool
*/
protected function isOverlayRecordMigratedAlready(int $overlayUid): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$migratedRecord = $queryBuilder
->select('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'legacy_overlay_uid',
$queryBuilder->createNamedParameter($overlayUid, \PDO::PARAM_INT)
)
)
->execute()
->fetch();
return !empty($migratedRecord);
}
/**
* Check if the database table "pages_language_overlay" exists and if so, if there are entries in the DB table.
*
* @return bool
* @throws \InvalidArgumentException
*/
protected function checkIfWizardIsRequired(): bool
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$connection = $connectionPool->getConnectionByName('Default');
$tableNames = $connection->getSchemaManager()->listTableNames();
if (in_array('pages_language_overlay', $tableNames, true)) {
// table is available, now check if there are entries in it
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages_language_overlay');
$numberOfEntries = $queryBuilder->count('*')
->from('pages_language_overlay')
->execute()
->fetchColumn();
return (bool)$numberOfEntries;
}
return false;
}
/**
* Once the update wizard is run through, the feature to not load any pages_language_overlay data can
* be activated.
*
* Basically writes 'SYS/features/unifiedPageTranslationHandling' to LocalConfiguration.php
*/
protected function enableFeatureFlag()
{
GeneralUtility::makeInstance(ConfigurationManager::class)->enableFeature('unifiedPageTranslationHandling');
}
}