5d21dc566e0860bd221a5cb269987e15e57f905a
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / RowUpdater / L10nModeUpdater.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Install\Updates\RowUpdater;
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 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\DataHandling\DataHandler;
21 use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
22 use TYPO3\CMS\Core\DataHandling\Localization\State;
23 use TYPO3\CMS\Core\Localization\LanguageService;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Core\Versioning\VersionState;
26
27 /**
28 * Migrate values for database records having columns
29 * using "l10n_mode" set to "mergeIfNotBlank" or "exclude".
30 */
31 class L10nModeUpdater implements RowUpdaterInterface
32 {
33 /**
34 * List of tables with information about to migrate fields.
35 * Created during hasPotentialUpdateForTable(), used in updateTableRow()
36 *
37 * @var array
38 */
39 protected $payload = [];
40
41 /**
42 * Get title
43 *
44 * @return string
45 */
46 public function getTitle(): string
47 {
48 return 'Migrate values in database records having "l10n_mode"'
49 . ' either set to "exclude" or "mergeIfNotBlank"';
50 }
51
52 /**
53 * Return true if a table needs modifications.
54 *
55 * @param string $tableName Table name to check
56 * @return bool True if this table has fields to migrate
57 */
58 public function hasPotentialUpdateForTable(string $tableName): bool
59 {
60 $this->payload[$tableName] = $this->getL10nModePayloadForTable($tableName);
61 return !empty($this->payload[$tableName]['localizations']);
62 }
63
64 /**
65 * Update single row if needed
66 *
67 * @param string $tableName
68 * @param array $inputRow Given row data
69 * @return array Modified row data
70 */
71 public function updateTableRow(string $tableName, array $inputRow): array
72 {
73 $currentId = $inputRow['uid'];
74
75 if (empty($this->payload[$tableName]['localizations'][$currentId])) {
76 return $inputRow;
77 }
78
79 // disable DataHandler hooks for processing this update
80 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'])) {
81 $dataHandlerHooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'];
82 unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']);
83 }
84
85 if (empty($GLOBALS['LANG'])) {
86 $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
87 }
88 if (!empty($GLOBALS['BE_USER'])) {
89 $adminUser = $GLOBALS['BE_USER'];
90 }
91 // the admin user is required to defined workspace state when working with DataHandler
92 $fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
93 $fakeAdminUser->user = ['uid' => 0, 'username' => '_migration_', 'admin' => 1];
94 $fakeAdminUser->workspace = ($inputRow['t3ver_wsid'] ?? 0);
95 $GLOBALS['BE_USER'] = $fakeAdminUser;
96
97 $tablePayload = $this->payload[$tableName];
98 $parentId = $tablePayload['localizations'][$currentId];
99 $parentTableName = ($tableName === 'pages_language_overlay' ? 'pages' : $tableName);
100
101 $liveId = $currentId;
102 if (!empty($inputRow['t3ver_wsid'])
103 && !empty($inputRow['t3ver_oid'])
104 && !VersionState::cast($inputRow['t3ver_state'])
105 ->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
106 $liveId = $inputRow['t3ver_oid'];
107 }
108
109 $dataMap = [];
110
111 // simulate modifying a parent record to trigger dependent updates
112 if (in_array('exclude', $tablePayload['fieldModes'])) {
113 $parentRecord = $this->getRow($parentTableName, $parentId);
114 foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
115 if ($fieldMode !== 'exclude') {
116 continue;
117 }
118 $dataMap[$parentTableName][$parentId][$fieldName] = $parentRecord[$fieldName];
119 }
120 $dataMap = DataMapProcessor::instance($dataMap, $fakeAdminUser)->process();
121 unset($dataMap[$parentTableName][$parentId]);
122 if (empty($dataMap[$parentTableName])) {
123 unset($dataMap[$parentTableName]);
124 }
125 }
126
127 // define localization states and thus trigger updates later
128 if (State::isApplicable($tableName)) {
129 $stateUpdates = [];
130 foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
131 if ($fieldMode !== 'mergeIfNotBlank') {
132 continue;
133 }
134 if (!empty($inputRow[$fieldName])) {
135 $stateUpdates[$fieldName] = State::STATE_CUSTOM;
136 } else {
137 $stateUpdates[$fieldName] = State::STATE_PARENT;
138 }
139 }
140
141 $languageState = State::create($tableName);
142 $languageState->update($stateUpdates);
143 // only consider field names that still used mergeIfNotBlank
144 $modifiedFieldNames = array_intersect(
145 array_keys($tablePayload['fieldModes']),
146 $languageState->getModifiedFieldNames()
147 );
148 if (!empty($modifiedFieldNames)) {
149 $dataMap = [
150 $tableName => [
151 $liveId => [
152 'l10n_state' => $languageState->toArray()
153 ]
154 ]
155 ];
156 }
157 }
158
159 if (empty($dataMap)) {
160 return $inputRow;
161 }
162
163 // let DataHandler process all updates, $inputRow won't change
164 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
165 $dataHandler->enableLogging = false;
166 $dataHandler->start($dataMap, [], $fakeAdminUser);
167 $dataHandler->process_datamap();
168
169 if (!empty($dataHandlerHooks)) {
170 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'] = $dataHandlerHooks;
171 }
172 if (!empty($adminUser)) {
173 $GLOBALS['BE_USER'] = $adminUser;
174 }
175
176 // the unchanged(!) state as submitted
177 return $inputRow;
178 }
179
180 /**
181 * Retrieves field names grouped per table name having "l10n_mode" set
182 * to a relevant value that shall be migrated in database records.
183 *
184 * Resulting array is structured like this:
185 * + fields: [field a, field b, ...]
186 * + sources
187 * + source uid: [localization uid, localization uid, ...]
188 *
189 * @param string $tableName Table name
190 * @return array Payload information for this table
191 * @throws \RuntimeException
192 */
193 protected function getL10nModePayloadForTable(string $tableName): array
194 {
195 if (!is_array($GLOBALS['TCA'][$tableName])) {
196 throw new \RuntimeException(
197 'Globals TCA of given table name must exist',
198 1484176136
199 );
200 }
201
202 $tableDefinition = $GLOBALS['TCA'][$tableName];
203 $languageFieldName = ($tableDefinition['ctrl']['languageField'] ?? null);
204 $parentFieldName = ($tableDefinition['ctrl']['transOrigPointerField'] ?? null);
205
206 if (
207 empty($tableDefinition['columns'])
208 || !is_array($tableDefinition['columns'])
209 || empty($languageFieldName)
210 || empty($parentFieldName)
211 ) {
212 return [];
213 }
214
215 $fieldModes = [];
216 foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) {
217 $l10nMode = ($fieldConfiguration['l10n_mode'] ?? null);
218 $allowLanguageSynchronization = ($fieldConfiguration['config']['behaviour']['allowLanguageSynchronization'] ?? null);
219
220 if ($l10nMode === 'exclude') {
221 $fieldModes[$fieldName] = $l10nMode;
222 } elseif ($allowLanguageSynchronization) {
223 $fieldModes[$fieldName] = 'mergeIfNotBlank';
224 }
225 }
226
227 if (empty($fieldModes)) {
228 return [];
229 }
230
231 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
232 $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
233 $queryBuilder->getRestrictions()->removeAll();
234 $queryBuilder->from($tableName);
235
236 $parentFieldName = $tableDefinition['ctrl']['transOrigPointerField'];
237 $selectFieldNames = ['uid', $parentFieldName];
238
239 $predicates = [
240 $queryBuilder->expr()->gt(
241 $tableDefinition['ctrl']['languageField'],
242 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
243 ),
244 $queryBuilder->expr()->gt(
245 $parentFieldName,
246 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
247 )
248 ];
249
250 if (!empty($tableDefinition['ctrl']['versioningWS'])) {
251 $selectFieldNames = array_merge(
252 $selectFieldNames,
253 ['t3ver_wsid', 't3ver_oid', 't3ver_state']
254 );
255 $predicates[] = $queryBuilder->expr()->orX(
256 $queryBuilder->expr()->eq(
257 't3ver_state',
258 $queryBuilder->createNamedParameter(
259 VersionState::NEW_PLACEHOLDER_VERSION,
260 \PDO::PARAM_INT
261 )
262 ),
263 $queryBuilder->expr()->eq(
264 't3ver_state',
265 $queryBuilder->createNamedParameter(
266 VersionState::DEFAULT_STATE,
267 \PDO::PARAM_INT
268 )
269 ),
270 $queryBuilder->expr()->eq(
271 't3ver_state',
272 $queryBuilder->createNamedParameter(
273 VersionState::MOVE_POINTER,
274 \PDO::PARAM_INT
275 )
276 )
277 );
278 }
279
280 $statement = $queryBuilder
281 ->select(...$selectFieldNames)
282 ->andWhere(...$predicates)
283 ->execute();
284
285 $payload = [];
286
287 foreach ($statement as $row) {
288 $translationId = $row['uid'];
289 $parentId = (int)$row[$parentFieldName];
290 $payload['localizations'][$translationId] = $parentId;
291 }
292 if (!empty($payload['localizations'])) {
293 $payload['fieldModes'] = $fieldModes;
294 }
295
296 return $payload;
297 }
298
299 /**
300 * @param string $tableName
301 * @param int $id
302 * @return array
303 */
304 protected function getRow(string $tableName, int $id)
305 {
306 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
307 ->getQueryBuilderForTable($tableName);
308 $queryBuilder->getRestrictions()->removeAll();
309
310 $statement = $queryBuilder
311 ->select('*')
312 ->from($tableName)
313 ->where(
314 $queryBuilder->expr()->eq(
315 'uid',
316 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
317 )
318 )
319 ->execute();
320
321 return $statement->fetch();
322 }
323 }