1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Install\Updates; 19 20use Symfony\Component\Console\Output\OutputInterface; 21use TYPO3\CMS\Core\Database\ConnectionPool; 22use TYPO3\CMS\Core\Utility\GeneralUtility; 23use TYPO3\CMS\Install\Service\LoadTcaService; 24 25/** 26 * Merge pages_language_overlay rows into pages table 27 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API. 28 */ 29class MigratePagesLanguageOverlayUpdate implements UpgradeWizardInterface, ChattyInterface 30{ 31 /** 32 * @var OutputInterface 33 */ 34 protected $output; 35 36 /** 37 * @return string Unique identifier of this updater 38 */ 39 public function getIdentifier(): string 40 { 41 return 'pagesLanguageOverlay'; 42 } 43 44 /** 45 * @return string Title of this updater 46 */ 47 public function getTitle(): string 48 { 49 return 'Migrate content from pages_language_overlay to pages'; 50 } 51 52 /** 53 * @return string Longer description of this updater 54 */ 55 public function getDescription(): string 56 { 57 return 'The table pages_language_overlay will be removed to align the translation ' 58 . 'handling for pages with the rest of the core. This wizard transfers all data to the pages ' 59 . 'table by creating new entries and linking them to the l10n parent. This might take a while, ' 60 . 'because max. (amount of pages) x (active languages) new entries need be created.'; 61 } 62 63 /** 64 * Checks whether updates are required. 65 * 66 * @return bool Whether an update is required (TRUE) or not (FALSE) 67 */ 68 public function updateNecessary(): bool 69 { 70 // Check if the database table even exists 71 if ($this->checkIfWizardIsRequired()) { 72 return true; 73 } 74 return false; 75 } 76 77 /** 78 * @return string[] All new fields and tables must exist 79 */ 80 public function getPrerequisites(): array 81 { 82 return [ 83 DatabaseUpdatedPrerequisite::class 84 ]; 85 } 86 87 /** 88 * Additional output if there are columns with mm config 89 * 90 * @param OutputInterface $output 91 */ 92 public function setOutput(OutputInterface $output): void 93 { 94 $this->output = $output; 95 } 96 97 /** 98 * Performs the update. 99 * 100 * @return bool Whether everything went smoothly or not 101 */ 102 public function executeUpdate(): bool 103 { 104 // Warn for TCA relation configurations which are not migrated. 105 if (isset($GLOBALS['TCA']['pages_language_overlay']['columns']) 106 && is_array($GLOBALS['TCA']['pages_language_overlay']['columns']) 107 ) { 108 foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) { 109 if (isset($fieldConfiguration['config']['MM'])) { 110 $this->output->writeln('The pages_language_overlay field ' . $fieldName 111 . ' with its MM relation configuration can not be migrated' 112 . ' automatically. Existing data relations to this field have' 113 . ' to be migrated manually.'); 114 } 115 } 116 } 117 118 // Ensure pages_language_overlay is still available in TCA 119 GeneralUtility::makeInstance(LoadTcaService::class)->loadExtensionTablesWithoutMigration(); 120 $this->mergePagesLanguageOverlayIntoPages(); 121 $this->updateInlineRelations(); 122 $this->updateSysHistoryRelations(); 123 return true; 124 } 125 126 /** 127 * 1. Fetches ALL pages_language_overlay (= translations) records 128 * 2. Fetches the given page record (= original language) for each translation 129 * 3. Populates the values from the original language IF the field in the translation record is NOT SET (empty is fine) 130 * 4. Adds proper fields for the translations which is 131 * - l10n_parent = UID of the original-language-record 132 * - pid = PID of the original-language-record (please note: THIS IS DIFFERENT THAN IN pages_language_overlay) 133 * - l10n_source = UID of the original-language-record (only this is supported currently) 134 */ 135 protected function mergePagesLanguageOverlayIntoPages() 136 { 137 $overlayQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages_language_overlay'); 138 $overlayQueryBuilder->getRestrictions()->removeAll(); 139 $overlayRecords = $overlayQueryBuilder 140 ->select('*') 141 ->from('pages_language_overlay') 142 ->execute(); 143 $pagesConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages'); 144 $pagesColumns = $pagesConnection->getSchemaManager()->listTableDetails('pages')->getColumns(); 145 $pagesColumnTypes = []; 146 foreach ($pagesColumns as $pageColumn) { 147 $pagesColumnTypes[$pageColumn->getName()] = $pageColumn->getType()->getBindingType(); 148 } 149 while ($overlayRecord = $overlayRecords->fetch()) { 150 // Early continue if record has been migrated before 151 if ($this->isOverlayRecordMigratedAlready((int)$overlayRecord['uid'])) { 152 continue; 153 } 154 155 $values = []; 156 $originalPageId = (int)$overlayRecord['pid']; 157 $page = $this->fetchDefaultLanguagePageRecord($originalPageId); 158 if (!empty($page)) { 159 foreach ($pagesColumns as $pageColumn) { 160 $name = $pageColumn->getName(); 161 if (isset($overlayRecord[$name])) { 162 $values[$name] = $overlayRecord[$name]; 163 } elseif (isset($page[$name])) { 164 $values[$name] = $page[$name]; 165 } 166 } 167 168 $values['pid'] = $page['pid']; 169 $values['l10n_parent'] = $originalPageId; 170 $values['l10n_source'] = $originalPageId; 171 $values['legacy_overlay_uid'] = $overlayRecord['uid']; 172 unset($values['uid']); 173 $pagesConnection->insert( 174 'pages', 175 $values, 176 $pagesColumnTypes 177 ); 178 } 179 } 180 } 181 182 /** 183 * Inline relations with foreign_field, foreign_table, foreign_table_field on 184 * pages_language_overlay TCA get their existing relations updated to new 185 * uid and pages table. 186 */ 187 protected function updateInlineRelations() 188 { 189 if (isset($GLOBALS['TCA']['pages_language_overlay']['columns']) && is_array($GLOBALS['TCA']['pages_language_overlay']['columns'])) { 190 foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) { 191 // Migrate any 1:n relations 192 if ($fieldConfiguration['config']['type'] === 'inline' 193 && !empty($fieldConfiguration['config']['foreign_field']) 194 && !empty($fieldConfiguration['config']['foreign_table']) 195 && !empty($fieldConfiguration['config']['foreign_table_field']) 196 ) { 197 $foreignTable = trim($fieldConfiguration['config']['foreign_table']); 198 $foreignField = trim($fieldConfiguration['config']['foreign_field']); 199 $foreignTableField = trim($fieldConfiguration['config']['foreign_table_field']); 200 $translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 201 $translatedPagesQueryBuilder->getRestrictions()->removeAll(); 202 $translatedPagesRows = $translatedPagesQueryBuilder 203 ->select('uid', 'legacy_overlay_uid') 204 ->from('pages') 205 ->where( 206 $translatedPagesQueryBuilder->expr()->gt( 207 'l10n_parent', 208 $translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 209 ) 210 ) 211 ->execute(); 212 while ($translatedPageRow = $translatedPagesRows->fetch()) { 213 $foreignTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable); 214 $foreignTableQueryBuilder->getRestrictions()->removeAll(); 215 $foreignTableQueryBuilder 216 ->update($foreignTable) 217 ->set($foreignField, $translatedPageRow['uid']) 218 ->set($foreignTableField, 'pages') 219 ->where( 220 $foreignTableQueryBuilder->expr()->eq( 221 $foreignField, 222 $foreignTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT) 223 ), 224 $foreignTableQueryBuilder->expr()->eq( 225 $foreignTableField, 226 $foreignTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR) 227 ) 228 ) 229 ->execute(); 230 } 231 } 232 } 233 } 234 } 235 236 /** 237 * Update recuid and tablename of sys_history table to pages and new uid 238 * for all pages_language_overlay rows 239 */ 240 protected function updateSysHistoryRelations() 241 { 242 $translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 243 $translatedPagesQueryBuilder->getRestrictions()->removeAll(); 244 $translatedPagesRows = $translatedPagesQueryBuilder 245 ->select('uid', 'legacy_overlay_uid') 246 ->from('pages') 247 ->where( 248 $translatedPagesQueryBuilder->expr()->gt( 249 'l10n_parent', 250 $translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 251 ) 252 ) 253 ->execute(); 254 while ($translatedPageRow = $translatedPagesRows->fetch()) { 255 $historyTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history'); 256 $historyTableQueryBuilder->getRestrictions()->removeAll(); 257 $historyTableQueryBuilder 258 ->update('sys_history') 259 ->set('tablename', 'pages') 260 ->set('recuid', $translatedPageRow['uid']) 261 ->where( 262 $historyTableQueryBuilder->expr()->eq( 263 'recuid', 264 $historyTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT) 265 ), 266 $historyTableQueryBuilder->expr()->eq( 267 'tablename', 268 $historyTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR) 269 ) 270 ) 271 ->execute(); 272 } 273 } 274 275 /** 276 * Fetches a certain page 277 * 278 * @param int $pageId 279 * @return array 280 */ 281 protected function fetchDefaultLanguagePageRecord(int $pageId): array 282 { 283 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 284 $queryBuilder->getRestrictions()->removeAll(); 285 $page = $queryBuilder 286 ->select('*') 287 ->from('pages') 288 ->where( 289 $queryBuilder->expr()->eq( 290 'uid', 291 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) 292 ) 293 ) 294 ->execute() 295 ->fetch(); 296 return $page ?: []; 297 } 298 299 /** 300 * Verify if a single overlay record has been migrated to pages already 301 * by checking the db field legacy_overlay_uid for the orig uid 302 * 303 * @param int $overlayUid 304 * @return bool 305 */ 306 protected function isOverlayRecordMigratedAlready(int $overlayUid): bool 307 { 308 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 309 $queryBuilder->getRestrictions()->removeAll(); 310 $migratedRecord = $queryBuilder 311 ->select('uid') 312 ->from('pages') 313 ->where( 314 $queryBuilder->expr()->eq( 315 'legacy_overlay_uid', 316 $queryBuilder->createNamedParameter($overlayUid, \PDO::PARAM_INT) 317 ) 318 ) 319 ->execute() 320 ->fetch(); 321 return !empty($migratedRecord); 322 } 323 324 /** 325 * Check if the database table "pages_language_overlay" exists and if so, if there are entries in the DB table. 326 * 327 * @return bool 328 * @throws \InvalidArgumentException 329 */ 330 protected function checkIfWizardIsRequired(): bool 331 { 332 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 333 $connection = $connectionPool->getConnectionByName('Default'); 334 $tableNames = $connection->getSchemaManager()->listTableNames(); 335 if (in_array('pages_language_overlay', $tableNames, true)) { 336 // table is available, now check if there are entries in it 337 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 338 ->getQueryBuilderForTable('pages_language_overlay'); 339 $numberOfEntries = $queryBuilder->count('*') 340 ->from('pages_language_overlay') 341 ->execute() 342 ->fetchColumn(); 343 return (bool)$numberOfEntries; 344 } 345 346 return false; 347 } 348} 349