Completed
Push — master ( 734e6b...e66cb2 )
by
unknown
17:00
created

WorkspaceService::getPreviewLinkLifetime()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
826
            GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
827
        }
828
829
        return $this->versionsOnPageCache[$workspaceId][$pageId];
830
    }
831
832
    /**
833
     * Gets all pages that have workspace versions per table.
834
     *
835
     * Result:
836
     * [
837
     *   'sys_template' => [],
838
     *   'tt_content' => [
839
     *     1 => true,
840
     *     11 => true,
841
     *     13 => true,
842
     *     15 => true
843
     *   ],
844
     *   'tx_something => [
845
     *     15 => true,
846
     *     11 => true,
847
     *     21 => true
848
     *   ],
849
     * ]
850
     *
851
     * @param int $workspaceId
852
     *
853
     * @return array
854
     */
855
    public function getPagesWithVersionsInTable($workspaceId)
856
    {
857
        foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
858
            if ($tableName === 'pages' || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
859
                continue;
860
            }
861
862
            $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
863
        }
864
865
        return $this->pagesWithVersionsInTable[$workspaceId];
866
    }
867
868
    /**
869
     * Gets all pages that have workspace versions in a particular table.
870
     *
871
     * Result:
872
     * [
873
     *   1 => true,
874
     *   11 => true,
875
     *   13 => true,
876
     *   15 => true
877
     * ],
878
     *
879
     * @param int $workspaceId
880
     * @param string $tableName
881
     * @return array
882
     */
883
    protected function fetchPagesWithVersionsInTable($workspaceId, $tableName)
884
    {
885
        if ((int)$workspaceId === 0) {
886
            return [];
887
        }
888
889
        if (!isset($this->pagesWithVersionsInTable[$workspaceId])) {
890
            $this->pagesWithVersionsInTable[$workspaceId] = [];
891
        }
892
893
        if (!isset($this->pagesWithVersionsInTable[$workspaceId][$tableName])) {
894
            $this->pagesWithVersionsInTable[$workspaceId][$tableName] = [];
895
896
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
897
            $queryBuilder->getRestrictions()
898
                ->removeAll()
899
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
900
901
            $movePointerParameter = $queryBuilder->createNamedParameter(
902
                VersionState::MOVE_POINTER,
903
                \PDO::PARAM_INT
904
            );
905
            $workspaceIdParameter = $queryBuilder->createNamedParameter(
906
                $workspaceId,
907
                \PDO::PARAM_INT
908
            );
909
            $onlineVersionParameter = $queryBuilder->createNamedParameter(
910
                0,
911
                \PDO::PARAM_INT
912
            );
913
            // create sub-queries, parameters are available for main query
914
            $versionQueryBuilder = $this->createQueryBuilderForTable($tableName)
915
                ->select('A.t3ver_oid')
916
                ->from($tableName, 'A')
917
                ->where(
918
                    $queryBuilder->expr()->gt('A.t3ver_oid', $onlineVersionParameter),
919
                    $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
920
                    $queryBuilder->expr()->neq('A.t3ver_state', $movePointerParameter)
921
                );
922
            $movePointerQueryBuilder = $this->createQueryBuilderForTable($tableName)
923
                ->select('A.t3ver_oid')
924
                ->from($tableName, 'A')
925
                ->where(
926
                    $queryBuilder->expr()->gt('A.t3ver_oid', $onlineVersionParameter),
927
                    $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
928
                    $queryBuilder->expr()->eq('A.t3ver_state', $movePointerParameter)
929
                );
930
            $subQuery = '%s IN (%s)';
931
            // execute main query
932
            $result = $queryBuilder
933
                ->select('B.pid AS pageId')
934
                ->from($tableName, 'B')
935
                ->orWhere(
936
                    sprintf(
937
                        $subQuery,
938
                        $queryBuilder->quoteIdentifier('B.uid'),
939
                        $versionQueryBuilder->getSQL()
940
                    ),
941
                    sprintf(
942
                        $subQuery,
943
                        $queryBuilder->quoteIdentifier('B.t3ver_move_id'),
944
                        $movePointerQueryBuilder->getSQL()
945
                    )
946
                )
947
                ->groupBy('B.pid')
948
                ->execute();
949
950
            $pageIds = [];
951
            while ($row = $result->fetch()) {
952
                $pageIds[$row['pageId']] = true;
953
            }
954
955
            $this->pagesWithVersionsInTable[$workspaceId][$tableName] = $pageIds;
956
957
            $parameters = [
958
                'workspaceId' => $workspaceId,
959
                'tableName' => $tableName,
960
                'pagesWithVersionsInTable' => &$this->pagesWithVersionsInTable,
961
            ];
962
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Workspaces\Service\WorkspaceService::class]['fetchPagesWithVersionsInTable'] ?? [] as $hookFunction) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
963
                GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
964
            }
965
        }
966
967
        return $this->pagesWithVersionsInTable[$workspaceId][$tableName];
968
    }
969
970
    /**
971
     * @param string $tableName
972
     * @return QueryBuilder
973
     */
974
    protected function createQueryBuilderForTable(string $tableName)
975
    {
976
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
977
            ->getQueryBuilderForTable($tableName);
978
        $queryBuilder->getRestrictions()
979
            ->removeAll()
980
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
981
        return $queryBuilder;
982
    }
983
984
    /**
985
     * @return LanguageService|null
986
     */
987
    protected static function getLanguageService(): ?LanguageService
988
    {
989
        return $GLOBALS['LANG'] ?? null;
990
    }
991
}
992