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