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