[BUGFIX] Workspace preview links for multiple languages
[Packages/TYPO3.CMS.git] / typo3 / sysext / workspaces / Classes / Service / WorkspaceService.php
1 <?php
2 namespace TYPO3\CMS\Workspaces\Service;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\DatabaseConnection;
19 use TYPO3\CMS\Core\SingletonInterface;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Versioning\VersionState;
22
23 /**
24 * Workspace service
25 */
26 class WorkspaceService implements SingletonInterface {
27
28 /**
29 * @var array
30 */
31 protected $pageCache = array();
32
33 /**
34 * @var array
35 */
36 protected $versionsOnPageCache = array();
37
38 const TABLE_WORKSPACE = 'sys_workspace';
39 const SELECT_ALL_WORKSPACES = -98;
40 const LIVE_WORKSPACE_ID = 0;
41 /**
42 * retrieves the available workspaces from the database and checks whether
43 * they're available to the current BE user
44 *
45 * @return array array of worspaces available to the current user
46 */
47 public function getAvailableWorkspaces() {
48 $availableWorkspaces = array();
49 // add default workspaces
50 if ($GLOBALS['BE_USER']->checkWorkspace(array('uid' => (string)self::LIVE_WORKSPACE_ID))) {
51 $availableWorkspaces[self::LIVE_WORKSPACE_ID] = self::getWorkspaceTitle(self::LIVE_WORKSPACE_ID);
52 }
53 // add custom workspaces (selecting all, filtering by BE_USER check):
54 $customWorkspaces = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid, title, adminusers, members', 'sys_workspace', 'pid = 0' . BackendUtility::deleteClause('sys_workspace'), '', 'title');
55 if (!empty($customWorkspaces)) {
56 foreach ($customWorkspaces as $workspace) {
57 if ($GLOBALS['BE_USER']->checkWorkspace($workspace)) {
58 $availableWorkspaces[$workspace['uid']] = $workspace['title'];
59 }
60 }
61 }
62 return $availableWorkspaces;
63 }
64
65 /**
66 * Gets the current workspace ID.
67 *
68 * @return int The current workspace ID
69 */
70 public function getCurrentWorkspace() {
71 $workspaceId = $GLOBALS['BE_USER']->workspace;
72 $activeId = $GLOBALS['BE_USER']->getSessionData('tx_workspace_activeWorkspace');
73
74 // Avoid invalid workspace settings
75 if ($activeId !== NULL && $activeId !== self::SELECT_ALL_WORKSPACES) {
76 $availableWorkspaces = $this->getAvailableWorkspaces();
77 if (!isset($availableWorkspaces[$activeId])) {
78 $activeId = NULL;
79 }
80 }
81
82 if ($activeId !== NULL) {
83 $workspaceId = $activeId;
84 }
85
86 return $workspaceId;
87 }
88
89 /**
90 * Find the title for the requested workspace.
91 *
92 * @param int $wsId
93 * @return string
94 * @throws \InvalidArgumentException
95 */
96 static public function getWorkspaceTitle($wsId) {
97 $title = FALSE;
98 switch ($wsId) {
99 case self::LIVE_WORKSPACE_ID:
100 $title = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_misc.xlf:shortcut_onlineWS');
101 break;
102 default:
103 $labelField = $GLOBALS['TCA']['sys_workspace']['ctrl']['label'];
104 $wsRecord = BackendUtility::getRecord('sys_workspace', $wsId, 'uid,' . $labelField);
105 if (is_array($wsRecord)) {
106 $title = $wsRecord[$labelField];
107 }
108 }
109 if ($title === FALSE) {
110 throw new \InvalidArgumentException('No such workspace defined');
111 }
112 return $title;
113 }
114
115 /**
116 * Building tcemain CMD-array for swapping all versions in a workspace.
117 *
118 * @param int Real workspace ID, cannot be ONLINE (zero).
119 * @param bool If set, then the currently online versions are swapped into the workspace in exchange for the offline versions. Otherwise the workspace is emptied.
120 * @param int $pageId The page id
121 * @param int $language Select specific language only
122 * @return array Command array for tcemain
123 */
124 public function getCmdArrayForPublishWS($wsid, $doSwap, $pageId = 0, $language = NULL) {
125 $wsid = (int)$wsid;
126 $cmd = array();
127 if ($wsid >= -1 && $wsid !== 0) {
128 // Define stage to select:
129 $stage = -99;
130 if ($wsid > 0) {
131 $workspaceRec = BackendUtility::getRecord('sys_workspace', $wsid);
132 if ($workspaceRec['publish_access'] & 1) {
133 $stage = \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_ID;
134 }
135 }
136 // Select all versions to swap:
137 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 0, 'tables_modify', $language);
138 // Traverse the selection to build CMD array:
139 foreach ($versions as $table => $records) {
140 foreach ($records as $rec) {
141 // Build the cmd Array:
142 $cmd[$table][$rec['t3ver_oid']]['version'] = array('action' => 'swap', 'swapWith' => $rec['uid'], 'swapIntoWS' => $doSwap ? 1 : 0);
143 }
144 }
145 }
146 return $cmd;
147 }
148
149 /**
150 * Building tcemain CMD-array for releasing all versions in a workspace.
151 *
152 * @param int Real workspace ID, cannot be ONLINE (zero).
153 * @param bool Run Flush (TRUE) or ClearWSID (FALSE) command
154 * @param int $pageId The page id
155 * @param int $language Select specific language only
156 * @return array Command array for tcemain
157 */
158 public function getCmdArrayForFlushWS($wsid, $flush = TRUE, $pageId = 0, $language = NULL) {
159 $wsid = (int)$wsid;
160 $cmd = array();
161 if ($wsid >= -1 && $wsid !== 0) {
162 // Define stage to select:
163 $stage = -99;
164 // Select all versions to swap:
165 $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 0, 'tables_modify', $language);
166 // Traverse the selection to build CMD array:
167 foreach ($versions as $table => $records) {
168 foreach ($records as $rec) {
169 // Build the cmd Array:
170 $cmd[$table][$rec['uid']]['version'] = array('action' => $flush ? 'flush' : 'clearWSID');
171 }
172 }
173 }
174 return $cmd;
175 }
176
177 /**
178 * Select all records from workspace pending for publishing
179 * Used from backend to display workspace overview
180 * User for auto-publishing for selecting versions for publication
181 *
182 * @param int 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
183 * @param int Lifecycle filter: 1 = select all drafts (never-published), 2 = select all published one or more times (archive/multiple), anything else selects all.
184 * @param int Stage filter: -99 means no filtering, otherwise it will be used to select only elements with that stage. For publishing, that would be "10
185 * @param int Page id: Live page for which to find versions in workspace!
186 * @param int Recursion Level - select versions recursive - parameter is only relevant if $pageId != -1
187 * @param string 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.
188 * @param int $language Select specific language only
189 * @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
190 */
191 public function selectVersionsInWorkspace($wsid, $filter = 0, $stage = -99, $pageId = -1, $recursionLevel = 0, $selectionType = 'tables_select', $language = NULL) {
192 $wsid = (int)$wsid;
193 $filter = (int)$filter;
194 $output = array();
195 // Contains either nothing or a list with live-uids
196 if ($pageId != -1 && $recursionLevel > 0) {
197 $pageList = $this->getTreeUids($pageId, $wsid, $recursionLevel);
198 } elseif ($pageId != -1) {
199 $pageList = $pageId;
200 } else {
201 $pageList = '';
202 // check if person may only see a "virtual" page-root
203 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
204 $mountPoints = array_unique($mountPoints);
205 if (!in_array(0, $mountPoints)) {
206 $tempPageIds = array();
207 foreach ($mountPoints as $mountPoint) {
208 $tempPageIds[] = $this->getTreeUids($mountPoint, $wsid, $recursionLevel);
209 }
210 $pageList = implode(',', $tempPageIds);
211 }
212 }
213 // Traversing all tables supporting versioning:
214 foreach ($GLOBALS['TCA'] as $table => $cfg) {
215 // we do not collect records from tables without permissions on them.
216 if (!$GLOBALS['BE_USER']->check($selectionType, $table)) {
217 continue;
218 }
219 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
220 $recs = $this->selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language);
221 if ((int)$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] === 2) {
222 $moveRecs = $this->getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage);
223 $recs = array_merge($recs, $moveRecs);
224 }
225 $recs = $this->filterPermittedElements($recs, $table);
226 if (!empty($recs)) {
227 $output[$table] = $recs;
228 }
229 }
230 }
231 return $output;
232 }
233
234 /**
235 * Find all versionized elements except moved records.
236 *
237 * @param string $table
238 * @param string $pageList
239 * @param int $wsid
240 * @param int $filter
241 * @param int $stage
242 * @param int $language
243 * @return array
244 */
245 protected function selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language = NULL) {
246 // Include root level page as there might be some records with where root level restriction is ignored (e.g. FAL records)
247 if ($pageList !== '' && BackendUtility::isRootLevelRestrictionIgnored($table)) {
248 $pageList .= ',0';
249 }
250 $isTableLocalizable = BackendUtility::isTableLocalizable($table);
251 $languageParentField = '';
252 // If table is not localizable, but localized reocrds shall
253 // be collected, an empty result array needs to be returned:
254 if ($isTableLocalizable === FALSE && $language > 0) {
255 return array();
256 } elseif ($isTableLocalizable) {
257 $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . ', ';
258 }
259 $fields = 'A.uid, A.t3ver_oid, A.t3ver_stage, ' . $languageParentField . 'B.pid AS wspid, B.pid AS livepid';
260 if ($isTableLocalizable) {
261 $fields .= ', A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
262 }
263 $from = $table . ' A,' . $table . ' B';
264 // Table A is the offline version and pid=-1 defines offline
265 $where = 'A.pid=-1 AND A.t3ver_state!=' . new VersionState(VersionState::MOVE_POINTER);
266 if ($pageList) {
267 $pidField = $table === 'pages' ? 'uid' : 'pid';
268 $pidConstraint = strstr($pageList, ',') ? ' IN (' . $pageList . ')' : '=' . $pageList;
269 $where .= ' AND B.' . $pidField . $pidConstraint;
270 }
271 if ($isTableLocalizable && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($language)) {
272 $where .= ' AND A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '=' . $language;
273 }
274 // For "real" workspace numbers, select by that.
275 // If = -98, select all that are NOT online (zero).
276 // Anything else below -1 will not select on the wsid and therefore select all!
277 if ($wsid > self::SELECT_ALL_WORKSPACES) {
278 $where .= ' AND A.t3ver_wsid=' . $wsid;
279 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
280 $where .= ' AND A.t3ver_wsid!=0';
281 }
282 // lifecycle filter:
283 // 1 = select all drafts (never-published),
284 // 2 = select all published one or more times (archive/multiple)
285 if ($filter === 1 || $filter === 2) {
286 $where .= ' AND A.t3ver_count ' . ($filter === 1 ? '= 0' : '> 0');
287 }
288 if ($stage != -99) {
289 $where .= ' AND A.t3ver_stage=' . (int)$stage;
290 }
291 // Table B (online) must have PID >= 0 to signify being online.
292 $where .= ' AND B.pid>=0';
293 // ... and finally the join between the two tables.
294 $where .= ' AND A.t3ver_oid=B.uid';
295 $where .= BackendUtility::deleteClause($table, 'A');
296 $where .= BackendUtility::deleteClause($table, 'B');
297 // Select all records from this table in the database from the workspace
298 // This joins the online version with the offline version as tables A and B
299 // Order by UID, mostly to have a sorting in the backend overview module which doesn't "jump around" when swapping.
300 $res = $this->getDatabaseConnection()->exec_SELECTgetRows($fields, $from, $where, '', 'B.uid');
301 return is_array($res) ? $res : array();
302 }
303
304 /**
305 * Find all moved records at their new position.
306 *
307 * @param string $table
308 * @param string $pageList
309 * @param int $wsid
310 * @param int $filter
311 * @param int $stage
312 * @return array
313 */
314 protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage) {
315 // Aliases:
316 // A - moveTo placeholder
317 // B - online record
318 // C - moveFrom placeholder
319 $fields = 'A.pid AS wspid, B.uid AS t3ver_oid, C.uid AS uid, B.pid AS livepid';
320 $from = $table . ' A, ' . $table . ' B,' . $table . ' C';
321 $where = 'A.t3ver_state=' . new VersionState(VersionState::MOVE_PLACEHOLDER) . ' AND B.pid>0 AND B.t3ver_state='
322 . new VersionState(VersionState::DEFAULT_STATE) . ' AND B.t3ver_wsid=0 AND C.pid=-1 AND C.t3ver_state='
323 . new VersionState(VersionState::MOVE_POINTER);
324 if ($wsid > self::SELECT_ALL_WORKSPACES) {
325 $where .= ' AND A.t3ver_wsid=' . $wsid . ' AND C.t3ver_wsid=' . $wsid;
326 } elseif ($wsid === self::SELECT_ALL_WORKSPACES) {
327 $where .= ' AND A.t3ver_wsid!=0 AND C.t3ver_wsid!=0 ';
328 }
329 // lifecycle filter:
330 // 1 = select all drafts (never-published),
331 // 2 = select all published one or more times (archive/multiple)
332 if ($filter === 1 || $filter === 2) {
333 $where .= ' AND C.t3ver_count ' . ($filter === 1 ? '= 0' : '> 0');
334 }
335 if ($stage != -99) {
336 $where .= ' AND C.t3ver_stage=' . (int)$stage;
337 }
338 if ($pageList) {
339 $pidField = $table === 'pages' ? 'B.uid' : 'A.pid';
340 $pidConstraint = strstr($pageList, ',') ? ' IN (' . $pageList . ')' : '=' . $pageList;
341 $where .= ' AND ' . $pidField . $pidConstraint;
342 }
343 $where .= ' AND A.t3ver_move_id = B.uid AND B.uid = C.t3ver_oid';
344 $where .= BackendUtility::deleteClause($table, 'A');
345 $where .= BackendUtility::deleteClause($table, 'B');
346 $where .= BackendUtility::deleteClause($table, 'C');
347 $res = $this->getDatabaseConnection()->exec_SELECTgetRows($fields, $from, $where, '', 'A.uid');
348 return is_array($res) ? $res : array();
349 }
350
351 /**
352 * Find all page uids recursive starting from a specific page
353 *
354 * @param int $pageId
355 * @param int $wsid
356 * @param int $recursionLevel
357 * @return string Comma sep. uid list
358 */
359 protected function getTreeUids($pageId, $wsid, $recursionLevel) {
360 // Reusing existing functionality with the drawback that
361 // mount points are not covered yet
362 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
363 /** @var $searchObj \TYPO3\CMS\Core\Database\QueryView */
364 $searchObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\QueryView::class);
365 if ($pageId > 0) {
366 $pageList = $searchObj->getTreeList($pageId, $recursionLevel, 0, $perms_clause);
367 } else {
368 $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
369 if (!is_array($mountPoints) || empty($mountPoints)) {
370 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
371 $mountPoints = array_unique($mountPoints);
372 }
373 $newList = array();
374 foreach ($mountPoints as $mountPoint) {
375 $newList[] = $searchObj->getTreeList($mountPoint, $recursionLevel, 0, $perms_clause);
376 }
377 $pageList = implode(',', $newList);
378 }
379 unset($searchObj);
380 if ((int)$GLOBALS['TCA']['pages']['ctrl']['versioningWS'] === 2 && $pageList) {
381 // Remove the "subbranch" if a page was moved away
382 $movedAwayPages = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid, pid, t3ver_move_id', 'pages', 't3ver_move_id IN (' . $pageList . ') AND t3ver_wsid=' . (int)$wsid . BackendUtility::deleteClause('pages'), '', 'uid', '', 't3ver_move_id');
383 $pageIds = GeneralUtility::intExplode(',', $pageList, TRUE);
384 // move all pages away
385 $newList = array_diff($pageIds, array_keys($movedAwayPages));
386 // keep current page in the list
387 $newList[] = $pageId;
388 // move back in if still connected to the "remaining" pages
389 do {
390 $changed = FALSE;
391 foreach ($movedAwayPages as $uid => $rec) {
392 if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) {
393 $newList[] = $uid;
394 $changed = TRUE;
395 }
396 }
397 } while ($changed);
398 $pageList = implode(',', $newList);
399 // In case moving pages is enabled we need to replace all move-to pointer with their origin
400 $pages = $this->getDatabaseConnection()->exec_SELECTgetRows('uid, t3ver_move_id', 'pages', 'uid IN (' . $pageList . ')' . BackendUtility::deleteClause('pages'), '', 'uid', '', 'uid');
401 $newList = array();
402 $pageIds = GeneralUtility::intExplode(',', $pageList, TRUE);
403 if (!in_array($pageId, $pageIds)) {
404 $pageIds[] = $pageId;
405 }
406 foreach ($pageIds as $pageId) {
407 if ((int)$pages[$pageId]['t3ver_move_id'] > 0) {
408 $newList[] = (int)$pages[$pageId]['t3ver_move_id'];
409 } else {
410 $newList[] = $pageId;
411 }
412 }
413 $pageList = implode(',', $newList);
414 }
415 return $pageList;
416 }
417
418 /**
419 * Remove all records which are not permitted for the user
420 *
421 * @param array $recs
422 * @param string $table
423 * @return array
424 */
425 protected function filterPermittedElements($recs, $table) {
426 $permittedElements = array();
427 if (is_array($recs)) {
428 foreach ($recs as $rec) {
429 if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) {
430 $permittedElements[] = $rec;
431 }
432 }
433 }
434 return $permittedElements;
435 }
436
437 /**
438 * Checking access to the page the record is on, respecting ignored root level restrictions
439 *
440 * @param string $table Name of the table
441 * @param array $record Record row to be checked
442 * @return bool
443 */
444 protected function isPageAccessibleForCurrentUser($table, array $record) {
445 $pageIdField = $table === 'pages' ? 'uid' : 'wspid';
446 $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : NULL;
447 if ($pageId === NULL) {
448 return FALSE;
449 }
450 if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) {
451 return TRUE;
452 }
453 $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody');
454
455 return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1);
456 }
457
458 /**
459 * Check current be users language access on given record.
460 *
461 * @param string $table Name of the table
462 * @param array $record Record row to be checked
463 * @return bool
464 */
465 protected function isLanguageAccessibleForCurrentUser($table, array $record) {
466 if (BackendUtility::isTableLocalizable($table)) {
467 $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
468 } else {
469 return TRUE;
470 }
471 return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid);
472 }
473
474 /**
475 * Trivial check to see if the user already migrated his workspaces
476 * to the new style (either manually or with the migrator scripts)
477 *
478 * @return bool
479 */
480 static public function isOldStyleWorkspaceUsed() {
481 $cacheKey = 'workspace-oldstyleworkspace-notused';
482 $cacheResult = $GLOBALS['BE_USER']->getSessionData($cacheKey);
483 if (!$cacheResult) {
484 $where = 'adminusers != \'\' AND adminusers NOT LIKE \'%be_users%\' AND adminusers NOT LIKE \'%be_groups%\' AND deleted=0';
485 $count = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('uid', 'sys_workspace', $where);
486 $oldStyleWorkspaceIsUsed = $count > 0;
487 $GLOBALS['BE_USER']->setAndSaveSessionData($cacheKey, !$oldStyleWorkspaceIsUsed);
488 } else {
489 $oldStyleWorkspaceIsUsed = !$cacheResult;
490 }
491 return $oldStyleWorkspaceIsUsed;
492 }
493
494 /**
495 * Determine whether a specific page is new and not yet available in the LIVE workspace
496 *
497 * @param int $id Primary key of the page to check
498 * @param int $language Language for which to check the page
499 * @return bool
500 */
501 static public function isNewPage($id, $language = 0) {
502 $isNewPage = FALSE;
503 // If the language is not default, check state of overlay
504 if ($language > 0) {
505 $whereClause = 'pid = ' . (int)$id;
506 $whereClause .= ' AND ' . $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'] . ' = ' . (int)$language;
507 $whereClause .= ' AND t3ver_wsid = ' . (int)$GLOBALS['BE_USER']->workspace;
508 $whereClause .= BackendUtility::deleteClause('pages_language_overlay');
509 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('t3ver_state', 'pages_language_overlay', $whereClause);
510 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
511 $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
512 }
513 } else {
514 $rec = BackendUtility::getRecord('pages', $id, 't3ver_state');
515 if (is_array($rec)) {
516 $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
517 }
518 }
519 return $isNewPage;
520 }
521
522 /**
523 * Generates a view link for a page.
524 *
525 * @static
526 * @param string $table Table to be used
527 * @param int $uid Uid of the version(!) record
528 * @param array $liveRecord Optional live record data
529 * @param array $versionRecord Optional version record data
530 * @return string
531 */
532 static public function viewSingleRecord($table, $uid, array $liveRecord = NULL, array $versionRecord = NULL) {
533 $viewUrl = '';
534
535 if ($table == 'pages') {
536 $viewUrl = BackendUtility::viewOnClick(BackendUtility::getLiveVersionIdOfRecord('pages', $uid));
537 } elseif ($table === 'pages_language_overlay' || $table === 'tt_content') {
538 if ($liveRecord === NULL) {
539 $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
540 }
541 if ($versionRecord === NULL) {
542 $versionRecord = BackendUtility::getRecord($table, $uid);
543 }
544 if (VersionState::cast($versionRecord['t3ver_state'])->equals(VersionState::MOVE_POINTER)) {
545 $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRecord['uid'], 'pid');
546 }
547
548 $previewPageId = (empty($movePlaceholder['pid']) ? $liveRecord['pid'] : $movePlaceholder['pid']);
549 $additionalParameters = '&tx_workspaces_web_workspacesworkspaces[previewWS]=' . $versionRecord['t3ver_wsid'];
550
551 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
552 if ($versionRecord[$languageField] > 0) {
553 $additionalParameters .= '&L=' . $versionRecord[$languageField];
554 }
555
556 $viewUrl = BackendUtility::viewOnClick($previewPageId, '', '', '', '', $additionalParameters);
557 } else {
558 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'])) {
559 $_params = array(
560 'table' => $table,
561 'uid' => $uid,
562 'record' => $liveRecord,
563 'liveRecord' => $liveRecord,
564 'versionRecord' => $versionRecord,
565 );
566 $_funcRef = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'];
567 $null = NULL;
568 $viewUrl = GeneralUtility::callUserFunction($_funcRef, $_params, $null);
569 }
570 }
571
572 return $viewUrl;
573 }
574
575 /**
576 * Determine whether this page for the current
577 *
578 * @param int $pageUid
579 * @param int $workspaceUid
580 * @return bool
581 */
582 public function canCreatePreviewLink($pageUid, $workspaceUid) {
583 $result = TRUE;
584 if ($pageUid > 0 && $workspaceUid > 0) {
585 $pageRecord = BackendUtility::getRecord('pages', $pageUid);
586 BackendUtility::workspaceOL('pages', $pageRecord, $workspaceUid);
587 if (
588 !GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['FE']['content_doktypes'], $pageRecord['doktype'])
589 || VersionState::cast($pageRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
590 ) {
591 $result = FALSE;
592 }
593 } else {
594 $result = FALSE;
595 }
596 return $result;
597 }
598
599 /**
600 * Generates a workspace preview link.
601 *
602 * @param int $uid The ID of the record to be linked
603 * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
604 */
605 public function generateWorkspacePreviewLink($uid) {
606 $previewObject = GeneralUtility::makeInstance(\TYPO3\CMS\Version\Hook\PreviewHook::class);
607 $timeToLiveHours = $previewObject->getPreviewLinkLifetime();
608 $previewKeyword = $previewObject->compilePreviewKeyword('', $GLOBALS['BE_USER']->user['uid'], $timeToLiveHours * 3600, $this->getCurrentWorkspace());
609 $linkParams = array(
610 'ADMCMD_prev' => $previewKeyword,
611 'id' => $uid
612 );
613 return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams);
614 }
615
616 /**
617 * Generates a workspace splitted preview link.
618 *
619 * @param int $uid The ID of the record to be linked
620 * @param bool $addDomain Parameter to decide if domain should be added to the generated link, FALSE per default
621 * @return string the preview link without the trailing '/'
622 */
623 public function generateWorkspaceSplittedPreviewLink($uid, $addDomain = FALSE) {
624 // In case a $pageUid is submitted we need to make sure it points to a live-page
625 if ($uid > 0) {
626 $uid = $this->getLivePageUid($uid);
627 }
628 /** @var $uriBuilder \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder */
629 $uriBuilder = $this->getObjectManager()->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
630 $redirect = 'index.php?redirect_url=';
631 // @todo this should maybe be changed so that the extbase URI Builder can deal with module names directly
632 $originalM = GeneralUtility::_GET('M');
633 GeneralUtility::_GETset('web_WorkspacesWorkspaces', 'M');
634 $viewScript = $uriBuilder->uriFor('index', array(), 'Preview', 'workspaces', 'web_workspacesworkspaces') . '&id=';
635 GeneralUtility::_GETset($originalM, 'M');
636 if ($addDomain === TRUE) {
637 return BackendUtility::getViewDomain($uid) . $redirect . urlencode($viewScript) . $uid;
638 } else {
639 return $viewScript;
640 }
641 }
642
643 /**
644 * Generate workspace preview links for all available languages of a page
645 *
646 * @param int $uid
647 * @return array
648 */
649 public function generateWorkspacePreviewLinksForAllLanguages($uid) {
650 $previewUrl = $this->generateWorkspacePreviewLink($uid);
651 $previewLanguages = $this->getAvailableLanguages($uid);
652 $previewLinks = array();
653
654 foreach ($previewLanguages as $languageUid => $language) {
655 $previewLinks[$language] = $previewUrl . '&L=' . $languageUid;
656 }
657
658 return $previewLinks;
659 }
660
661 /**
662 * Find the Live-Uid for a given page,
663 * the results are cached at run-time to avoid too many database-queries
664 *
665 * @throws \InvalidArgumentException
666 * @param int $uid
667 * @return int
668 */
669 public function getLivePageUid($uid) {
670 if (!isset($this->pageCache[$uid])) {
671 $pageRecord = BackendUtility::getRecord('pages', $uid);
672 if (is_array($pageRecord)) {
673 $this->pageCache[$uid] = $pageRecord['t3ver_oid'] ? $pageRecord['t3ver_oid'] : $uid;
674 } else {
675 throw new \InvalidArgumentException('uid is supposed to point to an existing page - given value was:' . $uid, 1290628113);
676 }
677 }
678 return $this->pageCache[$uid];
679 }
680
681 /**
682 * Checks if a page has record versions according to a given workspace
683 *
684 * @param int $workspace
685 * @param int $pageId
686 * @return bool
687 */
688 public function hasPageRecordVersions($workspace, $pageId) {
689 $workspace = (int)$workspace;
690 $pageId = (int)$pageId;
691 if ($workspace === 0) {
692 return FALSE;
693 }
694
695 if (isset($this->versionsOnPageCache[$pageId][$workspace])) {
696 return $this->versionsOnPageCache[$pageId][$workspace];
697 }
698
699 if (!empty($this->versionsOnPageCache)) {
700 return FALSE;
701 }
702
703 $this->versionsOnPageCache[$pageId][$workspace] = FALSE;
704 foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
705 if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) {
706 continue;
707 }
708 $joinStatement = 'A.t3ver_oid=B.uid';
709 // Consider records that are moved to a different page
710 if (BackendUtility::isTableMovePlaceholderAware($tableName)) {
711 $movePointer = new VersionState(VersionState::MOVE_POINTER);
712 $joinStatement = '(A.t3ver_oid=B.uid AND A.t3ver_state<>' . $movePointer
713 . ' OR A.t3ver_oid=B.t3ver_move_id AND A.t3ver_state=' . $movePointer . ')';
714 }
715 // Select all records from this table in the database from the workspace
716 // This joins the online version with the offline version as tables A and B
717 $records = $this->getDatabaseConnection()->exec_SELECTgetRows(
718 'B.uid as live_uid, B.pid as live_pid, A.uid as offline_uid',
719 $tableName . ' A,' . $tableName . ' B',
720 'A.pid=-1 AND A.t3ver_wsid=' . $workspace . ' AND ' . $joinStatement .
721 BackendUtility::deleteClause($tableName, 'A') . BackendUtility::deleteClause($tableName, 'B'),
722 'live_pid'
723 );
724
725 if (!empty($records)) {
726 foreach ($records as $record) {
727 $this->versionsOnPageCache[$record['live_pid']][$workspace] = TRUE;
728 }
729 }
730 }
731
732 return $this->versionsOnPageCache[$pageId][$workspace];
733 }
734
735 /**
736 * @return DatabaseConnection
737 */
738 protected function getDatabaseConnection() {
739 return $GLOBALS['TYPO3_DB'];
740 }
741
742 /**
743 * @return \TYPO3\CMS\Extbase\Object\ObjectManager
744 */
745 protected function getObjectManager() {
746 return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
747 }
748
749 /**
750 * Get the available languages of a certain page
751 *
752 * @param int $pageId
753 * @return array
754 */
755 public function getAvailableLanguages($pageId) {
756 $languageOptions = array();
757 /** @var \TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider $translationConfigurationProvider */
758 $translationConfigurationProvider = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider::class);
759 $systemLanguages = $translationConfigurationProvider->getSystemLanguages($pageId);
760
761 if ($GLOBALS['BE_USER']->checkLanguageAccess(0)) {
762 // Use configured label for default language
763 $languageOptions[0] = $systemLanguages[0]['title'];
764 }
765 $pages = BackendUtility::getRecordsByField('pages_language_overlay', 'pid', $pageId);
766
767 if (!is_array($pages)) {
768 return $languageOptions;
769 }
770
771 foreach ($pages as $page) {
772 $languageId = (int)$page['sys_language_uid'];
773 // Only add links to active languages the user has access to
774 if (isset($systemLanguages[$languageId]) && $GLOBALS['BE_USER']->checkLanguageAccess($languageId)) {
775 $languageOptions[$page['sys_language_uid']] = $systemLanguages[$languageId]['title'];
776 }
777 }
778
779 return $languageOptions;
780 }
781 }