[BUGFIX] Correct type of argument passed to BackendUtility::viewOnClick
[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 } elseif ($isTableLocalizable) {
280 $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
281 }
282
283 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
284 $queryBuilder->getRestrictions()->removeAll()
285 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
286
287 $fields = ['A.uid', 'A.t3ver_oid', 'A.t3ver_stage', 'B.pid AS wspid', 'B.pid AS livepid'];
288 if ($isTableLocalizable) {
289 $fields[] = $languageParentField;
290 $fields[] = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
291 }
292 // Table A is the offline version and pid=-1 defines offline
293 // Table B (online) must have PID >= 0 to signify being online.
294 $constraints = [
295 $queryBuilder->expr()->eq(
296 'A.pid',
297 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
298 ),
299 $queryBuilder->expr()->gte(
300 'B.pid',
301 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
302 ),
303 $queryBuilder->expr()->neq(
304 'A.t3ver_state',
305 $queryBuilder->createNamedParameter(
306 (string)new VersionState(VersionState::MOVE_POINTER),
307 \PDO::PARAM_INT
308 )
309 )
310 ];
311
312 if ($pageList) {
313 $pidField = $table === 'pages' ? 'uid' : 'pid';
314 $constraints[] = $queryBuilder->expr()->in(
315 'B.' . $pidField,
316 $queryBuilder->createNamedParameter(
317 GeneralUtility::intExplode(',', $pageList, true),
318 Connection::PARAM_INT_ARRAY
319 )
320 );
321 }
322
323 if ($isTableLocalizable && MathUtility::canBeInterpretedAsInteger($language)) {
324 $constraints[] = $queryBuilder->expr()->eq(
325 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'],
326 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
327 );
328 }
329
330 // For "real" workspace numbers, select by that.
331 // If = -98, select all that are NOT online (zero).
332 // Anything else below -1 will not select on the wsid and therefore select all!
333 if ($wsid > self::SELECT_ALL_WORKSPACES) {
334 $constraints[] = $queryBuilder->expr()->eq(
335 'A.t3ver_wsid',
336 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
337 );
338 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
339 $constraints[] = $queryBuilder->expr()->neq(
340 'A.t3ver_wsid',
341 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
342 );
343 }
344
345 // lifecycle filter:
346 // 1 = select all drafts (never-published),
347 // 2 = select all published one or more times (archive/multiple)
348 if ($filter === 1) {
349 $constraints[] = $queryBuilder->expr()->eq(
350 'A.t3ver_count',
351 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
352 );
353 } elseif ($filter === 2) {
354 $constraints[] = $queryBuilder->expr()->gt(
355 'A.t3ver_count',
356 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
357 );
358 }
359
360 if ((int)$stage !== -99) {
361 $constraints[] = $queryBuilder->expr()->eq(
362 'A.t3ver_stage',
363 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
364 );
365 }
366
367 // ... and finally the join between the two tables.
368 $constraints[] = $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'));
369
370 // Select all records from this table in the database from the workspace
371 // This joins the online version with the offline version as tables A and B
372 // Order by UID, mostly to have a sorting in the backend overview module which
373 // doesn't "jump around" when swapping.
374 $rows = $queryBuilder->select(...$fields)
375 ->from($table, 'A')
376 ->from($table, 'B')
377 ->where(...$constraints)
378 ->orderBy('B.uid')
379 ->execute()
380 ->fetchAll();
381
382 return $rows;
383 }
384
385 /**
386 * Find all moved records at their new position.
387 *
388 * @param string $table
389 * @param string $pageList
390 * @param int $wsid
391 * @param int $filter
392 * @param int $stage
393 * @return array
394 */
395 protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage)
396 {
397 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
398 $queryBuilder->getRestrictions()->removeAll()
399 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
400
401 // Aliases:
402 // A - moveTo placeholder
403 // B - online record
404 // C - moveFrom placeholder
405 $constraints = [
406 $queryBuilder->expr()->eq(
407 'A.t3ver_state',
408 $queryBuilder->createNamedParameter(
409 (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
410 \PDO::PARAM_INT
411 )
412 ),
413 $queryBuilder->expr()->gt(
414 'B.pid',
415 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
416 ),
417 $queryBuilder->expr()->eq(
418 'B.t3ver_state',
419 $queryBuilder->createNamedParameter(
420 (string)new VersionState(VersionState::DEFAULT_STATE),
421 \PDO::PARAM_INT
422 )
423 ),
424 $queryBuilder->expr()->eq(
425 'B.t3ver_wsid',
426 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
427 ),
428 $queryBuilder->expr()->eq(
429 'C.pid',
430 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
431 ),
432 $queryBuilder->expr()->eq(
433 'C.t3ver_state',
434 $queryBuilder->createNamedParameter(
435 (string)new VersionState(VersionState::MOVE_POINTER),
436 \PDO::PARAM_INT
437 )
438 ),
439 $queryBuilder->expr()->eq('A.t3ver_move_id', $queryBuilder->quoteIdentifier('B.uid')),
440 $queryBuilder->expr()->eq('B.uid', $queryBuilder->quoteIdentifier('C.t3ver_oid'))
441 ];
442
443 if ($wsid > self::SELECT_ALL_WORKSPACES) {
444 $constraints[] = $queryBuilder->expr()->eq(
445 'A.t3ver_wsid',
446 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
447 );
448 $constraints[] = $queryBuilder->expr()->eq(
449 'C.t3ver_wsid',
450 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
451 );
452 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
453 $constraints[] = $queryBuilder->expr()->neq(
454 'A.t3ver_wsid',
455 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
456 );
457 $constraints[] = $queryBuilder->expr()->neq(
458 'C.t3ver_wsid',
459 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
460 );
461 }
462
463 // lifecycle filter:
464 // 1 = select all drafts (never-published),
465 // 2 = select all published one or more times (archive/multiple)
466 if ($filter === 1) {
467 $constraints[] = $queryBuilder->expr()->eq(
468 'C.t3ver_count',
469 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
470 );
471 } elseif ($filter === 2) {
472 $constraints[] = $queryBuilder->expr()->gt(
473 'C.t3ver_count',
474 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
475 );
476 }
477
478 if ((int)$stage != -99) {
479 $constraints[] = $queryBuilder->expr()->eq(
480 'C.t3ver_stage',
481 $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
482 );
483 }
484
485 if ($pageList) {
486 $pidField = $table === 'pages' ? 'B.uid' : 'A.pid';
487 $constraints[] = $queryBuilder->expr()->in(
488 $pidField,
489 $queryBuilder->createNamedParameter(
490 GeneralUtility::intExplode(',', $pageList, true),
491 Connection::PARAM_INT_ARRAY
492 )
493 );
494 }
495
496 $rows = $queryBuilder
497 ->select('A.pid AS wspid', 'B.uid AS t3ver_oid', 'C.uid AS uid', 'B.pid AS livepid')
498 ->from($table, 'A')
499 ->from($table, 'B')
500 ->from($table, 'C')
501 ->where(...$constraints)
502 ->orderBy('A.uid')
503 ->execute()
504 ->fetchAll();
505
506 return $rows;
507 }
508
509 /**
510 * Find all page uids recursive starting from a specific page
511 *
512 * @param int $pageId
513 * @param int $wsid
514 * @param int $recursionLevel
515 * @return string Comma sep. uid list
516 */
517 protected function getTreeUids($pageId, $wsid, $recursionLevel)
518 {
519 // Reusing existing functionality with the drawback that
520 // mount points are not covered yet
521 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
522 /** @var $searchObj \TYPO3\CMS\Core\Database\QueryView */
523 $searchObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\QueryView::class);
524 if ($pageId > 0) {
525 $pageList = $searchObj->getTreeList($pageId, $recursionLevel, 0, $perms_clause);
526 } else {
527 $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
528 if (!is_array($mountPoints) || empty($mountPoints)) {
529 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
530 $mountPoints = array_unique($mountPoints);
531 }
532 $newList = [];
533 foreach ($mountPoints as $mountPoint) {
534 $newList[] = $searchObj->getTreeList($mountPoint, $recursionLevel, 0, $perms_clause);
535 }
536 $pageList = implode(',', $newList);
537 }
538 unset($searchObj);
539
540 if (BackendUtility::isTableWorkspaceEnabled('pages') && $pageList) {
541 // Remove the "subbranch" if a page was moved away
542 $pageIds = GeneralUtility::intExplode(',', $pageList, true);
543 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
544 $queryBuilder->getRestrictions()
545 ->removeAll()
546 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
547 $result = $queryBuilder
548 ->select('uid', 'pid', 't3ver_move_id')
549 ->from('pages')
550 ->where(
551 $queryBuilder->expr()->in(
552 't3ver_move_id',
553 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
554 ),
555 $queryBuilder->expr()->eq(
556 't3ver_wsid',
557 $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
558 )
559 )
560 ->orderBy('uid')
561 ->execute();
562
563 $movedAwayPages = [];
564 while ($row = $result->fetch()) {
565 $movedAwayPages[$row['t3ver_move_id']] = $row;
566 }
567
568 // move all pages away
569 $newList = array_diff($pageIds, array_keys($movedAwayPages));
570 // keep current page in the list
571 $newList[] = $pageId;
572 // move back in if still connected to the "remaining" pages
573 do {
574 $changed = false;
575 foreach ($movedAwayPages as $uid => $rec) {
576 if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) {
577 $newList[] = $uid;
578 $changed = true;
579 }
580 }
581 } while ($changed);
582
583 // In case moving pages is enabled we need to replace all move-to pointer with their origin
584 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
585 $queryBuilder->getRestrictions()
586 ->removeAll()
587 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
588 $result = $queryBuilder->select('uid', 't3ver_move_id')
589 ->from('pages')
590 ->where(
591 $queryBuilder->expr()->in(
592 'uid',
593 $queryBuilder->createNamedParameter($newList, Connection::PARAM_INT_ARRAY)
594 )
595 )
596 ->orderBy('uid')
597 ->execute();
598
599 $pages = [];
600 while ($row = $result->fetch()) {
601 $pages[$row['uid']] = $row;
602 }
603
604 $pageIds = $newList;
605 if (!in_array($pageId, $pageIds)) {
606 $pageIds[] = $pageId;
607 }
608
609 $newList = [];
610 foreach ($pageIds as $pageId) {
611 if ((int)$pages[$pageId]['t3ver_move_id'] > 0) {
612 $newList[] = (int)$pages[$pageId]['t3ver_move_id'];
613 } else {
614 $newList[] = $pageId;
615 }
616 }
617 $pageList = implode(',', $newList);
618 }
619
620 return $pageList;
621 }
622
623 /**
624 * Remove all records which are not permitted for the user
625 *
626 * @param array $recs
627 * @param string $table
628 * @return array
629 */
630 protected function filterPermittedElements($recs, $table)
631 {
632 $permittedElements = [];
633 if (is_array($recs)) {
634 foreach ($recs as $rec) {
635 if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) {
636 $permittedElements[] = $rec;
637 }
638 }
639 }
640 return $permittedElements;
641 }
642
643 /**
644 * Checking access to the page the record is on, respecting ignored root level restrictions
645 *
646 * @param string $table Name of the table
647 * @param array $record Record row to be checked
648 * @return bool
649 */
650 protected function isPageAccessibleForCurrentUser($table, array $record)
651 {
652 $pageIdField = $table === 'pages' ? 'uid' : 'wspid';
653 $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : null;
654 if ($pageId === null) {
655 return false;
656 }
657 if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) {
658 return true;
659 }
660 $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody');
661
662 return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1);
663 }
664
665 /**
666 * Check current be users language access on given record.
667 *
668 * @param string $table Name of the table
669 * @param array $record Record row to be checked
670 * @return bool
671 */
672 protected function isLanguageAccessibleForCurrentUser($table, array $record)
673 {
674 if (BackendUtility::isTableLocalizable($table)) {
675 $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
676 } else {
677 return true;
678 }
679 return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid);
680 }
681
682 /**
683 * Determine whether a specific page is new and not yet available in the LIVE workspace
684 *
685 * @param int $id Primary key of the page to check
686 * @param int $language Language for which to check the page
687 * @return bool
688 */
689 public static function isNewPage($id, $language = 0)
690 {
691 $isNewPage = false;
692 // If the language is not default, check state of overlay
693 if ($language > 0) {
694 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
695 ->getQueryBuilderForTable('pages_language_overlay');
696 $queryBuilder->getRestrictions()
697 ->removeAll()
698 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
699 $row = $queryBuilder->select('t3ver_state')
700 ->from('pages_language_overlay')
701 ->where(
702 $queryBuilder->expr()->eq(
703 'pid',
704 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
705 ),
706 $queryBuilder->expr()->eq(
707 $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'],
708 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
709 ),
710 $queryBuilder->expr()->eq(
711 't3ver_wsid',
712 $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT)
713 )
714 )
715 ->setMaxResults(1)
716 ->execute()
717 ->fetch();
718
719 if ($row !== false) {
720 $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
721 }
722 } else {
723 $rec = BackendUtility::getRecord('pages', $id, 't3ver_state');
724 if (is_array($rec)) {
725 $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
726 }
727 }
728 return $isNewPage;
729 }
730
731 /**
732 * Generates a view link for a page.
733 *
734 * @param string $table Table to be used
735 * @param int $uid Uid of the version(!) record
736 * @param array $liveRecord Optional live record data
737 * @param array $versionRecord Optional version record data
738 * @return string
739 */
740 public static function viewSingleRecord($table, $uid, array $liveRecord = null, array $versionRecord = null)
741 {
742 if ($table === 'pages') {
743 return BackendUtility::viewOnClick(BackendUtility::getLiveVersionIdOfRecord('pages', $uid));
744 }
745
746 if ($liveRecord === null) {
747 $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
748 }
749 if ($versionRecord === null) {
750 $versionRecord = BackendUtility::getRecord($table, $uid);
751 }
752 if (VersionState::cast($versionRecord['t3ver_state'])->equals(VersionState::MOVE_POINTER)) {
753 $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRecord['uid'], 'pid');
754 }
755
756 // Directly use pid value and consider move placeholders
757 $previewPageId = (empty($movePlaceholder['pid']) ? $liveRecord['pid'] : $movePlaceholder['pid']);
758 $additionalParameters = '&tx_workspaces_web_workspacesworkspaces[previewWS]=' . $versionRecord['t3ver_wsid'];
759 // Add language parameter if record is a localization
760 if (BackendUtility::isTableLocalizable($table)) {
761 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
762 if ($versionRecord[$languageField] > 0) {
763 $additionalParameters .= '&L=' . $versionRecord[$languageField];
764 }
765 }
766
767 $pageTsConfig = BackendUtility::getPagesTSconfig($previewPageId);
768 $viewUrl = '';
769
770 // Directly use determined direct page id
771 if ($table === 'pages_language_overlay' || $table === 'tt_content') {
772 $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters);
773 // Analyze Page TSconfig options.workspaces.previewPageId
774 } elseif (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table]) || !empty($pageTsConfig['options.']['workspaces.']['previewPageId'])) {
775 if (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table])) {
776 $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId.'][$table];
777 } else {
778 $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId'];
779 }
780 // Extract possible settings (e.g. "field:pid")
781 list($previewKey, $previewValue) = explode(':', $previewConfiguration, 2);
782 if ($previewKey === 'field') {
783 $previewPageId = (int)$liveRecord[$previewValue];
784 } else {
785 $previewPageId = (int)$previewConfiguration;
786 }
787 $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters);
788 // Call user function to render the single record view
789 } elseif (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'])) {
790 $_params = [
791 'table' => $table,
792 'uid' => $uid,
793 'record' => $liveRecord,
794 'liveRecord' => $liveRecord,
795 'versionRecord' => $versionRecord,
796 ];
797 $_funcRef = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'];
798 $null = null;
799 $viewUrl = GeneralUtility::callUserFunction($_funcRef, $_params, $null);
800 }
801
802 return $viewUrl;
803 }
804
805 /**
806 * Determine whether this page for the current
807 *
808 * @param int $pageUid
809 * @param int $workspaceUid
810 * @return bool
811 */
812 public function canCreatePreviewLink($pageUid, $workspaceUid)
813 {
814 $result = true;
815 if ($pageUid > 0 && $workspaceUid > 0) {
816 $pageRecord = BackendUtility::getRecord('pages', $pageUid);
817 BackendUtility::workspaceOL('pages', $pageRecord, $workspaceUid);
818 if (
819 !GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['FE']['content_doktypes'], $pageRecord['doktype'])
820 || VersionState::cast($pageRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
821 ) {
822 $result = false;
823 }
824 } else {
825 $result = false;
826 }
827 return $result;
828 }
829
830 /**
831 * Generates a workspace preview link.
832 *
833 * @param int $uid The ID of the record to be linked
834 * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
835 */
836 public function generateWorkspacePreviewLink($uid)
837 {
838 $previewObject = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Hook\PreviewHook::class);
839 $timeToLiveHours = $previewObject->getPreviewLinkLifetime();
840 $previewKeyword = $previewObject->compilePreviewKeyword('', $GLOBALS['BE_USER']->user['uid'], $timeToLiveHours * 3600, $this->getCurrentWorkspace());
841 $linkParams = [
842 'ADMCMD_prev' => $previewKeyword,
843 'id' => $uid
844 ];
845 return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams);
846 }
847
848 /**
849 * Generates a workspace splitted preview link.
850 *
851 * @param int $uid The ID of the record to be linked
852 * @param bool $addDomain Parameter to decide if domain should be added to the generated link, FALSE per default
853 * @return string the preview link without the trailing '/'
854 */
855 public function generateWorkspaceSplittedPreviewLink($uid, $addDomain = false)
856 {
857 // In case a $pageUid is submitted we need to make sure it points to a live-page
858 if ($uid > 0) {
859 $uid = $this->getLivePageUid($uid);
860 }
861 /** @var $uriBuilder \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder */
862 $uriBuilder = $this->getObjectManager()->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
863 $redirect = 'index.php?redirect_url=';
864 // @todo this should maybe be changed so that the extbase URI Builder can deal with module names directly
865 $originalM = GeneralUtility::_GET('M');
866 GeneralUtility::_GETset('web_WorkspacesWorkspaces', 'M');
867 $viewScript = $uriBuilder->uriFor('index', [], 'Preview', 'workspaces', 'web_workspacesworkspaces') . '&id=';
868 GeneralUtility::_GETset($originalM, 'M');
869 if ($addDomain === true) {
870 return BackendUtility::getViewDomain($uid) . $redirect . urlencode($viewScript) . $uid;
871 } else {
872 return $viewScript;
873 }
874 }
875
876 /**
877 * Generate workspace preview links for all available languages of a page
878 *
879 * @param int $uid
880 * @return array
881 */
882 public function generateWorkspacePreviewLinksForAllLanguages($uid)
883 {
884 $previewUrl = $this->generateWorkspacePreviewLink($uid);
885 $previewLanguages = $this->getAvailableLanguages($uid);
886 $previewLinks = [];
887
888 foreach ($previewLanguages as $languageUid => $language) {
889 $previewLinks[$language] = $previewUrl . '&L=' . $languageUid;
890 }
891
892 return $previewLinks;
893 }
894
895 /**
896 * Find the Live-Uid for a given page,
897 * the results are cached at run-time to avoid too many database-queries
898 *
899 * @throws \InvalidArgumentException
900 * @param int $uid
901 * @return int
902 */
903 public function getLivePageUid($uid)
904 {
905 if (!isset($this->pageCache[$uid])) {
906 $pageRecord = BackendUtility::getRecord('pages', $uid);
907 if (is_array($pageRecord)) {
908 $this->pageCache[$uid] = $pageRecord['t3ver_oid'] ? $pageRecord['t3ver_oid'] : $uid;
909 } else {
910 throw new \InvalidArgumentException('uid is supposed to point to an existing page - given value was: ' . $uid, 1290628113);
911 }
912 }
913 return $this->pageCache[$uid];
914 }
915
916 /**
917 * Determines whether a page has workspace versions.
918 *
919 * @param int $workspaceId
920 * @param int $pageId
921 * @return bool
922 */
923 public function hasPageRecordVersions($workspaceId, $pageId)
924 {
925 if ((int)$workspaceId === 0 || (int)$pageId === 0) {
926 return false;
927 }
928
929 if (isset($this->versionsOnPageCache[$workspaceId][$pageId])) {
930 return $this->versionsOnPageCache[$workspaceId][$pageId];
931 }
932
933 $this->versionsOnPageCache[$workspaceId][$pageId] = false;
934
935 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
936 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
937 continue;
938 }
939
940 $pages = $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
941 // Early break on first match
942 if (!empty($pages[(string)$pageId])) {
943 $this->versionsOnPageCache[$workspaceId][$pageId] = true;
944 break;
945 }
946 }
947
948 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'])
949 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'])) {
950 $parameters = [
951 'workspaceId' => $workspaceId,
952 'pageId' => $pageId,
953 'versionsOnPageCache' => &$this->versionsOnPageCache,
954 ];
955 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'] as $hookFunction) {
956 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
957 }
958 }
959
960 return $this->versionsOnPageCache[$workspaceId][$pageId];
961 }
962
963 /**
964 * Gets all pages that have workspace versions per table.
965 *
966 * Result:
967 * [
968 * 'tt_content' => [
969 * 1 => 1,
970 * 11 => 11,
971 * 13 => 13,
972 * 15 => 15
973 * ],
974 * 'tx_something => [
975 * 15 => 15,
976 * 11 => 11,
977 * 21 => 21
978 * ],
979 * ]
980 *
981 * @param int $workspaceId
982 * @return array
983 */
984 public function getPagesWithVersionsInTable($workspaceId)
985 {
986 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
987 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
988 continue;
989 }
990
991 $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
992 }
993
994 return $this->pagesWithVersionsInTable[$workspaceId];
995 }
996
997 /**
998 * Gets all pages that have workspace versions in a particular table.
999 *
1000 * Result:
1001 * [
1002 * 1 => 1,
1003 * 11 => 11,
1004 * 13 => 13,
1005 * 15 => 15
1006 * ],
1007 *
1008 * @param int $workspaceId
1009 * @param string $tableName
1010 * @return array
1011 */
1012 protected function fetchPagesWithVersionsInTable($workspaceId, $tableName)
1013 {
1014 if ((int)$workspaceId === 0) {
1015 return [];
1016 }
1017
1018 if (!isset($this->pagesWithVersionsInTable[$workspaceId])) {
1019 $this->pagesWithVersionsInTable[$workspaceId] = [];
1020 }
1021
1022 if (!isset($this->pagesWithVersionsInTable[$workspaceId][$tableName])) {
1023 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = [];
1024
1025 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
1026 $queryBuilder->getRestrictions()
1027 ->removeAll()
1028 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1029
1030 $movePointerParameter = $queryBuilder->createNamedParameter(
1031 VersionState::MOVE_POINTER,
1032 \PDO::PARAM_INT
1033 );
1034 $workspaceIdParameter = $queryBuilder->createNamedParameter(
1035 $workspaceId,
1036 \PDO::PARAM_INT
1037 );
1038 $pageIdParameter = $queryBuilder->createNamedParameter(
1039 -1,
1040 \PDO::PARAM_INT
1041 );
1042 // create sub-queries, parameters are available for main query
1043 $versionQueryBuilder = $this->createQueryBuilderForTable($tableName)
1044 ->select('A.t3ver_oid')
1045 ->from($tableName, 'A')
1046 ->where(
1047 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
1048 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
1049 $queryBuilder->expr()->neq('A.t3ver_state', $movePointerParameter)
1050 );
1051 $movePointerQueryBuilder = $this->createQueryBuilderForTable($tableName)
1052 ->select('A.t3ver_oid')
1053 ->from($tableName, 'A')
1054 ->where(
1055 $queryBuilder->expr()->eq('A.pid', $pageIdParameter),
1056 $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
1057 $queryBuilder->expr()->eq('A.t3ver_state', $movePointerParameter)
1058 );
1059 $subQuery = '%s IN (%s)';
1060 // execute main query
1061 $result = $queryBuilder
1062 ->select('B.pid AS pageId')
1063 ->from($tableName, 'B')
1064 ->orWhere(
1065 sprintf(
1066 $subQuery,
1067 $queryBuilder->quoteIdentifier('B.uid'),
1068 $versionQueryBuilder->getSQL()
1069 ),
1070 sprintf(
1071 $subQuery,
1072 $queryBuilder->quoteIdentifier('B.t3ver_move_id'),
1073 $movePointerQueryBuilder->getSQL()
1074 )
1075 )
1076 ->groupBy('pageId')
1077 ->execute();
1078
1079 $pageIds = [];
1080 while ($row = $result->fetch()) {
1081 $pageIds[$row['pageId']] = true;
1082 }
1083
1084 $this->pagesWithVersionsInTable[$workspaceId][$tableName] = $pageIds;
1085
1086 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['fetchPagesWithVersionsInTable'])
1087 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['fetchPagesWithVersionsInTable'])) {
1088 $parameters = [
1089 'workspaceId' => $workspaceId,
1090 'tableName' => $tableName,
1091 'pagesWithVersionsInTable' => &$this->pagesWithVersionsInTable,
1092 ];
1093 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['fetchPagesWithVersionsInTable'] as $hookFunction) {
1094 GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
1095 }
1096 }
1097 }
1098
1099 return $this->pagesWithVersionsInTable[$workspaceId][$tableName];
1100 }
1101
1102 /**
1103 * @param string $tableName
1104 * @return QueryBuilder
1105 */
1106 protected function createQueryBuilderForTable(string $tableName)
1107 {
1108 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1109 ->getQueryBuilderForTable($tableName);
1110 $queryBuilder->getRestrictions()
1111 ->removeAll()
1112 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1113 return $queryBuilder;
1114 }
1115
1116 /**
1117 * @return \TYPO3\CMS\Extbase\Object\ObjectManager
1118 */
1119 protected function getObjectManager()
1120 {
1121 return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
1122 }
1123
1124 /**
1125 * Get the available languages of a certain page
1126 *
1127 * @param int $pageId
1128 * @return array
1129 */
1130 public function getAvailableLanguages($pageId)
1131 {
1132 $languageOptions = [];
1133 /** @var \TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider $translationConfigurationProvider */
1134 $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
1135 $systemLanguages = $translationConfigurationProvider->getSystemLanguages($pageId);
1136
1137 if ($GLOBALS['BE_USER']->checkLanguageAccess(0)) {
1138 // Use configured label for default language
1139 $languageOptions[0] = $systemLanguages[0]['title'];
1140 }
1141
1142 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1143 ->getQueryBuilderForTable('pages_language_overlay');
1144 $queryBuilder->getRestrictions()
1145 ->removeAll()
1146 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1147 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1148
1149 $result = $queryBuilder->select('sys_language_uid')
1150 ->from('pages_language_overlay')
1151 ->where(
1152 $queryBuilder->expr()->eq(
1153 'pid',
1154 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1155 )
1156 )
1157 ->execute();
1158
1159 while ($row = $result->fetch()) {
1160 $languageId = (int)$row['sys_language_uid'];
1161 // Only add links to active languages the user has access to
1162 if (isset($systemLanguages[$languageId]) && $GLOBALS['BE_USER']->checkLanguageAccess($languageId)) {
1163 $languageOptions[$languageId] = $systemLanguages[$languageId]['title'];
1164 }
1165 }
1166
1167 return $languageOptions;
1168 }
1169 }