Passed
Push — master ( 1e85f0...74899e )
by
unknown
14:10
created

hasPotentialUpdateForTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(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
18
namespace TYPO3\CMS\Install\Updates\RowUpdater;
19
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26
/**
27
 * TYPO3 v11 does not need a "versioned" / "placeholder" pair for newly created records in a version anymore.
28
 *
29
 * This upgrade wizards merges those records pairs into one record.
30
 *
31
 * The strategy is to keep the t3ver_state=1 record and to merge "payload" fields data like
32
 * "header / bodytext" and so on from the t3ver_state=-1 record over to the t3ver_state=1 records.
33
 * The t3ver_state=-1 record is then deleted (or marked as deleted if the table is soft-delete aware).
34
 *
35
 * For relations, this is a bit more tricky. When dealing with CSV and ForeignField relations,
36
 * existing relations are connected to the t3ver_state=1 record. This is fine. For MM relations,
37
 * they point to the t3ver_state=-1 record, though. The implementation thus finds and updates
38
 * those MM relations.
39
 *
40
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
41
 */
42
class WorkspaceNewPlaceholderRemovalMigration implements RowUpdaterInterface, LoggerAwareInterface
43
{
44
    use LoggerAwareTrait;
45
46
    public function getTitle(): string
47
    {
48
        return 'Scan for new versioned records of workspaces and migrate the placeholder and versioned records into one record.';
49
    }
50
51
    /**
52
     * @param string $tableName Table name to check
53
     * @return bool Return true if a table has workspace enabled
54
     */
55
    public function hasPotentialUpdateForTable(string $tableName): bool
56
    {
57
        return BackendUtility::isTableWorkspaceEnabled($tableName);
58
    }
59
60
    public function updateTableRow(string $tableName, array $row): array
61
    {
62
        $versionState = (int)($row['t3ver_state'] ?? 0);
63
        if ($versionState === 1) {
64
            $versionedRecord = $this->fetchVersionedRecord($tableName, (int)$row['uid']);
65
            if ($versionedRecord === null) {
66
                return $row;
67
            }
68
            foreach ($versionedRecord as $fieldName => $value) {
69
                if (in_array($fieldName, ['uid', 'pid', 'deleted', 't3ver_state', 't3ver_oid'], true)) {
70
                    continue;
71
                }
72
                if ($this->isMMField($tableName, $fieldName)) {
73
                    $this->transferMMValues($tableName, $fieldName, (int)$versionedRecord['uid'], (int)$row['uid']);
74
                    continue;
75
                }
76
                $row[$fieldName] = $value;
77
            }
78
        } elseif ($versionState === -1) {
79
            // Delete this row, as it has no use anymore.
80
            // This is safe to do since the uid of the t3ver_state=1 record is always lower than the -1 one,
81
            // so the record has been handled already. Rows are always sorted by uid in the row updater.
82
            $row['deleted'] = 1;
83
        }
84
        return $row;
85
    }
86
87
    /**
88
     * Fetch the t3ver_state = -1 record for a given t3ver_state = 1 record.
89
     *
90
     * @param string $tableName
91
     * @param int $uid
92
     * @return array|null the versioned record or null if none was found.
93
     */
94
    protected function fetchVersionedRecord(string $tableName, int $uid): ?array
95
    {
96
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
97
        $queryBuilder->getRestrictions()->removeAll();
98
        $row = $queryBuilder
99
            ->select('*')
100
            ->from($tableName)
101
            ->where(
102
                $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
103
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
104
            )
105
            ->execute()
106
            ->fetch();
107
        return is_array($row) ? $row : null;
108
    }
109
110
    protected function isMMField(string $tableName, string $fieldName): bool
111
    {
112
        $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? null;
113
        if (!is_array($fieldConfig)) {
114
            return false;
115
        }
116
        if (isset($fieldConfig['MM'])) {
117
            return true;
118
        }
119
        return false;
120
    }
121
122
    /**
123
     * Because MM does not contain workspace information, they were previously bound directly
124
     * to the versioned record, this information is now transferred to the new version t3ver_state=1
125
     * record.
126
     *
127
     * @param string $tableName
128
     * @param string $fieldName
129
     * @param int $originalUid
130
     * @param int $newUid
131
     */
132
    protected function transferMMValues(string $tableName, string $fieldName, int $originalUid, int $newUid): void
133
    {
134
        $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? null;
135
136
        $mmTable = $fieldConfig['MM'];
137
        $matchMMFields = $fieldConfig['MM_match_fields'] ?? null;
138
        $matchMMFieldsMultiple = $fieldConfig['MM_oppositeUsage'] ?? null;
139
        $isOnRightSide = is_array($matchMMFieldsMultiple);
140
        $relationFieldName = $isOnRightSide ? 'uid_local' : 'uid_foreign';
141
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($mmTable);
142
        $queryBuilder->update($mmTable)
143
            ->set($relationFieldName, $newUid, true, \PDO::PARAM_INT)
144
            ->where(
145
                $queryBuilder->expr()->eq($relationFieldName, $queryBuilder->createNamedParameter($originalUid, \PDO::PARAM_INT))
146
            );
147
        if ($matchMMFields) {
148
            foreach ($matchMMFields as $matchMMFieldName => $matchMMFieldValue) {
149
                $queryBuilder->andWhere(
150
                    $queryBuilder->expr()->eq($matchMMFieldName, $queryBuilder->createNamedParameter($matchMMFieldValue))
151
                );
152
            }
153
        }
154
        $queryBuilder->execute();
155
    }
156
}
157