Test Failed
Branch master (7b1793)
by Tymoteusz
15:35
created

WorkspaceService::generateWorkspacePreviewLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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