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