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