[TASK] Clean up workspace preview top bar
[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\Configuration\TranslationConfigurationProvider;
18 use TYPO3\CMS\Backend\Routing\UriBuilder;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Database\Connection;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
24 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
26 use TYPO3\CMS\Core\SingletonInterface;
27 use TYPO3\CMS\Core\Type\Bitmask\Permission;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Core\Utility\MathUtility;
30 use TYPO3\CMS\Core\Versioning\VersionState;
31
32 /**
33 * Workspace service
34 */
35 class WorkspaceService implements SingletonInterface
36 {
37 /**
38 * @var array
39 */
40 protected $pageCache = [];
41
42 /**
43 * @var array
44 */
45 protected $versionsOnPageCache = [];
46
47 /**
48 * @var array
49 */
50 protected $pagesWithVersionsInTable = [];
51
52 const TABLE_WORKSPACE = 'sys_workspace';
53 const SELECT_ALL_WORKSPACES = -98;
54 const LIVE_WORKSPACE_ID = 0;
55 /**
56 * retrieves the available workspaces from the database and checks whether
57 * they're available to the current BE user
58 *
59 * @return array array of worspaces available to the current user
60 */
61 public function getAvailableWorkspaces()
62 {
63 $availableWorkspaces = [];
64 // add default workspaces
65 if ($GLOBALS['BE_USER']->checkWorkspace(['uid' => (string)self::LIVE_WORKSPACE_ID])) {
66 $availableWorkspaces[self::LIVE_WORKSPACE_ID] = self::getWorkspaceTitle(self::LIVE_WORKSPACE_ID);
67 }
68 // add custom workspaces (selecting all, filtering by BE_USER check):
69
70 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
71 $queryBuilder->getRestrictions()
72 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
73
74 $result = $queryBuilder
75 ->select('uid', 'title', 'adminusers', 'members')
76 ->from('sys_workspace')
77 ->orderBy('title')
78 ->execute();
79
80 while ($workspace = $result->fetch()) {
81 if ($GLOBALS['BE_USER']->checkWorkspace($workspace)) {
82 $availableWorkspaces[$workspace['uid']] = $workspace['title'];
83 }
84 }
85 return $availableWorkspaces;
86 }
87
88 /**
89 * Gets the current workspace ID.
90 *
91 * @return int The current workspace ID
92 */
93 public function getCurrentWorkspace()
94 {
95 $workspaceId = $GLOBALS['BE_USER']->workspace;
96 $activeId = $GLOBALS['BE_USER']->getSessionData('tx_workspace_activeWorkspace');
97
98 // Avoid invalid workspace settings
99 if ($activeId !== null && $activeId !== self::SELECT_ALL_WORKSPACES) {
100 $availableWorkspaces = $this->getAvailableWorkspaces();
101 if (isset($availableWorkspaces[$activeId])) {
102 $workspaceId = $activeId;
103 }
104 }
105
106 return $workspaceId;
107 }
108
109 /**
110 * Find the title for the requested workspace.
111 *
112 * @param int $wsId
113 * @return string
114 * @throws \InvalidArgumentException
115 */
116 public static function getWorkspaceTitle($wsId)
117 {
118 $title = false;
119 switch ($wsId) {
120 case self::LIVE_WORKSPACE_ID:
121 $title = $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:shortcut_onlineWS');
122 break;
123 default:
124 $labelField = $GLOBALS['TCA']['sys_workspace']['ctrl']['label'];
125 $wsRecord = BackendUtility::getRecord('sys_workspace', $wsId, 'uid,' . $labelField);
126 if (is_array($wsRecord)) {
127 $title = $wsRecord[$labelField];
128 }
129 }
130 if ($title === false) {
131 throw new \InvalidArgumentException('No such workspace defined', 1476045469);
132 }
133 return $title;
134 }
135
136 /**
137 * Building DataHandler CMD-array for swapping all versions in a workspace.
138 *
139 * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
140 * @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.
141 * @param int $pageId The page id
142 * @param int $language Select specific language only
143 * @return array Command array for DataHandler
144 */
145 public function getCmdArrayForPublishWS($wsid, $doSwap, $pageId = 0, $language = null)
146 {
147 $wsid = (int)$wsid;
148 $cmd = [];
149 if ($wsid > 0) {
150 // Define stage to select:
151 $stage = -99;
152 if ($wsid > 0) {
153 $workspaceRec = BackendUtility::getRecord('sys_workspace', $wsid);
154 if ($workspaceRec['publish_access'] & 1) {
155 $stage = \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_ID;
156 }
157 }
158 // Select all versions to swap:
159 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 999, 'tables_modify', $language);
160 // Traverse the selection to build CMD array:
161 foreach ($versions as $table => $records) {
162 foreach ($records as $rec) {
163 // Build the cmd Array:
164 $cmd[$table][$rec['t3ver_oid']]['version'] = ['action' => 'swap', 'swapWith' => $rec['uid'], 'swapIntoWS' => $doSwap ? 1 : 0];
165 }
166 }
167 }
168 return $cmd;
169 }
170
171 /**
172 * Building DataHandler CMD-array for releasing all versions in a workspace.
173 *
174 * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
175 * @param bool $flush Run Flush (TRUE) or ClearWSID (FALSE) command
176 * @param int $pageId The page id
177 * @param int $language Select specific language only
178 * @return array Command array for DataHandler
179 */
180 public function getCmdArrayForFlushWS($wsid, $flush = true, $pageId = 0, $language = null)
181 {
182 $wsid = (int)$wsid;
183 $cmd = [];
184 if ($wsid > 0) {
185 // Define stage to select:
186 $stage = -99;
187 // Select all versions to swap:
188 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 999, 'tables_modify', $language);
189 // Traverse the selection to build CMD array:
190 foreach ($versions as $table => $records) {
191 foreach ($records as $rec) {
192 // Build the cmd Array:
193 $cmd[$table][$rec['uid']]['version'] = ['action' => $flush ? 'flush' : 'clearWSID'];
194 }
195 }
196 }
197 return $cmd;
198 }
199
200 /**
201 * Select all records from workspace pending for publishing
202 * Used from backend to display workspace overview
203 * User for auto-publishing for selecting versions for publication
204 *
205 * @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
206 * @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.
207 * @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
208 * @param int $pageId Page id: Live page for which to find versions in workspace!
209 * @param int $recursionLevel Recursion Level - select versions recursive - parameter is only relevant if $pageId != -1
210 * @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.
211 * @param int $language Select specific language only
212 * @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
213 */
214 public function selectVersionsInWorkspace($wsid, $filter = 0, $stage = -99, $pageId = -1, $recursionLevel = 0, $selectionType = 'tables_select', $language = null)
215 {
216 $wsid = (int)$wsid;
217 $filter = (int)$filter;
218 $output = [];
219 // Contains either nothing or a list with live-uids
220 if ($pageId != -1 && $recursionLevel > 0) {
221 $pageList = $this->getTreeUids($pageId, $wsid, $recursionLevel);
222 } elseif ($pageId != -1) {
223 $pageList = $pageId;
224 } else {
225 $pageList = '';
226 // check if person may only see a "virtual" page-root
227 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
228 $mountPoints = array_unique($mountPoints);
229 if (!in_array(0, $mountPoints)) {
230 $tempPageIds = [];
231 foreach ($mountPoints as $mountPoint) {
232 $tempPageIds[] = $this->getTreeUids($mountPoint, $wsid, $recursionLevel);
233 }
234 $pageList = implode(',', $tempPageIds);
235 $pageList = implode(',', array_unique(explode(',', $pageList)));
236 }
237 }
238 // Traversing all tables supporting versioning:
239 foreach ($GLOBALS['TCA'] as $table => $cfg) {
240 // we do not collect records from tables without permissions on them.
241 if (!$GLOBALS['BE_USER']->check($selectionType, $table)) {
242 continue;
243 }
244 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
245 $recs = $this->selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language);
246 $moveRecs = $this->getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage);
247 $recs = array_merge($recs, $moveRecs);
248 $recs = $this->filterPermittedElements($recs, $table);
249 if (!empty($recs)) {
250 $output[$table] = $recs;
251 }
252 }
253 }
254 return $output;
255 }
256
257 /**
258 * Find all versionized elements except moved records.
259 *
260 * @param string $table
261 * @param string $pageList
262 * @param int $wsid
263 * @param int $filter
264 * @param int $stage
265 * @param int $language
266 * @return array
267 */
268 protected function selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language = null)
269 {
270 // Include root level page as there might be some records with where root level
271 // restriction is ignored (e.g. FAL records)
272 if ($pageList !== '' && BackendUtility::isRootLevelRestrictionIgnored($table)) {
273 $pageList .= ',0';
274 }
275 $isTableLocalizable = BackendUtility::isTableLocalizable($table);
276 $languageParentField = '';
277 // If table is not localizable, but localized reocrds shall
278 // be collected, an empty result array needs to be returned:
279 if ($isTableLocalizable === false && $language > 0) {
280 return [];
281 }
282 if ($isTableLocalizable) {
283 $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
284 }
285
286 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
287 $queryBuilder->getRestrictions()->removeAll()
288 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
289
290 $fields = ['A.uid', 'A.t3ver_oid', 'A.t3ver_stage', 'B.pid AS wspid', 'B.pid AS livepid'];
291 if ($isTableLocalizable) {
292 $fields[] = $languageParentField;
293 $fields[] = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
294 }
295 // Table A is the offline version and pid=-1 defines offline
296 // Table B (online) must have PID >= 0 to signify being online.
297 $constraints = [
298 $queryBuilder->expr()->eq(
299 'A.pid',
300 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
301 ),
302 $queryBuilder->expr()->gte(
303 'B.pid',
304 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
305 ),
306 $queryBuilder->expr()->neq(
307 'A.t3ver_state',
308 $queryBuilder->createNamedParameter(
309 (string)new VersionState(VersionState::MOVE_POINTER),
310 \PDO::PARAM_INT
311 )
312 )
313 ];
314
315 if ($pageList) {
316 $pidField = $table === 'pages' ? 'uid' : 'pid';
317 $constraints[] = $queryBuilder->expr()->in(
318 'B.' . $pidField,
319 $queryBuilder->createNamedParameter(
320 GeneralUtility::intExplode(',', $pageList, true),
321 Connection::PARAM_INT_ARRAY
322 )
323 );
324 }
325
326 if ($isTableLocalizable && MathUtility::canBeInterpretedAsInteger($language)) {
327 $constraints[] = $queryBuilder->expr()->eq(
328 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'],
329 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
330 );
331 }
332
333 // For "real" workspace numbers, select by that.
334 // If = -98, select all that are NOT online (zero).
335 // Anything else below -1 will not select on the wsid and therefore select all!
336 if ($wsid > self::SELECT_ALL_WORKSPACES) {
337 $constraints[] = $queryBuilder->expr()->eq(
338 'A.t3ver_wsid',
339 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
340 );
341 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
342 $constraints[] = $queryBuilder->expr()->neq(
343 'A.t3ver_wsid',
344 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
345 );
346 }
347
348 // lifecycle filter:
349 // 1 = select all drafts (never-published),
350 // 2 = select all published one or more times (archive/multiple)
351 if ($filter === 1) {
352 $constraints[] = $queryBuilder->expr()->eq(
353 'A.t3ver_count',
354 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
355 );
356 } elseif ($filter === 2) {
357 $constraints[] = $queryBuilder->expr()->gt(
358 'A.t3ver_count',
359 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
360 );
361 }
362
363 if ((int)$stage !== -99) {
364 $constraints[] = $queryBuilder->expr()->eq(
365 'A.t3ver_stage',
366 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
367 );
368 }
369
370 // ... and finally the join between the two tables.
371 $constraints[] = $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'));
372
373 // Select all records from this table in the database from the workspace
374 // This joins the online version with the offline version as tables A and B
375 // Order by UID, mostly to have a sorting in the backend overview module which
376 // doesn't "jump around" when swapping.
377 $rows = $queryBuilder->select(...$fields)
378 ->from($table, 'A')
379 ->from($table, 'B')
380 ->where(...$constraints)
381 ->orderBy('B.uid')
382 ->execute()
383 ->fetchAll();
384
385 return $rows;
386 }
387
388 /**
389 * Find all moved records at their new position.
390 *
391 * @param string $table
392 * @param string $pageList
393 * @param int $wsid
394 * @param int $filter
395 * @param int $stage
396 * @return array
397 */
398 protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage)
399 {
400 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
401 $queryBuilder->getRestrictions()->removeAll()
402 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
403
404 // Aliases:
405 // A - moveTo placeholder
406 // B - online record
407 // C - moveFrom placeholder
408 $constraints = [
409 $queryBuilder->expr()->eq(
410 'A.t3ver_state',
411 $queryBuilder->createNamedParameter(
412 (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
413 \PDO::PARAM_INT
414 )
415 ),
416 $queryBuilder->expr()->gt(
417 'B.pid',
418 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
419 ),
420 $queryBuilder->expr()->eq(
421 'B.t3ver_state',
422 $queryBuilder->createNamedParameter(
423 (string)new VersionState(VersionState::DEFAULT_STATE),
424 \PDO::PARAM_INT
425 )
426 ),
427 $queryBuilder->expr()->eq(
428 'B.t3ver_wsid',
429 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
430 ),
431 $queryBuilder->expr()->eq(
432 'C.pid',
433 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
434 ),
435 $queryBuilder->expr()->eq(
436 'C.t3ver_state',
437 $queryBuilder->createNamedParameter(
438 (string)new VersionState(VersionState::MOVE_POINTER),
439 \PDO::PARAM_INT
440 )
441 ),
442 $queryBuilder->expr()->eq('A.t3ver_move_id', $queryBuilder->quoteIdentifier('B.uid')),
443 $queryBuilder->expr()->eq('B.uid', $queryBuilder->quoteIdentifier('C.t3ver_oid'))
444 ];
445
446 if ($wsid > self::SELECT_ALL_WORKSPACES) {
447 $constraints[] = $queryBuilder->expr()->eq(
448 'A.t3ver_wsid',
449 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
450 );
451 $constraints[] = $queryBuilder->expr()->eq(
452 'C.t3ver_wsid',
453 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
454 );
455 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
456 $constraints[] = $queryBuilder->expr()->neq(
457 'A.t3ver_wsid',
458 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
459 );
460 $constraints[] = $queryBuilder->expr()->neq(
461 'C.t3ver_wsid',
462 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
463 );
464 }
465
466 // lifecycle filter:
467 // 1 = select all drafts (never-published),
468 // 2 = select all published one or more times (archive/multiple)
469 if ($filter === 1) {
470 $constraints[] = $queryBuilder->expr()->eq(
471 'C.t3ver_count',
472 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
473 );
474 } elseif ($filter === 2) {
475 $constraints[] = $queryBuilder->expr()->gt(
476 'C.t3ver_count',
477 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
478 );
479 }
480
481 if ((int)$stage != -99) {
482 $constraints[] = $queryBuilder->expr()->eq(
483 'C.t3ver_stage',
484 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
485 );
486 }
487
488 if ($pageList) {
489 $pidField = $table === 'pages' ? 'B.uid' : 'A.pid';
490 $constraints[] = $queryBuilder->expr()->in(
491 $pidField,
492 $queryBuilder->createNamedParameter(
493 GeneralUtility::intExplode(',', $pageList, true),
494 Connection::PARAM_INT_ARRAY
495 )
496 );
497 }
498
499 $rows = $queryBuilder
500 ->select('A.pid AS wspid', 'B.uid AS t3ver_oid', 'C.uid AS uid', 'B.pid AS livepid')
501 ->from($table, 'A')
502 ->from($table, 'B')
503 ->from($table, 'C')
504 ->where(...$constraints)
505 ->orderBy('A.uid')
506 ->execute()
507 ->fetchAll();
508
509 return $rows;
510 }
511
512 /**
513 * Find all page uids recursive starting from a specific page
514 *
515 * @param int $pageId
516 * @param int $wsid
517 * @param int $recursionLevel
518 * @return string Comma sep. uid list
519 */
520 protected function getTreeUids($pageId, $wsid, $recursionLevel)
521 {
522 // Reusing existing functionality with the drawback that
523 // mount points are not covered yet
524 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
525 /** @var $searchObj \TYPO3\CMS\Core\Database\QueryView */
526 $searchObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\QueryView::class);
527 if ($pageId > 0) {
528 $pageList = $searchObj->getTreeList($pageId, $recursionLevel, 0, $perms_clause);
529 } else {
530 $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
531 if (!is_array($mountPoints) || empty($mountPoints)) {
532 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
533 $mountPoints = array_unique($mountPoints);
534 }
535 $newList = [];
536 foreach ($mountPoints as $mountPoint) {
537 $newList[] = $searchObj->getTreeList($mountPoint, $recursionLevel, 0, $perms_clause);
538 }
539 $pageList = implode(',', $newList);
540 }
541 unset($searchObj);
542
543 if (BackendUtility::isTableWorkspaceEnabled('pages') && $pageList) {
544 // Remove the "subbranch" if a page was moved away
545 $pageIds = GeneralUtility::intExplode(',', $pageList, true);
546 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
547 $queryBuilder->getRestrictions()
548 ->removeAll()
549 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
550 $result = $queryBuilder
551 ->select('uid', 'pid', 't3ver_move_id')
552 ->from('pages')
553 ->where(
554 $queryBuilder->expr()->in(
555 't3ver_move_id',
556 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
557 ),
558 $queryBuilder->expr()->eq(
559 't3ver_wsid',
560 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
561 )
562 )
563 ->orderBy('uid')
564 ->execute();
565
566 $movedAwayPages = [];
567 while ($row = $result->fetch()) {
568 $movedAwayPages[$row['t3ver_move_id']] = $row;
569 }
570
571 // move all pages away
572 $newList = array_diff($pageIds, array_keys($movedAwayPages));
573 // keep current page in the list
574 $newList[] = $pageId;
575 // move back in if still connected to the "remaining" pages
576 do {
577 $changed = false;
578 foreach ($movedAwayPages as $uid => $rec) {
579 if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) {
580 $newList[] = $uid;
581 $changed = true;
582 }
583 }
584 } while ($changed);
585
586 // In case moving pages is enabled we need to replace all move-to pointer with their origin
587 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
588 $queryBuilder->getRestrictions()
589 ->removeAll()
590 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
591 $result = $queryBuilder->select('uid', 't3ver_move_id')
592 ->from('pages')
593 ->where(
594 $queryBuilder->expr()->in(
595 'uid',
596 $queryBuilder->createNamedParameter($newList, Connection::PARAM_INT_ARRAY)
597 )
598 )
599 ->orderBy('uid')
600 ->execute();
601
602 $pages = [];
603 while ($row = $result->fetch()) {
604 $pages[$row['uid']] = $row;
605 }
606
607 $pageIds = $newList;
608 if (!in_array($pageId, $pageIds)) {
609 $pageIds[] = $pageId;
610 }
611
612 $newList = [];
613 foreach ($pageIds as $pageId) {
614 if ((int)$pages[$pageId]['t3ver_move_id'] > 0) {
615 $newList[] = (int)$pages[$pageId]['t3ver_move_id'];
616 } else {
617 $newList[] = $pageId;
618 }
619 }
620 $pageList = implode(',', $newList);
621 }
622
623 return $pageList;
624 }
625
626 /**
627 * Remove all records which are not permitted for the user
628 *
629 * @param array $recs
630 * @param string $table
631 * @return array
632 */
633 protected function filterPermittedElements($recs, $table)
634 {
635 $permittedElements = [];
636 if (is_array($recs)) {
637 foreach ($recs as $rec) {
638 if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) {
639 $permittedElements[] = $rec;
640 }
641 }
642 }
643 return $permittedElements;
644 }
645
646 /**
647 * Checking access to the page the record is on, respecting ignored root level restrictions
648 *
649 * @param string $table Name of the table
650 * @param array $record Record row to be checked
651 * @return bool
652 */
653 protected function isPageAccessibleForCurrentUser($table, array $record)
654 {
655 $pageIdField = $table === 'pages' ? 'uid' : 'wspid';
656 $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : null;
657 if ($pageId === null) {
658 return false;
659 }
660 if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) {
661 return true;
662 }
663 $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody');
664
665 return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1);
666 }
667
668 /**
669 * Check current be users language access on given record.
670 *
671 * @param string $table Name of the table
672 * @param array $record Record row to be checked
673 * @return bool
674 */
675 protected function isLanguageAccessibleForCurrentUser($table, array $record)
676 {
677 if (BackendUtility::isTableLocalizable($table)) {
678 $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
679 } else {
680 return true;
681 }
682 return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid);
683 }
684
685 /**
686 * Determine whether a specific page is new and not yet available in the LIVE workspace
687 *
688 * @param int $id Primary key of the page to check
689 * @param int $language Language for which to check the page
690 * @return bool
691 */
692 public static function isNewPage($id, $language = 0)
693 {
694 $isNewPage = false;
695 // If the language is not default, check state of overlay
696 if ($language > 0) {
697 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
698 ->getQueryBuilderForTable('pages');
699 $queryBuilder->getRestrictions()
700 ->removeAll()
701 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
702 $row = $queryBuilder->select('t3ver_state')
703 ->from('pages')
704 ->where(
705 $queryBuilder->expr()->eq(
706 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
707 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
708 ),
709 $queryBuilder->expr()->eq(
710 $GLOBALS['TCA']['pages']['ctrl']['languageField'],
711 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
712 ),
713 $queryBuilder->expr()->eq(
714 't3ver_wsid',
715 $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT)
716 )
717 )
718 ->setMaxResults(1)
719 ->execute()
720 ->fetch();
721
722 if ($row !== false) {
723 $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
724 }
725 } else {
726 $rec = BackendUtility::getRecord('pages', $id, 't3ver_state');
727 if (is_array($rec)) {
728 $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
729 }
730 }
731 return $isNewPage;
732 }
733
734 /**
735 * Generates a view link for a page.
736 *
737 * @param string $table Table to be used
738 * @param int $uid Uid of the version(!) record
739 * @param array $liveRecord Optional live record data
740 * @param array $versionRecord Optional version record data
741 * @return string
742 */
743 public static function viewSingleRecord($table, $uid, array $liveRecord = null, array $versionRecord = null)
744 {
745 if ($table === 'pages') {
746 return BackendUtility::viewOnClick(BackendUtility::getLiveVersionIdOfRecord('pages', $uid));
747 }
748
749 if ($liveRecord === null) {
750 $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
751 }
752 if ($versionRecord === null) {
753 $versionRecord = BackendUtility::getRecord($table, $uid);
754 }
755 if (VersionState::cast($versionRecord['t3ver_state'])->equals(VersionState::MOVE_POINTER)) {
756 $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRecord['uid'], 'pid');
757 }
758
759 // Directly use pid value and consider move placeholders
760 $previewPageId = (empty($movePlaceholder['pid']) ? $liveRecord['pid'] : $movePlaceholder['pid']);
761 $additionalParameters = '&previewWS=' . $versionRecord['t3ver_wsid'];
762 // Add language parameter if record is a localization
763 if (BackendUtility::isTableLocalizable($table)) {
764 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
765 if ($versionRecord[$languageField] > 0) {
766 $additionalParameters .= '&L=' . $versionRecord[$languageField];
767 }
768 }
769
770 $pageTsConfig = BackendUtility::getPagesTSconfig($previewPageId);
771 $viewUrl = '';
772
773 // Directly use determined direct page id
774 if ($table === 'tt_content') {
775 $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters);
776 } elseif (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table]) || !empty($pageTsConfig['options.']['workspaces.']['previewPageId'])) {
777 // Analyze Page TSconfig options.workspaces.previewPageId
778 if (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table])) {
779 $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId.'][$table];
780 } else {
781 $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId'];
782 }
783 // Extract possible settings (e.g. "field:pid")
784 list($previewKey, $previewValue) = explode(':', $previewConfiguration, 2);
785 if ($previewKey === 'field') {
786 $previewPageId = (int)$liveRecord[$previewValue];
787 } else {
788 $previewPageId = (int)$previewConfiguration;
789 }
790 $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters);
791 } elseif (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'])) {
792 // Call user function to render the single record view
793 $_params = [
794 'table' => $table,
795 'uid' => $uid,
796 'record' => $liveRecord,
797 'liveRecord' => $liveRecord,
798 'versionRecord' => $versionRecord,
799 ];
800 $_funcRef = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'];
801 $null = null;
802 $viewUrl = GeneralUtility::callUserFunction($_funcRef, $_params, $null);
803 }
804
805 return $viewUrl;
806 }
807
808 /**
809 * Determine whether this page for the current
810 *
811 * @param int $pageUid
812 * @param int $workspaceUid
813 * @return bool
814 */
815 public function canCreatePreviewLink($pageUid, $workspaceUid)
816 {
817 $result = true;
818 if ($pageUid > 0 && $workspaceUid > 0) {
819 $pageRecord = BackendUtility::getRecord('pages', $pageUid);
820 BackendUtility::workspaceOL('pages', $pageRecord, $workspaceUid);
821 if (VersionState::cast($pageRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
822 $result = false;
823 }
824 } else {
825 $result = false;
826 }
827 return $result;
828 }
829
830 /**
831 * Generates a workspace preview link.
832 *
833 * @param int $uid The ID of the record to be linked
834 * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
835 */
836 public function generateWorkspacePreviewLink($uid)
837 {
838 $previewObject = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Hook\PreviewHook::class);
839 $timeToLiveHours = $previewObject->getPreviewLinkLifetime();
840 $previewKeyword = $previewObject->compilePreviewKeyword($GLOBALS['BE_USER']->user['uid'], $timeToLiveHours * 3600, $this->getCurrentWorkspace());
841 $linkParams = [
842 'ADMCMD_prev' => $previewKeyword,
843 'id' => $uid
844 ];
845 return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams);
846 }
847
848 /**
849 * Generates a workspace splitted preview link.
850 *
851 * @param int $uid The ID of the record to be linked
852 * @param bool $addDomain Parameter to decide if domain should be added to the generated link, FALSE per default
853 * @return string the preview link without the trailing '/'
854 */
855 public function generateWorkspaceSplittedPreviewLink($uid, $addDomain = false)
856 {
857 // In case a $pageUid is submitted we need to make sure it points to a live-page
858 if ($uid > 0) {
859 $uid = $this->getLivePageUid($uid);
860 }
861 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
862 // the actual uid will be appended directly in BackendUtility Hook
863 $viewScript = $uriBuilder->buildUriFromRoute('workspace_previewcontrols', ['id' => '']);
864 if ($addDomain === true) {
865 $viewScript = $uriBuilder->buildUriFromRoute('workspace_previewcontrols', ['id' => $uid]);
866 return BackendUtility::getViewDomain($uid) . 'index.php?redirect_url=' . urlencode($viewScript);
867 }
868 return $viewScript;
869 }
870
871 /**
872 * Generate workspace preview links for all available languages of a page
873 *
874 * @param int $uid
875 * @return array
876 */
877 public function generateWorkspacePreviewLinksForAllLanguages($uid)
878 {
879 $previewUrl = $this->generateWorkspacePreviewLink($uid);
880 $previewLanguages = $this->getAvailableLanguages($uid);
881 $previewLinks = [];
882
883 foreach ($previewLanguages as $languageUid => $language) {
884 $previewLinks[$language] = $previewUrl . '&L=' . $languageUid;
885 }
886
887 return $previewLinks;
888 }
889
890 /**
891 * Find the Live-Uid for a given page,
892 * the results are cached at run-time to avoid too many database-queries
893 *
894 * @throws \InvalidArgumentException
895 * @param int $uid
896 * @return int
897 */
898 public function getLivePageUid($uid)
899 {
900 if (!isset($this->pageCache[$uid])) {
901 $pageRecord = BackendUtility::getRecord('pages', $uid);
902 if (is_array($pageRecord)) {
903 $this->pageCache[$uid] = $pageRecord['t3ver_oid'] ? $pageRecord['t3ver_oid'] : $uid;
904 } else {
905 throw new \InvalidArgumentException('uid is supposed to point to an existing page - given value was: ' . $uid, 1290628113);
906 }
907 }
908 return $this->pageCache[$uid];
909 }
910
911 /**
912 * Determines whether a page has workspace versions.
913 *
914 * @param int $workspaceId
915 * @param int $pageId
916 * @return bool
917 */
918 public function hasPageRecordVersions($workspaceId, $pageId)
919 {
920 if ((int)$workspaceId === 0 || (int)$pageId === 0) {
921 return false;
922 }
923
924 if (isset($this->versionsOnPageCache[$workspaceId][$pageId])) {
925 return $this->versionsOnPageCache[$workspaceId][$pageId];
926 }
927
928 $this->versionsOnPageCache[$workspaceId][$pageId] = false;
929
930 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
931 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
932 continue;
933 }
934
935 $pages = $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
936 // Early break on first match
937 if (!empty($pages[(string)$pageId])) {
938 $this->versionsOnPageCache[$workspaceId][$pageId] = true;
939 break;
940 }
941 }
942
943 $parameters = [
944 'workspaceId' => $workspaceId,
945 'pageId' => $pageId,
946 'versionsOnPageCache' => &$this->versionsOnPageCache,
947 ];
948 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'] ?? [] as $hookFunction) {
949 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
950 }
951
952 return $this->versionsOnPageCache[$workspaceId][$pageId];
953 }
954
955 /**
956 * Gets all pages that have workspace versions per table.
957 *
958 * Result:
959 * [
960 * 'sys_template' => [],
961 * 'tt_content' => [
962 * 1 => true,
963 * 11 => true,
964 * 13 => true,
965 * 15 => true
966 * ],
967 * 'tx_something => [
968 * 15 => true,
969 * 11 => true,
970 * 21 => true
971 * ],
972 * ]
973 *
974 * @param int $workspaceId
975 *
976 * @return array
977 */
978 public function getPagesWithVersionsInTable($workspaceId)
979 {
980 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
981 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
982 continue;
983 }
984
985 $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
986 }
987
988 return $this->pagesWithVersionsInTable[$workspaceId];
989 }
990
991 /**
992 * Gets all pages that have workspace versions in a particular table.
993 *
994 * Result:
995 * [
996 * 1 => true,
997 * 11 => true,
998 * 13 => true,
999 * 15 => true
1000 * ],
1001 *
1002 * @param int $workspaceId
1003 * @param string $tableName
1004 * @return array
1005 */
1006 protected function fetchPagesWithVersionsInTable($workspaceId, $tableName)
1007 {
1008 if ((int)$workspaceId === 0) {
1009 return [];
1010 }
1011
1012 if (!isset($this->pagesWithVersionsInTable[$workspaceId])) {
1013 $this->pagesWithVersionsInTable[$workspaceId] = [];
1014 }
1015
1016 if (!isset($this->pagesWithVersionsInTable[$workspaceId][$tableName])) {
1017 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = [];
1018
1019 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
1020 $queryBuilder->getRestrictions()
1021 ->removeAll()
1022 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1023
1024 $movePointerParameter = $queryBuilder->createNamedParameter(
1025 VersionState::MOVE_POINTER,
1026 \PDO::PARAM_INT
1027 );
1028 $workspaceIdParameter = $queryBuilder->createNamedParameter(
1029 $workspaceId,
1030 \PDO::PARAM_INT
1031 );
1032 $pageIdParameter = $queryBuilder->createNamedParameter(
1033 -1,
1034 \PDO::PARAM_INT
1035 );
1036 // create sub-queries, parameters are available for main query
1037 $versionQueryBuilder = $this->createQueryBuilderForTable($tableName)
1038 ->select('A.t3ver_oid')
1039 ->from($tableName, 'A')
1040 ->where(
1041 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
1042 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
1043 $queryBuilder->expr()->neq('A.t3ver_state', $movePointerParameter)
1044 );
1045 $movePointerQueryBuilder = $this->createQueryBuilderForTable($tableName)
1046 ->select('A.t3ver_oid')
1047 ->from($tableName, 'A')
1048 ->where(
1049 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
1050 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
1051 $queryBuilder->expr()->eq('A.t3ver_state', $movePointerParameter)
1052 );
1053 $subQuery = '%s IN (%s)';
1054 // execute main query
1055 $result = $queryBuilder
1056 ->select('B.pid AS pageId')
1057 ->from($tableName, 'B')
1058 ->orWhere(
1059 sprintf(
1060 $subQuery,
1061 $queryBuilder->quoteIdentifier('B.uid'),
1062 $versionQueryBuilder->getSQL()
1063 ),
1064 sprintf(
1065 $subQuery,
1066 $queryBuilder->quoteIdentifier('B.t3ver_move_id'),
1067 $movePointerQueryBuilder->getSQL()
1068 )
1069 )
1070 ->groupBy('B.pid')
1071 ->execute();
1072
1073 $pageIds = [];
1074 while ($row = $result->fetch()) {
1075 $pageIds[$row['pageId']] = true;
1076 }
1077
1078 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = $pageIds;
1079
1080 $parameters = [
1081 'workspaceId' => $workspaceId,
1082 'tableName' => $tableName,
1083 'pagesWithVersionsInTable' => &$this->pagesWithVersionsInTable,
1084 ];
1085 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['fetchPagesWithVersionsInTable'] ?? [] as $hookFunction) {
1086 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
1087 }
1088 }
1089
1090 return $this->pagesWithVersionsInTable[$workspaceId][$tableName];
1091 }
1092
1093 /**
1094 * @param string $tableName
1095 * @return QueryBuilder
1096 */
1097 protected function createQueryBuilderForTable(string $tableName)
1098 {
1099 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1100 ->getQueryBuilderForTable($tableName);
1101 $queryBuilder->getRestrictions()
1102 ->removeAll()
1103 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1104 return $queryBuilder;
1105 }
1106
1107 /**
1108 * @return \TYPO3\CMS\Extbase\Object\ObjectManager
1109 */
1110 protected function getObjectManager()
1111 {
1112 return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
1113 }
1114
1115 /**
1116 * Get the available languages of a certain page
1117 *
1118 * @param int $pageId
1119 * @return array
1120 */
1121 public function getAvailableLanguages($pageId)
1122 {
1123 $languageOptions = [];
1124 /** @var \TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider $translationConfigurationProvider */
1125 $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
1126 $systemLanguages = $translationConfigurationProvider->getSystemLanguages($pageId);
1127
1128 if ($GLOBALS['BE_USER']->checkLanguageAccess(0)) {
1129 // Use configured label for default language
1130 $languageOptions[0] = $systemLanguages[0]['title'];
1131 }
1132
1133 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1134 ->getQueryBuilderForTable('pages');
1135 $queryBuilder->getRestrictions()
1136 ->removeAll()
1137 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1138 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1139
1140 $result = $queryBuilder->select('sys_language_uid')
1141 ->from('pages')
1142 ->where(
1143 $queryBuilder->expr()->eq(
1144 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
1145 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1146 )
1147 )
1148 ->execute();
1149
1150 while ($row = $result->fetch()) {
1151 $languageId = (int)$row['sys_language_uid'];
1152 // Only add links to active languages the user has access to
1153 if (isset($systemLanguages[$languageId]) && $GLOBALS['BE_USER']->checkLanguageAccess($languageId)) {
1154 $languageOptions[$languageId] = $systemLanguages[$languageId]['title'];
1155 }
1156 }
1157
1158 return $languageOptions;
1159 }
1160 }