[TASK] Update phpunit/phpunit to 8.3.3
[Packages/TYPO3.CMS.git] / typo3 / sysext / workspaces / Classes / Service / WorkspaceService.php
1 <?php
2 namespace TYPO3\CMS\Workspaces\Service;
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\Restriction\DeletedRestriction;
22 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
23 use TYPO3\CMS\Core\Database\QueryView;
24 use TYPO3\CMS\Core\Localization\LanguageService;
25 use TYPO3\CMS\Core\SingletonInterface;
26 use TYPO3\CMS\Core\Type\Bitmask\Permission;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29 use TYPO3\CMS\Core\Versioning\VersionState;
30
31 /**
32 * Workspace service
33 */
34 class WorkspaceService implements SingletonInterface
35 {
36 /**
37 * @var array
38 */
39 protected $versionsOnPageCache = [];
40
41 /**
42 * @var array
43 */
44 protected $pagesWithVersionsInTable = [];
45
46 const TABLE_WORKSPACE = 'sys_workspace';
47 const SELECT_ALL_WORKSPACES = -98;
48 const LIVE_WORKSPACE_ID = 0;
49
50 /**
51 * retrieves the available workspaces from the database and checks whether
52 * they're available to the current BE user
53 *
54 * @return array array of workspaces available to the current user
55 */
56 public function getAvailableWorkspaces()
57 {
58 $availableWorkspaces = [];
59 // add default workspaces
60 if ($GLOBALS['BE_USER']->checkWorkspace(['uid' => (string)self::LIVE_WORKSPACE_ID])) {
61 $availableWorkspaces[self::LIVE_WORKSPACE_ID] = self::getWorkspaceTitle(self::LIVE_WORKSPACE_ID);
62 }
63 // add custom workspaces (selecting all, filtering by BE_USER check):
64 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
65 $queryBuilder->getRestrictions()
66 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
67
68 $result = $queryBuilder
69 ->select('uid', 'title', 'adminusers', 'members')
70 ->from('sys_workspace')
71 ->orderBy('title')
72 ->execute();
73
74 while ($workspace = $result->fetch()) {
75 if ($GLOBALS['BE_USER']->checkWorkspace($workspace)) {
76 $availableWorkspaces[$workspace['uid']] = $workspace['title'];
77 }
78 }
79 return $availableWorkspaces;
80 }
81
82 /**
83 * Gets the current workspace ID.
84 *
85 * @return int The current workspace ID
86 */
87 public function getCurrentWorkspace()
88 {
89 $workspaceId = $GLOBALS['BE_USER']->workspace;
90 $activeId = $GLOBALS['BE_USER']->getSessionData('tx_workspace_activeWorkspace');
91
92 // Avoid invalid workspace settings
93 if ($activeId !== null && $activeId !== self::SELECT_ALL_WORKSPACES) {
94 $availableWorkspaces = $this->getAvailableWorkspaces();
95 if (isset($availableWorkspaces[$activeId])) {
96 $workspaceId = $activeId;
97 }
98 }
99
100 return $workspaceId;
101 }
102
103 /**
104 * Find the title for the requested workspace.
105 *
106 * @param int $wsId
107 * @return string
108 * @throws \InvalidArgumentException
109 */
110 public static function getWorkspaceTitle($wsId)
111 {
112 $title = false;
113 switch ($wsId) {
114 case self::LIVE_WORKSPACE_ID:
115 $title = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:shortcut_onlineWS');
116 break;
117 default:
118 $labelField = $GLOBALS['TCA']['sys_workspace']['ctrl']['label'];
119 $wsRecord = BackendUtility::getRecord('sys_workspace', $wsId, 'uid,' . $labelField);
120 if (is_array($wsRecord)) {
121 $title = $wsRecord[$labelField];
122 }
123 }
124 if ($title === false) {
125 throw new \InvalidArgumentException('No such workspace defined', 1476045469);
126 }
127 return $title;
128 }
129
130 /**
131 * Building DataHandler CMD-array for swapping all versions in a workspace.
132 *
133 * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
134 * @param bool $doSwap If set, then the currently online versions are swapped into the workspace in exchange for the offline versions. Otherwise the workspace is emptied.
135 * @param int $pageId The page id
136 * @param int $language Select specific language only
137 * @return array Command array for DataHandler
138 */
139 public function getCmdArrayForPublishWS($wsid, $doSwap, $pageId = 0, $language = null)
140 {
141 $wsid = (int)$wsid;
142 $cmd = [];
143 if ($wsid > 0) {
144 // Define stage to select:
145 $stage = -99;
146 if ($wsid > 0) {
147 $workspaceRec = BackendUtility::getRecord('sys_workspace', $wsid);
148 if ($workspaceRec['publish_access'] & 1) {
149 $stage = StagesService::STAGE_PUBLISH_ID;
150 }
151 }
152 // Select all versions to swap:
153 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 999, 'tables_modify', $language);
154 // Traverse the selection to build CMD array:
155 foreach ($versions as $table => $records) {
156 foreach ($records as $rec) {
157 // Build the cmd Array:
158 $cmd[$table][$rec['t3ver_oid']]['version'] = ['action' => 'swap', 'swapWith' => $rec['uid'], 'swapIntoWS' => $doSwap ? 1 : 0];
159 }
160 }
161 }
162 return $cmd;
163 }
164
165 /**
166 * Building DataHandler CMD-array for releasing all versions in a workspace.
167 *
168 * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
169 * @param bool $flush Run Flush (TRUE) or ClearWSID (FALSE) command
170 * @param int $pageId The page id
171 * @param int $language Select specific language only
172 * @return array Command array for DataHandler
173 */
174 public function getCmdArrayForFlushWS($wsid, $flush = true, $pageId = 0, $language = null)
175 {
176 $wsid = (int)$wsid;
177 $cmd = [];
178 if ($wsid > 0) {
179 // Define stage to select:
180 $stage = -99;
181 // Select all versions to swap:
182 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 999, 'tables_modify', $language);
183 // Traverse the selection to build CMD array:
184 foreach ($versions as $table => $records) {
185 foreach ($records as $rec) {
186 // Build the cmd Array:
187 $cmd[$table][$rec['uid']]['version'] = ['action' => $flush ? 'flush' : 'clearWSID'];
188 }
189 }
190 }
191 return $cmd;
192 }
193
194 /**
195 * Select all records from workspace pending for publishing
196 * Used from backend to display workspace overview
197 * User for auto-publishing for selecting versions for publication
198 *
199 * @param int $wsid Workspace ID. If -99, will select ALL versions from ANY workspace. If -98 will select all but ONLINE. >=-1 will select from the actual workspace
200 * @param int $filter Lifecycle filter: 1 = select all drafts (never-published), 2 = select all published one or more times (archive/multiple), anything else selects all.
201 * @param int $stage Stage filter: -99 means no filtering, otherwise it will be used to select only elements with that stage. For publishing, that would be "10
202 * @param int $pageId Page id: Live page for which to find versions in workspace!
203 * @param int $recursionLevel Recursion Level - select versions recursive - parameter is only relevant if $pageId != -1
204 * @param string $selectionType How to collect records for "listing" or "modify" these tables. Support the permissions of each type of record, see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::check.
205 * @param int $language Select specific language only
206 * @return array Array of all records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oidfields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid
207 */
208 public function selectVersionsInWorkspace($wsid, $filter = 0, $stage = -99, $pageId = -1, $recursionLevel = 0, $selectionType = 'tables_select', $language = null)
209 {
210 $wsid = (int)$wsid;
211 $filter = (int)$filter;
212 $output = [];
213 // Contains either nothing or a list with live-uids
214 if ($pageId != -1 && $recursionLevel > 0) {
215 $pageList = $this->getTreeUids($pageId, $wsid, $recursionLevel);
216 } elseif ($pageId != -1) {
217 $pageList = $pageId;
218 } else {
219 $pageList = '';
220 // check if person may only see a "virtual" page-root
221 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
222 $mountPoints = array_unique($mountPoints);
223 if (!in_array(0, $mountPoints)) {
224 $tempPageIds = [];
225 foreach ($mountPoints as $mountPoint) {
226 $tempPageIds[] = $this->getTreeUids($mountPoint, $wsid, $recursionLevel);
227 }
228 $pageList = implode(',', $tempPageIds);
229 $pageList = implode(',', array_unique(explode(',', $pageList)));
230 }
231 }
232 // Traversing all tables supporting versioning:
233 foreach ($GLOBALS['TCA'] as $table => $cfg) {
234 // we do not collect records from tables without permissions on them.
235 if (!$GLOBALS['BE_USER']->check($selectionType, $table)) {
236 continue;
237 }
238 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
239 $recs = $this->selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language);
240 $moveRecs = $this->getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage);
241 $recs = array_merge($recs, $moveRecs);
242 $recs = $this->filterPermittedElements($recs, $table);
243 if (!empty($recs)) {
244 $output[$table] = $recs;
245 }
246 }
247 }
248 return $output;
249 }
250
251 /**
252 * Find all versionized elements except moved records.
253 *
254 * @param string $table
255 * @param string $pageList
256 * @param int $wsid
257 * @param int $filter
258 * @param int $stage
259 * @param int $language
260 * @return array
261 */
262 protected function selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language = null)
263 {
264 // Include root level page as there might be some records with where root level
265 // restriction is ignored (e.g. FAL records)
266 if ($pageList !== '' && BackendUtility::isRootLevelRestrictionIgnored($table)) {
267 $pageList .= ',0';
268 }
269 $isTableLocalizable = BackendUtility::isTableLocalizable($table);
270 $languageParentField = '';
271 // If table is not localizable, but localized reocrds shall
272 // be collected, an empty result array needs to be returned:
273 if ($isTableLocalizable === false && $language > 0) {
274 return [];
275 }
276 if ($isTableLocalizable) {
277 $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
278 }
279
280 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
281 $queryBuilder->getRestrictions()->removeAll()
282 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
283
284 $fields = ['A.uid', 'A.t3ver_oid', 'A.t3ver_stage', 'B.pid AS wspid', 'B.pid AS livepid'];
285 if ($isTableLocalizable) {
286 $fields[] = $languageParentField;
287 $fields[] = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
288 }
289 // Table A is the offline version and pid=-1 defines offline
290 // Table B (online) must have PID >= 0 to signify being online.
291 $constraints = [
292 $queryBuilder->expr()->eq(
293 'A.pid',
294 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
295 ),
296 $queryBuilder->expr()->gte(
297 'B.pid',
298 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
299 ),
300 $queryBuilder->expr()->neq(
301 'A.t3ver_state',
302 $queryBuilder->createNamedParameter(
303 (string)new VersionState(VersionState::MOVE_POINTER),
304 \PDO::PARAM_INT
305 )
306 )
307 ];
308
309 if ($pageList) {
310 $pageIdRestriction = GeneralUtility::intExplode(',', $pageList, true);
311 if ($table === 'pages') {
312 $constraints[] = $queryBuilder->expr()->orX(
313 $queryBuilder->expr()->in(
314 'B.uid',
315 $queryBuilder->createNamedParameter(
316 $pageIdRestriction,
317 Connection::PARAM_INT_ARRAY
318 )
319 ),
320 $queryBuilder->expr()->in(
321 'B.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
322 $queryBuilder->createNamedParameter(
323 $pageIdRestriction,
324 Connection::PARAM_INT_ARRAY
325 )
326 )
327 );
328 } else {
329 $constraints[] = $queryBuilder->expr()->in(
330 'B.pid',
331 $queryBuilder->createNamedParameter(
332 $pageIdRestriction,
333 Connection::PARAM_INT_ARRAY
334 )
335 );
336 }
337 }
338
339 if ($isTableLocalizable && MathUtility::canBeInterpretedAsInteger($language)) {
340 $constraints[] = $queryBuilder->expr()->eq(
341 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'],
342 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
343 );
344 }
345
346 // For "real" workspace numbers, select by that.
347 // If = -98, select all that are NOT online (zero).
348 // Anything else below -1 will not select on the wsid and therefore select all!
349 if ($wsid > self::SELECT_ALL_WORKSPACES) {
350 $constraints[] = $queryBuilder->expr()->eq(
351 'A.t3ver_wsid',
352 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
353 );
354 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
355 $constraints[] = $queryBuilder->expr()->neq(
356 'A.t3ver_wsid',
357 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
358 );
359 }
360
361 // lifecycle filter:
362 // 1 = select all drafts (never-published),
363 // 2 = select all published one or more times (archive/multiple)
364 if ($filter === 1) {
365 $constraints[] = $queryBuilder->expr()->eq(
366 'A.t3ver_count',
367 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
368 );
369 } elseif ($filter === 2) {
370 $constraints[] = $queryBuilder->expr()->gt(
371 'A.t3ver_count',
372 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
373 );
374 }
375
376 if ((int)$stage !== -99) {
377 $constraints[] = $queryBuilder->expr()->eq(
378 'A.t3ver_stage',
379 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
380 );
381 }
382
383 // ... and finally the join between the two tables.
384 $constraints[] = $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'));
385
386 // Select all records from this table in the database from the workspace
387 // This joins the online version with the offline version as tables A and B
388 // Order by UID, mostly to have a sorting in the backend overview module which
389 // doesn't "jump around" when swapping.
390 $rows = $queryBuilder->select(...$fields)
391 ->from($table, 'A')
392 ->from($table, 'B')
393 ->where(...$constraints)
394 ->orderBy('B.uid')
395 ->execute()
396 ->fetchAll();
397
398 return $rows;
399 }
400
401 /**
402 * Find all moved records at their new position.
403 *
404 * @param string $table
405 * @param string $pageList
406 * @param int $wsid
407 * @param int $filter
408 * @param int $stage
409 * @return array
410 */
411 protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage)
412 {
413 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
414 $queryBuilder->getRestrictions()->removeAll()
415 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
416
417 // Aliases:
418 // A - moveTo placeholder
419 // B - online record
420 // C - moveFrom placeholder
421 $constraints = [
422 $queryBuilder->expr()->eq(
423 'A.t3ver_state',
424 $queryBuilder->createNamedParameter(
425 (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
426 \PDO::PARAM_INT
427 )
428 ),
429 $queryBuilder->expr()->gt(
430 'B.pid',
431 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
432 ),
433 $queryBuilder->expr()->eq(
434 'B.t3ver_state',
435 $queryBuilder->createNamedParameter(
436 (string)new VersionState(VersionState::DEFAULT_STATE),
437 \PDO::PARAM_INT
438 )
439 ),
440 $queryBuilder->expr()->eq(
441 'B.t3ver_wsid',
442 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
443 ),
444 $queryBuilder->expr()->eq(
445 'C.pid',
446 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
447 ),
448 $queryBuilder->expr()->eq(
449 'C.t3ver_state',
450 $queryBuilder->createNamedParameter(
451 (string)new VersionState(VersionState::MOVE_POINTER),
452 \PDO::PARAM_INT
453 )
454 ),
455 $queryBuilder->expr()->eq('A.t3ver_move_id', $queryBuilder->quoteIdentifier('B.uid')),
456 $queryBuilder->expr()->eq('B.uid', $queryBuilder->quoteIdentifier('C.t3ver_oid'))
457 ];
458
459 if ($wsid > self::SELECT_ALL_WORKSPACES) {
460 $constraints[] = $queryBuilder->expr()->eq(
461 'A.t3ver_wsid',
462 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
463 );
464 $constraints[] = $queryBuilder->expr()->eq(
465 'C.t3ver_wsid',
466 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
467 );
468 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
469 $constraints[] = $queryBuilder->expr()->neq(
470 'A.t3ver_wsid',
471 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
472 );
473 $constraints[] = $queryBuilder->expr()->neq(
474 'C.t3ver_wsid',
475 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
476 );
477 }
478
479 // lifecycle filter:
480 // 1 = select all drafts (never-published),
481 // 2 = select all published one or more times (archive/multiple)
482 if ($filter === 1) {
483 $constraints[] = $queryBuilder->expr()->eq(
484 'C.t3ver_count',
485 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
486 );
487 } elseif ($filter === 2) {
488 $constraints[] = $queryBuilder->expr()->gt(
489 'C.t3ver_count',
490 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
491 );
492 }
493
494 if ((int)$stage != -99) {
495 $constraints[] = $queryBuilder->expr()->eq(
496 'C.t3ver_stage',
497 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
498 );
499 }
500
501 if ($pageList) {
502 $pageIdRestriction = GeneralUtility::intExplode(',', $pageList, true);
503 if ($table === 'pages') {
504 $constraints[] = $queryBuilder->expr()->orX(
505 $queryBuilder->expr()->in(
506 'B.uid',
507 $queryBuilder->createNamedParameter(
508 $pageIdRestriction,
509 Connection::PARAM_INT_ARRAY
510 )
511 ),
512 $queryBuilder->expr()->in(
513 'B.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
514 $queryBuilder->createNamedParameter(
515 $pageIdRestriction,
516 Connection::PARAM_INT_ARRAY
517 )
518 )
519 );
520 } else {
521 $constraints[] = $queryBuilder->expr()->in(
522 'A.pid',
523 $queryBuilder->createNamedParameter(
524 $pageIdRestriction,
525 Connection::PARAM_INT_ARRAY
526 )
527 );
528 }
529 }
530
531 $rows = $queryBuilder
532 ->select('A.pid AS wspid', 'B.uid AS t3ver_oid', 'C.uid AS uid', 'B.pid AS livepid')
533 ->from($table, 'A')
534 ->from($table, 'B')
535 ->from($table, 'C')
536 ->where(...$constraints)
537 ->orderBy('A.uid')
538 ->execute()
539 ->fetchAll();
540
541 return $rows;
542 }
543
544 /**
545 * Find all page uids recursive starting from a specific page
546 *
547 * @param int $pageId
548 * @param int $wsid
549 * @param int $recursionLevel
550 * @return string Comma sep. uid list
551 */
552 protected function getTreeUids($pageId, $wsid, $recursionLevel)
553 {
554 // Reusing existing functionality with the drawback that
555 // mount points are not covered yet
556 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
557 $searchObj = GeneralUtility::makeInstance(QueryView::class);
558 if ($pageId > 0) {
559 $pageList = $searchObj->getTreeList($pageId, $recursionLevel, 0, $perms_clause);
560 } else {
561 $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
562 if (!is_array($mountPoints) || empty($mountPoints)) {
563 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
564 $mountPoints = array_unique($mountPoints);
565 }
566 $newList = [];
567 foreach ($mountPoints as $mountPoint) {
568 $newList[] = $searchObj->getTreeList($mountPoint, $recursionLevel, 0, $perms_clause);
569 }
570 $pageList = implode(',', $newList);
571 }
572 unset($searchObj);
573
574 if (BackendUtility::isTableWorkspaceEnabled('pages') && $pageList) {
575 // Remove the "subbranch" if a page was moved away
576 $pageIds = GeneralUtility::intExplode(',', $pageList, true);
577 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
578 $queryBuilder->getRestrictions()
579 ->removeAll()
580 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
581 $result = $queryBuilder
582 ->select('uid', 'pid', 't3ver_move_id')
583 ->from('pages')
584 ->where(
585 $queryBuilder->expr()->in(
586 't3ver_move_id',
587 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
588 ),
589 $queryBuilder->expr()->eq(
590 't3ver_wsid',
591 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
592 )
593 )
594 ->orderBy('uid')
595 ->execute();
596
597 $movedAwayPages = [];
598 while ($row = $result->fetch()) {
599 $movedAwayPages[$row['t3ver_move_id']] = $row;
600 }
601
602 // move all pages away
603 $newList = array_diff($pageIds, array_keys($movedAwayPages));
604 // keep current page in the list
605 $newList[] = $pageId;
606 // move back in if still connected to the "remaining" pages
607 do {
608 $changed = false;
609 foreach ($movedAwayPages as $uid => $rec) {
610 if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) {
611 $newList[] = $uid;
612 $changed = true;
613 }
614 }
615 } while ($changed);
616
617 // In case moving pages is enabled we need to replace all move-to pointer with their origin
618 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
619 $queryBuilder->getRestrictions()
620 ->removeAll()
621 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
622 $result = $queryBuilder->select('uid', 't3ver_move_id')
623 ->from('pages')
624 ->where(
625 $queryBuilder->expr()->in(
626 'uid',
627 $queryBuilder->createNamedParameter($newList, Connection::PARAM_INT_ARRAY)
628 )
629 )
630 ->orderBy('uid')
631 ->execute();
632
633 $pages = [];
634 while ($row = $result->fetch()) {
635 $pages[$row['uid']] = $row;
636 }
637
638 $pageIds = $newList;
639 if (!in_array($pageId, $pageIds)) {
640 $pageIds[] = $pageId;
641 }
642
643 $newList = [];
644 foreach ($pageIds as $pageId) {
645 if ((int)$pages[$pageId]['t3ver_move_id'] > 0) {
646 $newList[] = (int)$pages[$pageId]['t3ver_move_id'];
647 } else {
648 $newList[] = $pageId;
649 }
650 }
651 $pageList = implode(',', $newList);
652 }
653
654 return $pageList;
655 }
656
657 /**
658 * Remove all records which are not permitted for the user
659 *
660 * @param array $recs
661 * @param string $table
662 * @return array
663 */
664 protected function filterPermittedElements($recs, $table)
665 {
666 $permittedElements = [];
667 if (is_array($recs)) {
668 foreach ($recs as $rec) {
669 if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) {
670 $permittedElements[] = $rec;
671 }
672 }
673 }
674 return $permittedElements;
675 }
676
677 /**
678 * Checking access to the page the record is on, respecting ignored root level restrictions
679 *
680 * @param string $table Name of the table
681 * @param array $record Record row to be checked
682 * @return bool
683 */
684 protected function isPageAccessibleForCurrentUser($table, array $record)
685 {
686 $pageIdField = $table === 'pages' ? 'uid' : 'wspid';
687 $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : null;
688 if ($pageId === null) {
689 return false;
690 }
691 if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) {
692 return true;
693 }
694 $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody');
695
696 return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1);
697 }
698
699 /**
700 * Check current be users language access on given record.
701 *
702 * @param string $table Name of the table
703 * @param array $record Record row to be checked
704 * @return bool
705 */
706 protected function isLanguageAccessibleForCurrentUser($table, array $record)
707 {
708 if (BackendUtility::isTableLocalizable($table)) {
709 $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
710 } else {
711 return true;
712 }
713 return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid);
714 }
715
716 /**
717 * Determine whether a specific page is new and not yet available in the LIVE workspace
718 *
719 * @param int $id Primary key of the page to check
720 * @param int $language Language for which to check the page
721 * @return bool
722 */
723 public static function isNewPage($id, $language = 0)
724 {
725 $isNewPage = false;
726 // If the language is not default, check state of overlay
727 if ($language > 0) {
728 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
729 ->getQueryBuilderForTable('pages');
730 $queryBuilder->getRestrictions()
731 ->removeAll()
732 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
733 $row = $queryBuilder->select('t3ver_state')
734 ->from('pages')
735 ->where(
736 $queryBuilder->expr()->eq(
737 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
738 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
739 ),
740 $queryBuilder->expr()->eq(
741 $GLOBALS['TCA']['pages']['ctrl']['languageField'],
742 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
743 ),
744 $queryBuilder->expr()->eq(
745 't3ver_wsid',
746 $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT)
747 )
748 )
749 ->setMaxResults(1)
750 ->execute()
751 ->fetch();
752
753 if ($row !== false) {
754 $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
755 }
756 } else {
757 $rec = BackendUtility::getRecord('pages', $id, 't3ver_state');
758 if (is_array($rec)) {
759 $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
760 }
761 }
762 return $isNewPage;
763 }
764
765 /**
766 * Determines whether a page has workspace versions.
767 *
768 * @param int $workspaceId
769 * @param int $pageId
770 * @return bool
771 */
772 public function hasPageRecordVersions($workspaceId, $pageId)
773 {
774 if ((int)$workspaceId === 0 || (int)$pageId === 0) {
775 return false;
776 }
777
778 if (isset($this->versionsOnPageCache[$workspaceId][$pageId])) {
779 return $this->versionsOnPageCache[$workspaceId][$pageId];
780 }
781
782 $this->versionsOnPageCache[$workspaceId][$pageId] = false;
783
784 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
785 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
786 continue;
787 }
788
789 $pages = $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
790 // Early break on first match
791 if (!empty($pages[(string)$pageId])) {
792 $this->versionsOnPageCache[$workspaceId][$pageId] = true;
793 break;
794 }
795 }
796
797 $parameters = [
798 'workspaceId' => $workspaceId,
799 'pageId' => $pageId,
800 'versionsOnPageCache' => &$this->versionsOnPageCache,
801 ];
802 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'] ?? [] as $hookFunction) {
803 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
804 }
805
806 return $this->versionsOnPageCache[$workspaceId][$pageId];
807 }
808
809 /**
810 * Gets all pages that have workspace versions per table.
811 *
812 * Result:
813 * [
814 * 'sys_template' => [],
815 * 'tt_content' => [
816 * 1 => true,
817 * 11 => true,
818 * 13 => true,
819 * 15 => true
820 * ],
821 * 'tx_something => [
822 * 15 => true,
823 * 11 => true,
824 * 21 => true
825 * ],
826 * ]
827 *
828 * @param int $workspaceId
829 *
830 * @return array
831 */
832 public function getPagesWithVersionsInTable($workspaceId)
833 {
834 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
835 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
836 continue;
837 }
838
839 $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
840 }
841
842 return $this->pagesWithVersionsInTable[$workspaceId];
843 }
844
845 /**
846 * Gets all pages that have workspace versions in a particular table.
847 *
848 * Result:
849 * [
850 * 1 => true,
851 * 11 => true,
852 * 13 => true,
853 * 15 => true
854 * ],
855 *
856 * @param int $workspaceId
857 * @param string $tableName
858 * @return array
859 */
860 protected function fetchPagesWithVersionsInTable($workspaceId, $tableName)
861 {
862 if ((int)$workspaceId === 0) {
863 return [];
864 }
865
866 if (!isset($this->pagesWithVersionsInTable[$workspaceId])) {
867 $this->pagesWithVersionsInTable[$workspaceId] = [];
868 }
869
870 if (!isset($this->pagesWithVersionsInTable[$workspaceId][$tableName])) {
871 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = [];
872
873 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
874 $queryBuilder->getRestrictions()
875 ->removeAll()
876 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
877
878 $movePointerParameter = $queryBuilder->createNamedParameter(
879 VersionState::MOVE_POINTER,
880 \PDO::PARAM_INT
881 );
882 $workspaceIdParameter = $queryBuilder->createNamedParameter(
883 $workspaceId,
884 \PDO::PARAM_INT
885 );
886 $pageIdParameter = $queryBuilder->createNamedParameter(
887 -1,
888 \PDO::PARAM_INT
889 );
890 // create sub-queries, parameters are available for main query
891 $versionQueryBuilder = $this->createQueryBuilderForTable($tableName)
892 ->select('A.t3ver_oid')
893 ->from($tableName, 'A')
894 ->where(
895 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
896 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
897 $queryBuilder->expr()->neq('A.t3ver_state', $movePointerParameter)
898 );
899 $movePointerQueryBuilder = $this->createQueryBuilderForTable($tableName)
900 ->select('A.t3ver_oid')
901 ->from($tableName, 'A')
902 ->where(
903 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
904 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
905 $queryBuilder->expr()->eq('A.t3ver_state', $movePointerParameter)
906 );
907 $subQuery = '%s IN (%s)';
908 // execute main query
909 $result = $queryBuilder
910 ->select('B.pid AS pageId')
911 ->from($tableName, 'B')
912 ->orWhere(
913 sprintf(
914 $subQuery,
915 $queryBuilder->quoteIdentifier('B.uid'),
916 $versionQueryBuilder->getSQL()
917 ),
918 sprintf(
919 $subQuery,
920 $queryBuilder->quoteIdentifier('B.t3ver_move_id'),
921 $movePointerQueryBuilder->getSQL()
922 )
923 )
924 ->groupBy('B.pid')
925 ->execute();
926
927 $pageIds = [];
928 while ($row = $result->fetch()) {
929 $pageIds[$row['pageId']] = true;
930 }
931
932 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = $pageIds;
933
934 $parameters = [
935 'workspaceId' => $workspaceId,
936 'tableName' => $tableName,
937 'pagesWithVersionsInTable' => &$this->pagesWithVersionsInTable,
938 ];
939 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['fetchPagesWithVersionsInTable'] ?? [] as $hookFunction) {
940 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
941 }
942 }
943
944 return $this->pagesWithVersionsInTable[$workspaceId][$tableName];
945 }
946
947 /**
948 * @param string $tableName
949 * @return QueryBuilder
950 */
951 protected function createQueryBuilderForTable(string $tableName)
952 {
953 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
954 ->getQueryBuilderForTable($tableName);
955 $queryBuilder->getRestrictions()
956 ->removeAll()
957 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
958 return $queryBuilder;
959 }
960
961 /**
962 * @return LanguageService|null
963 */
964 protected static function getLanguageService(): ?LanguageService
965 {
966 return $GLOBALS['LANG'] ?? null;
967 }
968 }