[BUGFIX] Make LocalizationRepository handle copied records
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Domain / Repository / Localization / LocalizationRepository.php
1 <?php
2 namespace TYPO3\CMS\Backend\Domain\Repository\Localization;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\Connection;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
21 use TYPO3\CMS\Core\Database\Query\QueryHelper;
22 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
23 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26 /**
27 * Repository for record localizations
28 */
29 class LocalizationRepository
30 {
31 /**
32 * Fetch the language from which the records of a colPos in a certain language were initially localized
33 *
34 * @param int $pageId
35 * @param int $colPos
36 * @param int $localizedLanguage
37 * @return array|false
38 */
39 public function fetchOriginLanguage($pageId, $colPos, $localizedLanguage)
40 {
41 $queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
42
43 $constraints = [
44 $queryBuilder->expr()->eq(
45 'tt_content.colPos',
46 $queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
47 ),
48 $queryBuilder->expr()->eq(
49 'tt_content.pid',
50 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
51 ),
52 $queryBuilder->expr()->eq(
53 'tt_content.sys_language_uid',
54 $queryBuilder->createNamedParameter($localizedLanguage, \PDO::PARAM_INT)
55 ),
56 ];
57 $constraints += $this->getAllowedLanguageConstraintsForBackendUser();
58
59 $queryBuilder->select('tt_content_orig.sys_language_uid')
60 ->from('tt_content')
61 ->join(
62 'tt_content',
63 'tt_content',
64 'tt_content_orig',
65 $queryBuilder->expr()->eq(
66 'tt_content.l10n_source',
67 $queryBuilder->quoteIdentifier('tt_content_orig.uid')
68 )
69 )
70 ->join(
71 'tt_content_orig',
72 'sys_language',
73 'sys_language',
74 $queryBuilder->expr()->eq(
75 'tt_content_orig.sys_language_uid',
76 $queryBuilder->quoteIdentifier('sys_language.uid')
77 )
78 )
79 ->where(...$constraints)
80 ->groupBy('tt_content_orig.sys_language_uid');
81
82 return $queryBuilder->execute()->fetch();
83 }
84
85 /**
86 * Returns number of localized records in given page, colPos and language
87 * Records which were added to the language directly (not through translation) are not counted.
88 *
89 * @param int $pageId
90 * @param int $colPos
91 * @param int $languageId
92 * @return int
93 */
94 public function getLocalizedRecordCount($pageId, $colPos, $languageId)
95 {
96 $queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
97
98 $rowCount = $queryBuilder->count('uid')
99 ->from('tt_content')
100 ->where(
101 $queryBuilder->expr()->eq(
102 'tt_content.sys_language_uid',
103 $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
104 ),
105 $queryBuilder->expr()->eq(
106 'tt_content.colPos',
107 $queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
108 ),
109 $queryBuilder->expr()->eq(
110 'tt_content.pid',
111 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
112 ),
113 $queryBuilder->expr()->neq(
114 'tt_content.l10n_source',
115 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
116 )
117 )
118 ->execute()
119 ->fetchColumn(0);
120
121 return (int)$rowCount;
122 }
123
124 /**
125 * Fetch all available languages
126 *
127 * @param int $pageId
128 * @param int $colPos
129 * @param int $languageId
130 * @return array
131 */
132 public function fetchAvailableLanguages($pageId, $colPos, $languageId)
133 {
134 $queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
135
136 $constraints = [
137 $queryBuilder->expr()->eq(
138 'tt_content.sys_language_uid',
139 $queryBuilder->quoteIdentifier('sys_language.uid')
140 ),
141 $queryBuilder->expr()->eq(
142 'tt_content.colPos',
143 $queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
144 ),
145 $queryBuilder->expr()->eq(
146 'tt_content.pid',
147 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
148 ),
149 $queryBuilder->expr()->neq(
150 'sys_language.uid',
151 $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
152 )
153 ];
154 $constraints += $this->getAllowedLanguageConstraintsForBackendUser();
155
156 $queryBuilder->select('sys_language.uid')
157 ->from('tt_content')
158 ->from('sys_language')
159 ->where(...$constraints)
160 ->groupBy('sys_language.uid')
161 ->orderBy('sys_language.sorting');
162
163 $result = $queryBuilder->execute()->fetchAll();
164
165 return $result;
166 }
167
168 /**
169 * Builds an additional where clause to exclude deleted records and setting the versioning placeholders
170 *
171 * @return string
172 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
173 */
174 public function getExcludeQueryPart()
175 {
176 GeneralUtility::logDeprecatedFunction();
177
178 return BackendUtility::deleteClause('tt_content') . BackendUtility::versioningPlaceholderClause('tt_content');
179 }
180
181 /**
182 * Builds an additional where clause to exclude hidden languages and limit a backend user to its allowed languages,
183 * if the user is not an admin.
184 *
185 * @return string
186 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
187 */
188 public function getAllowedLanguagesForBackendUser()
189 {
190 GeneralUtility::logDeprecatedFunction();
191
192 $backendUser = $this->getBackendUser();
193 $additionalWhere = '';
194 if (!$backendUser->isAdmin()) {
195 $additionalWhere .= ' AND sys_language.hidden=0';
196
197 if (!empty($backendUser->user['allowed_languages'])) {
198 $additionalWhere .= ' AND sys_language.uid IN(' . implode(',', GeneralUtility::intExplode(',', $backendUser->user['allowed_languages'])) . ')';
199 }
200 }
201
202 return $additionalWhere;
203 }
204
205 /**
206 * Builds additional query constraints to exclude hidden languages and
207 * limit a backend user to its allowed languages (unless the user is an admin)
208 *
209 * @return array
210 */
211 protected function getAllowedLanguageConstraintsForBackendUser(): array
212 {
213 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
214 $constraints = [];
215
216 $backendUser = $this->getBackendUser();
217 if (!$backendUser->isAdmin()) {
218 if (!empty($GLOBALS['TCA']['sys_language']['ctrl']['enablecolumns']['disabled'])) {
219 $constraints[] = $queryBuilder->expr()->eq(
220 'sys_language.' . $GLOBALS['TCA']['sys_language']['ctrl']['enablecolumns']['disabled'],
221 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
222 );
223 }
224
225 if (!empty($backendUser->user['allowed_languages'])) {
226 $constraints[] = $queryBuilder->expr()->in(
227 'sys_language.uid',
228 $queryBuilder->createNamedParameter(
229 GeneralUtility::intExplode(',', $backendUser->user['allowed_languages'], true),
230 Connection::PARAM_INT_ARRAY
231 )
232 );
233 }
234 }
235
236 return $constraints;
237 }
238
239 /**
240 * Get records for copy process
241 *
242 * @param int $pageId
243 * @param int $colPos
244 * @param int $destLanguageId
245 * @param int $languageId
246 * @param string $fields
247 * @return \Doctrine\DBAL\Driver\Statement
248 */
249 public function getRecordsToCopyDatabaseResult($pageId, $colPos, $destLanguageId, $languageId, $fields = '*')
250 {
251 $originalUids = [];
252
253 // Get original uid of existing elements triggered language / colpos
254 $queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
255
256 $originalUidsStatement = $queryBuilder
257 ->select('l10n_source')
258 ->from('tt_content')
259 ->where(
260 $queryBuilder->expr()->eq(
261 'sys_language_uid',
262 $queryBuilder->createNamedParameter($destLanguageId, \PDO::PARAM_INT)
263 ),
264 $queryBuilder->expr()->eq(
265 'tt_content.colPos',
266 $queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
267 ),
268 $queryBuilder->expr()->eq(
269 'tt_content.pid',
270 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
271 )
272 )
273 ->execute();
274
275 while ($origUid = $originalUidsStatement->fetchColumn(0)) {
276 $originalUids[] = (int)$origUid;
277 }
278
279 $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
280 ->from('tt_content')
281 ->where(
282 $queryBuilder->expr()->eq(
283 'tt_content.sys_language_uid',
284 $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
285 ),
286 $queryBuilder->expr()->eq(
287 'tt_content.colPos',
288 $queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
289 ),
290 $queryBuilder->expr()->eq(
291 'tt_content.pid',
292 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
293 )
294 )
295 ->orderBy('tt_content.sorting');
296
297 if (!empty($originalUids)) {
298 $queryBuilder
299 ->andWhere(
300 $queryBuilder->expr()->notIn(
301 'tt_content.uid',
302 $queryBuilder->createNamedParameter($originalUids, Connection::PARAM_INT_ARRAY)
303 )
304 );
305 }
306
307 return $queryBuilder->execute();
308 }
309
310 /**
311 * Fetches the localization for a given record.
312 *
313 * @FIXME: This method is a clone of BackendUtility::getRecordLocalization, using origUid instead of transOrigPointerField
314 *
315 * @param string $table Table name present in $GLOBALS['TCA']
316 * @param int $uid The uid of the record
317 * @param int $language The uid of the language record in sys_language
318 * @param string $andWhereClause Optional additional WHERE clause (default: '')
319 * @return mixed Multidimensional array with selected records; if none exist, FALSE is returned
320 *
321 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
322 */
323 public function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
324 {
325 GeneralUtility::logDeprecatedFunction();
326 $recordLocalization = false;
327
328 // Pages still stores translations in the pages_language_overlay table, all other tables store in themself
329 if ($table === 'pages') {
330 $table = 'pages_language_overlay';
331 }
332
333 if (BackendUtility::isTableLocalizable($table)) {
334 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
335
336 if (isset($tcaCtrl['origUid'])) {
337 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
338 ->getQueryBuilderForTable($table);
339 $queryBuilder->getRestrictions()
340 ->removeAll()
341 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
342 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
343
344 $queryBuilder->select('*')
345 ->from($table)
346 ->where(
347 $queryBuilder->expr()->eq(
348 $tcaCtrl['origUid'],
349 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
350 ),
351 $queryBuilder->expr()->eq(
352 $tcaCtrl['languageField'],
353 $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
354 )
355 )
356 ->setMaxResults(1);
357
358 if ($andWhereClause) {
359 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
360 }
361
362 $recordLocalization = $queryBuilder->execute()->fetchAll();
363 }
364 }
365 return $recordLocalization;
366 }
367
368 /**
369 * Returning uid of previous localized record, if any, for tables with a "sortby" column
370 * Used when new localized records are created so that localized records are sorted in the same order as the default language records
371 *
372 * @FIXME: This method is a clone of DataHandler::getPreviousLocalizedRecordUid which is protected there and uses
373 * BackendUtility::getRecordLocalization which we also needed to clone in this class. Also, this method takes two
374 * language arguments.
375 *
376 * @param string $table Table name
377 * @param int $uid Uid of default language record
378 * @param int $pid Pid of default language record
379 * @param int $sourceLanguage Language of origin
380 * @param int $destinationLanguage Language of localization
381 * @return int uid of record after which the localized record should be inserted
382 *
383 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
384 */
385 public function getPreviousLocalizedRecordUid($table, $uid, $pid, $sourceLanguage, $destinationLanguage)
386 {
387 GeneralUtility::logDeprecatedFunction();
388 $previousLocalizedRecordUid = $uid;
389 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
390 $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
391 $select = [$sortRow, 'pid', 'uid'];
392 // For content elements, we also need the colPos
393 if ($table === 'tt_content') {
394 $select[] = 'colPos';
395 }
396 // Get the sort value of the default language record
397 $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
398 if (is_array($row)) {
399 $queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
400
401 $queryBuilder->select(...$select)
402 ->from($table)
403 ->where(
404 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
405 $queryBuilder->expr()->eq(
406 'sys_language_uid',
407 $queryBuilder->createNamedParameter($sourceLanguage, \PDO::PARAM_INT)
408 ),
409 $queryBuilder->expr()->lt(
410 $sortRow,
411 $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
412 )
413 );
414
415 // Respect the colPos for content elements
416 if ($table === 'tt_content') {
417 $queryBuilder->andWhere(
418 $queryBuilder->expr()->eq(
419 'colPos',
420 $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
421 )
422 );
423 }
424
425 $previousRow = $queryBuilder->orderBy($sortRow, 'DESC')->execute()->fetch();
426
427 // If there is an element, find its localized record in specified localization language
428 if ($previousRow !== false) {
429 $previousLocalizedRecord = $this->getRecordLocalization(
430 $table,
431 $previousRow['uid'],
432 $destinationLanguage
433 );
434 if (is_array($previousLocalizedRecord[0])) {
435 $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
436 }
437 }
438 }
439 }
440
441 return $previousLocalizedRecordUid;
442 }
443
444 /**
445 * Returns the current BE user.
446 *
447 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
448 */
449 protected function getBackendUser()
450 {
451 return $GLOBALS['BE_USER'];
452 }
453
454 /**
455 * Get a QueryBuilder for the given table with preconfigured restrictions
456 * to not retrieve workspace placeholders or deleted records.
457 *
458 * @param string $tableName
459 * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder
460 */
461 protected function getQueryBuilderWithWorkspaceRestriction(string $tableName): QueryBuilder
462 {
463 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
464 $queryBuilder->getRestrictions()
465 ->removeAll()
466 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
467 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
468
469 return $queryBuilder;
470 }
471 }