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