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