Completed
Push — master ( d53f54...2e1e23 )
by
unknown
17:40
created

WorkspaceService::createQueryBuilderForTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Workspaces\Service;
17
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\QueryHelper;
23
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
25
use TYPO3\CMS\Core\Localization\LanguageService;
26
use TYPO3\CMS\Core\SingletonInterface;
27
use TYPO3\CMS\Core\Type\Bitmask\Permission;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\MathUtility;
30
use TYPO3\CMS\Core\Versioning\VersionState;
31
32
/**
33
 * Workspace service
34
 */
35
class WorkspaceService implements SingletonInterface
36
{
37
    /**
38
     * @var array
39
     */
40
    protected $versionsOnPageCache = [];
41
42
    /**
43
     * @var array
44
     */
45
    protected $pagesWithVersionsInTable = [];
46
47
    const TABLE_WORKSPACE = 'sys_workspace';
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
        return $GLOBALS['BE_USER']->workspace;
90
    }
91
92
    /**
93
     * easy function to just return the number of hours.
94
     *
95
     * a preview link is valid, based on the workspaces' custom value (default to 48 hours)
96
     * or falls back to the users' TSconfig value "options.workspaces.previewLinkTTLHours".
97
     *
98
     * by default, it's 48hs.
99
     *
100
     * @return int The hours as a number
101
     */
102
    public function getPreviewLinkLifetime(): int
103
    {
104
        $workspaceId = $GLOBALS['BE_USER']->workspace;
105
        if ($workspaceId > 0) {
106
            $wsRecord = BackendUtility::getRecord('sys_workspace', $workspaceId, '*');
107
            if (($wsRecord['previewlink_lifetime'] ?? 0) > 0) {
108
                return (int)$wsRecord['previewlink_lifetime'];
109
            }
110
        }
111
        $ttlHours = (int)($GLOBALS['BE_USER']->getTSConfig()['options.']['workspaces.']['previewLinkTTLHours'] ?? 0);
112
        return $ttlHours ?: 24 * 2;
113
    }
114
115
    /**
116
     * Find the title for the requested workspace.
117
     *
118
     * @param int $wsId
119
     * @return string
120
     * @throws \InvalidArgumentException
121
     */
122
    public static function getWorkspaceTitle($wsId)
123
    {
124
        $title = false;
125
        switch ($wsId) {
126
            case self::LIVE_WORKSPACE_ID:
127
                $title = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:shortcut_onlineWS');
128
                break;
129
            default:
130
                $labelField = $GLOBALS['TCA']['sys_workspace']['ctrl']['label'];
131
                $wsRecord = BackendUtility::getRecord('sys_workspace', $wsId, 'uid,' . $labelField);
132
                if (is_array($wsRecord)) {
133
                    $title = $wsRecord[$labelField];
134
                }
135
        }
136
        if ($title === false) {
137
            throw new \InvalidArgumentException('No such workspace defined', 1476045469);
138
        }
139
        return $title;
140
    }
141
142
    /**
143
     * Building DataHandler CMD-array for publishing all versions in a workspace.
144
     *
145
     * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
146
     * @param bool $_ Unused, previously used to choose between swapping and publishing
147
     * @param int $pageId The page id
148
     * @param int|null $language Select specific language only
149
     * @return array Command array for DataHandler
150
     */
151
    public function getCmdArrayForPublishWS($wsid, $_ = false, $pageId = 0, $language = null)
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

151
    public function getCmdArrayForPublishWS($wsid, /** @scrutinizer ignore-unused */ $_ = false, $pageId = 0, $language = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
    {
153
        $wsid = (int)$wsid;
154
        $cmd = [];
155
        if ($wsid > 0) {
156
            // Define stage to select:
157
            $stage = -99;
158
            $workspaceRec = BackendUtility::getRecord('sys_workspace', $wsid);
159
            if ($workspaceRec['publish_access'] & 1) {
160
                $stage = StagesService::STAGE_PUBLISH_ID;
161
            }
162
            // Select all versions to publishing
163
            $versions = $this->selectVersionsInWorkspace(
164
                $wsid,
165
                $stage,
166
                $pageId ?: -1,
167
                999,
168
                'tables_modify',
169
                $language
170
            );
171
            // Traverse the selection to build CMD array:
172
            foreach ($versions as $table => $records) {
173
                foreach ($records as $rec) {
174
                    // Build the cmd Array:
175
                    $cmd[$table][$rec['t3ver_oid']]['version'] = ['action' => 'swap', 'swapWith' => $rec['uid']];
176
                }
177
            }
178
        }
179
        return $cmd;
180
    }
181
182
    /**
183
     * Building DataHandler CMD-array for releasing all versions in a workspace.
184
     *
185
     * @param int $wsid Real workspace ID, cannot be ONLINE (zero).
186
     * @param bool $flush Run Flush (TRUE) or ClearWSID (FALSE) command
187
     * @param int $pageId The page id
188
     * @param int $language Select specific language only
189
     * @return array Command array for DataHandler
190
     */
191
    public function getCmdArrayForFlushWS($wsid, $flush = true, $pageId = 0, $language = null)
192
    {
193
        $wsid = (int)$wsid;
194
        $cmd = [];
195
        if ($wsid > 0) {
196
            // Define stage to select:
197
            $stage = -99;
198
            // Select all versions to publish
199
            $versions = $this->selectVersionsInWorkspace(
200
                $wsid,
201
                $stage,
202
                $pageId ?: -1,
203
                999,
204
                'tables_modify',
205
                $language
206
            );
207
            // Traverse the selection to build CMD array:
208
            foreach ($versions as $table => $records) {
209
                foreach ($records as $rec) {
210
                    // Build the cmd Array:
211
                    $cmd[$table][$rec['uid']]['version'] = ['action' => $flush ? 'flush' : 'clearWSID'];
212
                }
213
            }
214
        }
215
        return $cmd;
216
    }
217
218
    /**
219
     * Select all records from workspace pending for publishing
220
     * Used from backend to display workspace overview
221
     * User for auto-publishing for selecting versions for publication
222
     *
223
     * @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
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, $stage = -99, $pageId = -1, $recursionLevel = 0, $selectionType = 'tables_select', $language = null)
232
    {
233
        $wsid = (int)$wsid;
234
        $output = [];
235
        // Contains either nothing or a list with live-uids
236
        if ($pageId != -1 && $recursionLevel > 0) {
237
            $pageList = $this->getTreeUids($pageId, $wsid, $recursionLevel);
238
        } elseif ($pageId != -1) {
239
            $pageList = $pageId;
240
        } else {
241
            $pageList = '';
242
            // check if person may only see a "virtual" page-root
243
            $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
244
            $mountPoints = array_unique($mountPoints);
245
            if (!in_array(0, $mountPoints)) {
246
                $tempPageIds = [];
247
                foreach ($mountPoints as $mountPoint) {
248
                    $tempPageIds[] = $this->getTreeUids($mountPoint, $wsid, $recursionLevel);
249
                }
250
                $pageList = implode(',', $tempPageIds);
251
                $pageList = implode(',', array_unique(explode(',', $pageList)));
252
            }
253
        }
254
        // Traversing all tables supporting versioning:
255
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
256
            // we do not collect records from tables without permissions on them.
257
            if (!$GLOBALS['BE_USER']->check($selectionType, $table)) {
258
                continue;
259
            }
260
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
261
                $recs = $this->selectAllVersionsFromPages($table, $pageList, $wsid, $stage, $language);
262
                $moveRecs = $this->getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $stage);
263
                $recs = array_merge($recs, $moveRecs);
264
                $recs = $this->filterPermittedElements($recs, $table);
265
                if (!empty($recs)) {
266
                    $output[$table] = $recs;
267
                }
268
            }
269
        }
270
        return $output;
271
    }
272
273
    /**
274
     * Find all versionized elements except moved records.
275
     *
276
     * @param string $table
277
     * @param string $pageList
278
     * @param int $wsid
279
     * @param int $stage
280
     * @param int $language
281
     * @return array
282
     */
283
    protected function selectAllVersionsFromPages($table, $pageList, $wsid, $stage, $language = null)
284
    {
285
        // Include root level page as there might be some records with where root level
286
        // restriction is ignored (e.g. FAL records)
287
        if ($pageList !== '' && BackendUtility::isRootLevelRestrictionIgnored($table)) {
288
            $pageList .= ',0';
289
        }
290
        $isTableLocalizable = BackendUtility::isTableLocalizable($table);
291
        $languageParentField = '';
292
        // If table is not localizable, but localized records shall
293
        // be collected, an empty result array needs to be returned:
294
        if ($isTableLocalizable === false && $language > 0) {
295
            return [];
296
        }
297
        if ($isTableLocalizable) {
298
            $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
299
        }
300
301
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
302
        $queryBuilder->getRestrictions()->removeAll()
303
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
304
305
        $fields = ['A.uid', 'A.pid', 'A.t3ver_oid', 'A.t3ver_stage', 'B.pid', 'B.pid AS wspid', 'B.pid AS livepid'];
306
        if ($isTableLocalizable) {
307
            $fields[] = $languageParentField;
308
            $fields[] = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
309
        }
310
        // Table A is the offline version and t3ver_oid>0 defines offline
311
        // Table B (online) must have t3ver_oid=0 to signify being online.
312
        $constraints = [
313
            $queryBuilder->expr()->gt(
314
                'A.t3ver_oid',
315
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
316
            ),
317
            $queryBuilder->expr()->eq(
318
                'B.t3ver_oid',
319
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
320
            ),
321
            $queryBuilder->expr()->neq(
322
                'A.t3ver_state',
323
                $queryBuilder->createNamedParameter(
324
                    (string)new VersionState(VersionState::MOVE_POINTER),
325
                    \PDO::PARAM_INT
326
                )
327
            )
328
        ];
329
330
        if ($pageList) {
331
            $pageIdRestriction = GeneralUtility::intExplode(',', $pageList, true);
332
            if ($table === 'pages') {
333
                $constraints[] = $queryBuilder->expr()->orX(
334
                    $queryBuilder->expr()->in(
335
                        'B.uid',
336
                        $queryBuilder->createNamedParameter(
337
                            $pageIdRestriction,
338
                            Connection::PARAM_INT_ARRAY
339
                        )
340
                    ),
341
                    $queryBuilder->expr()->in(
342
                        'B.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
343
                        $queryBuilder->createNamedParameter(
344
                            $pageIdRestriction,
345
                            Connection::PARAM_INT_ARRAY
346
                        )
347
                    )
348
                );
349
            } else {
350
                $constraints[] = $queryBuilder->expr()->in(
351
                    'B.pid',
352
                    $queryBuilder->createNamedParameter(
353
                        $pageIdRestriction,
354
                        Connection::PARAM_INT_ARRAY
355
                    )
356
                );
357
            }
358
        }
359
360
        if ($isTableLocalizable && MathUtility::canBeInterpretedAsInteger($language)) {
361
            $constraints[] = $queryBuilder->expr()->eq(
362
                'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'],
363
                $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
364
            );
365
        }
366
367
        if ($wsid >= 0) {
368
            $constraints[] = $queryBuilder->expr()->eq(
369
                'A.t3ver_wsid',
370
                $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
371
            );
372
        }
373
374
        if ((int)$stage !== -99) {
375
            $constraints[] = $queryBuilder->expr()->eq(
376
                'A.t3ver_stage',
377
                $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
378
            );
379
        }
380
381
        // ... and finally the join between the two tables.
382
        $constraints[] = $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'));
383
384
        // Select all records from this table in the database from the workspace
385
        // This joins the online version with the offline version as tables A and B
386
        // Order by UID, mostly to have a sorting in the backend overview module which
387
        // doesn't "jump around" when publishing.
388
        $rows = $queryBuilder->select(...$fields)
389
            ->from($table, 'A')
390
            ->from($table, 'B')
391
            ->where(...$constraints)
392
            ->orderBy('B.uid')
393
            ->execute()
394
            ->fetchAll();
395
396
        return $rows;
397
    }
398
399
    /**
400
     * Find all moved records at their new position.
401
     *
402
     * @param string $table
403
     * @param string $pageList
404
     * @param int $wsid
405
     * @param int $stage
406
     * @return array
407
     */
408
    protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $stage)
409
    {
410
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
411
        $queryBuilder->getRestrictions()->removeAll()
412
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
413
414
        // Aliases:
415
        // A - moveTo placeholder
416
        // B - online record
417
        // C - moveFrom placeholder
418
        $constraints = [
419
            $queryBuilder->expr()->eq(
420
                'A.t3ver_state',
421
                $queryBuilder->createNamedParameter(
422
                    (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
423
                    \PDO::PARAM_INT
424
                )
425
            ),
426
            $queryBuilder->expr()->gt(
427
                'B.pid',
428
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
429
            ),
430
            $queryBuilder->expr()->eq(
431
                'B.t3ver_state',
432
                $queryBuilder->createNamedParameter(
433
                    (string)new VersionState(VersionState::DEFAULT_STATE),
434
                    \PDO::PARAM_INT
435
                )
436
            ),
437
            $queryBuilder->expr()->eq(
438
                'B.t3ver_wsid',
439
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
440
            ),
441
            $queryBuilder->expr()->eq(
442
                'C.t3ver_state',
443
                $queryBuilder->createNamedParameter(
444
                    (string)new VersionState(VersionState::MOVE_POINTER),
445
                    \PDO::PARAM_INT
446
                )
447
            ),
448
            $queryBuilder->expr()->eq('A.t3ver_move_id', $queryBuilder->quoteIdentifier('B.uid')),
449
            $queryBuilder->expr()->eq('B.uid', $queryBuilder->quoteIdentifier('C.t3ver_oid'))
450
        ];
451
452
        if ($wsid >= 0) {
453
            $constraints[] = $queryBuilder->expr()->eq(
454
                'A.t3ver_wsid',
455
                $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
456
            );
457
            $constraints[] = $queryBuilder->expr()->eq(
458
                'C.t3ver_wsid',
459
                $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
460
            );
461
        }
462
463
        if ((int)$stage != -99) {
464
            $constraints[] = $queryBuilder->expr()->eq(
465
                'C.t3ver_stage',
466
                $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT)
467
            );
468
        }
469
470
        if ($pageList) {
471
            $pageIdRestriction = GeneralUtility::intExplode(',', $pageList, true);
472
            if ($table === 'pages') {
473
                $constraints[] = $queryBuilder->expr()->orX(
474
                    $queryBuilder->expr()->in(
475
                        'B.uid',
476
                        $queryBuilder->createNamedParameter(
477
                            $pageIdRestriction,
478
                            Connection::PARAM_INT_ARRAY
479
                        )
480
                    ),
481
                    $queryBuilder->expr()->in(
482
                        'B.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
483
                        $queryBuilder->createNamedParameter(
484
                            $pageIdRestriction,
485
                            Connection::PARAM_INT_ARRAY
486
                        )
487
                    )
488
                );
489
            } else {
490
                $constraints[] = $queryBuilder->expr()->in(
491
                    'A.pid',
492
                    $queryBuilder->createNamedParameter(
493
                        $pageIdRestriction,
494
                        Connection::PARAM_INT_ARRAY
495
                    )
496
                );
497
            }
498
        }
499
500
        $rows = $queryBuilder
501
            ->select('A.pid AS wspid', 'B.uid AS t3ver_oid', 'C.uid AS uid', 'B.pid AS livepid')
502
            ->from($table, 'A')
503
            ->from($table, 'B')
504
            ->from($table, 'C')
505
            ->where(...$constraints)
506
            ->orderBy('A.uid')
507
            ->execute()
508
            ->fetchAll();
509
510
        return $rows;
511
    }
512
513
    /**
514
     * Find all page uids recursive starting from a specific page
515
     *
516
     * @param int $pageId
517
     * @param int $wsid
518
     * @param int $recursionLevel
519
     * @return string Comma sep. uid list
520
     */
521
    protected function getTreeUids($pageId, $wsid, $recursionLevel)
522
    {
523
        // Reusing existing functionality with the drawback that
524
        // mount points are not covered yet
525
        $permsClause = QueryHelper::stripLogicalOperatorPrefix(
526
            $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW)
527
        );
528
        if ($pageId > 0) {
529
            $pageList = array_merge(
530
                [ (int)$pageId ],
531
                $this->getPageChildrenRecursive((int)$pageId, (int)$recursionLevel, 0, $permsClause)
532
            );
533
        } else {
534
            $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
535
            if (!is_array($mountPoints) || empty($mountPoints)) {
536
                $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
537
                $mountPoints = array_unique($mountPoints);
538
            }
539
            $pageList = [];
540
            foreach ($mountPoints as $mountPoint) {
541
                $pageList = array_merge(
542
                    $pageList
543
                    [ (int)$mountPoint ],
544
                    $this->getPageChildrenRecursive((int)$mountPoint, (int)$recursionLevel, 0, $permsClause)
545
                );
546
            }
547
        }
548
        $pageList = array_unique($pageList);
549
550
        if (BackendUtility::isTableWorkspaceEnabled('pages') && !empty($pageList)) {
551
            // Remove the "subbranch" if a page was moved away
552
            $pageIds = $pageList;
553
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
554
            $queryBuilder->getRestrictions()
555
                ->removeAll()
556
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
557
            $result = $queryBuilder
558
                ->select('uid', 'pid', 't3ver_move_id')
559
                ->from('pages')
560
                ->where(
561
                    $queryBuilder->expr()->in(
562
                        't3ver_move_id',
563
                        $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
564
                    ),
565
                    $queryBuilder->expr()->eq(
566
                        't3ver_wsid',
567
                        $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT)
568
                    )
569
                )
570
                ->orderBy('uid')
571
                ->execute();
572
573
            $movedAwayPages = [];
574
            while ($row = $result->fetch()) {
575
                $movedAwayPages[$row['t3ver_move_id']] = $row;
576
            }
577
578
            // move all pages away
579
            $newList = array_diff($pageIds, array_keys($movedAwayPages));
580
            // keep current page in the list
581
            $newList[] = $pageId;
582
            // move back in if still connected to the "remaining" pages
583
            do {
584
                $changed = false;
585
                foreach ($movedAwayPages as $uid => $rec) {
586
                    if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) {
587
                        $newList[] = $uid;
588
                        $changed = true;
589
                    }
590
                }
591
            } while ($changed);
592
593
            // In case moving pages is enabled we need to replace all move-to pointer with their origin
594
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
595
            $queryBuilder->getRestrictions()
596
                ->removeAll()
597
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
598
            $result = $queryBuilder->select('uid', 't3ver_move_id')
599
                ->from('pages')
600
                ->where(
601
                    $queryBuilder->expr()->in(
602
                        'uid',
603
                        $queryBuilder->createNamedParameter($newList, Connection::PARAM_INT_ARRAY)
604
                    )
605
                )
606
                ->orderBy('uid')
607
                ->execute();
608
609
            $pages = [];
610
            while ($row = $result->fetch()) {
611
                $pages[$row['uid']] = $row;
612
            }
613
614
            $pageIds = $newList;
615
            if (!in_array($pageId, $pageIds)) {
616
                $pageIds[] = $pageId;
617
            }
618
619
            $newList = [];
620
            foreach ($pageIds as $pageId) {
0 ignored issues
show
introduced by
$pageId is overwriting one of the parameters of this function.
Loading history...
621
                if ((int)$pages[$pageId]['t3ver_move_id'] > 0) {
622
                    $newList[] = (int)$pages[$pageId]['t3ver_move_id'];
623
                } else {
624
                    $newList[] = $pageId;
625
                }
626
            }
627
            $pageList = $newList;
628
        }
629
630
        return implode(',', $pageList);
631
    }
632
633
    /**
634
     * Recursively fetch all children of a given page
635
     *
636
     * @param int $pid uid of the page
637
     * @param int $depth
638
     * @param int $begin
639
     * @param string $permsClause
640
     * @return int[] List of child row $uid's
641
     */
642
    protected function getPageChildrenRecursive(int $pid, int $depth, int $begin, string $permsClause): array
643
    {
644
        $children = [];
645
        if ($pid && $depth > 0) {
646
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
647
            $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
648
            $statement = $queryBuilder->select('uid')
649
                ->from('pages')
650
                ->where(
651
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
652
                    $queryBuilder->expr()->eq('sys_language_uid', 0),
653
                    $permsClause
654
                )
655
                ->execute();
656
            while ($row = $statement->fetch()) {
657
                if ($begin <= 0) {
658
                    $children[] = (int)$row['uid'];
659
                }
660
                if ($depth > 1) {
661
                    $theSubList = $this->getPageChildrenRecursive((int)$row['uid'], $depth - 1, $begin - 1, $permsClause);
662
                    $children = array_merge($children, $theSubList);
663
                }
664
            }
665
        }
666
        return $children;
667
    }
668
669
    /**
670
     * Remove all records which are not permitted for the user
671
     *
672
     * @param array $recs
673
     * @param string $table
674
     * @return array
675
     */
676
    protected function filterPermittedElements($recs, $table)
677
    {
678
        $permittedElements = [];
679
        if (is_array($recs)) {
0 ignored issues
show
introduced by
The condition is_array($recs) is always true.
Loading history...
680
            foreach ($recs as $rec) {
681
                if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) {
682
                    $permittedElements[] = $rec;
683
                }
684
            }
685
        }
686
        return $permittedElements;
687
    }
688
689
    /**
690
     * Checking access to the page the record is on, respecting ignored root level restrictions
691
     *
692
     * @param string $table Name of the table
693
     * @param array $record Record row to be checked
694
     * @return bool
695
     */
696
    protected function isPageAccessibleForCurrentUser($table, array $record)
697
    {
698
        $pageIdField = $table === 'pages' ? 'uid' : 'wspid';
699
        $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : null;
700
        if ($pageId === null) {
701
            return false;
702
        }
703
        if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) {
704
            return true;
705
        }
706
        $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody');
707
708
        return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1);
709
    }
710
711
    /**
712
     * Check current be users language access on given record.
713
     *
714
     * @param string $table Name of the table
715
     * @param array $record Record row to be checked
716
     * @return bool
717
     */
718
    protected function isLanguageAccessibleForCurrentUser($table, array $record)
719
    {
720
        if (BackendUtility::isTableLocalizable($table)) {
721
            $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
722
        } else {
723
            return true;
724
        }
725
        return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid);
726
    }
727
728
    /**
729
     * Determine whether a specific page is new and not yet available in the LIVE workspace
730
     *
731
     * @param int $id Primary key of the page to check
732
     * @param int $language Language for which to check the page
733
     * @return bool
734
     */
735
    public static function isNewPage($id, $language = 0)
736
    {
737
        $isNewPage = false;
738
        // If the language is not default, check state of overlay
739
        if ($language > 0) {
740
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
741
                ->getQueryBuilderForTable('pages');
742
            $queryBuilder->getRestrictions()
743
                ->removeAll()
744
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
745
            $row = $queryBuilder->select('t3ver_state')
746
                ->from('pages')
747
                ->where(
748
                    $queryBuilder->expr()->eq(
749
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
750
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
751
                    ),
752
                    $queryBuilder->expr()->eq(
753
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
754
                        $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
755
                    ),
756
                    $queryBuilder->expr()->eq(
757
                        't3ver_wsid',
758
                        $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT)
759
                    )
760
                )
761
                ->setMaxResults(1)
762
                ->execute()
763
                ->fetch();
764
765
            if ($row !== false) {
766
                $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
767
            }
768
        } else {
769
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_state');
770
            if (is_array($rec)) {
771
                $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER);
772
            }
773
        }
774
        return $isNewPage;
775
    }
776
777
    /**
778
     * Determines whether a page has workspace versions.
779
     *
780
     * @param int $workspaceId
781
     * @param int $pageId
782
     * @return bool
783
     */
784
    public function hasPageRecordVersions($workspaceId, $pageId)
785
    {
786
        if ((int)$workspaceId === 0 || (int)$pageId === 0) {
787
            return false;
788
        }
789
790
        if (isset($this->versionsOnPageCache[$workspaceId][$pageId])) {
791
            return $this->versionsOnPageCache[$workspaceId][$pageId];
792
        }
793
794
        $this->versionsOnPageCache[$workspaceId][$pageId] = false;
795
796
        foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
797
            if ($tableName === 'pages' || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
798
                continue;
799
            }
800
801
            $pages = $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
802
            // Early break on first match
803
            if (!empty($pages[(string)$pageId])) {
804
                $this->versionsOnPageCache[$workspaceId][$pageId] = true;
805
                break;
806
            }
807
        }
808
809
        $parameters = [
810
            'workspaceId' => $workspaceId,
811
            'pageId' => $pageId,
812
            'versionsOnPageCache' => &$this->versionsOnPageCache,
813
        ];
814
        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...
815
            GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
816
        }
817
818
        return $this->versionsOnPageCache[$workspaceId][$pageId];
819
    }
820
821
    /**
822
     * Gets all pages that have workspace versions per table.
823
     *
824
     * Result:
825
     * [
826
     *   'sys_template' => [],
827
     *   'tt_content' => [
828
     *     1 => true,
829
     *     11 => true,
830
     *     13 => true,
831
     *     15 => true
832
     *   ],
833
     *   'tx_something => [
834
     *     15 => true,
835
     *     11 => true,
836
     *     21 => true
837
     *   ],
838
     * ]
839
     *
840
     * @param int $workspaceId
841
     *
842
     * @return array
843
     */
844
    public function getPagesWithVersionsInTable($workspaceId)
845
    {
846
        foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) {
847
            if ($tableName === 'pages' || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
848
                continue;
849
            }
850
851
            $this->fetchPagesWithVersionsInTable($workspaceId, $tableName);
852
        }
853
854
        return $this->pagesWithVersionsInTable[$workspaceId];
855
    }
856
857
    /**
858
     * Gets all pages that have workspace versions in a particular table.
859
     *
860
     * Result:
861
     * [
862
     *   1 => true,
863
     *   11 => true,
864
     *   13 => true,
865
     *   15 => true
866
     * ],
867
     *
868
     * @param int $workspaceId
869
     * @param string $tableName
870
     * @return array
871
     */
872
    protected function fetchPagesWithVersionsInTable($workspaceId, $tableName)
873
    {
874
        if ((int)$workspaceId === 0) {
875
            return [];
876
        }
877
878
        if (!isset($this->pagesWithVersionsInTable[$workspaceId])) {
879
            $this->pagesWithVersionsInTable[$workspaceId] = [];
880
        }
881
882
        if (!isset($this->pagesWithVersionsInTable[$workspaceId][$tableName])) {
883
            $this->pagesWithVersionsInTable[$workspaceId][$tableName] = [];
884
885
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
886
            $queryBuilder->getRestrictions()
887
                ->removeAll()
888
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
889
890
            $movePointerParameter = $queryBuilder->createNamedParameter(
891
                VersionState::MOVE_POINTER,
892
                \PDO::PARAM_INT
893
            );
894
            $workspaceIdParameter = $queryBuilder->createNamedParameter(
895
                $workspaceId,
896
                \PDO::PARAM_INT
897
            );
898
            $onlineVersionParameter = $queryBuilder->createNamedParameter(
899
                0,
900
                \PDO::PARAM_INT
901
            );
902
            // create sub-queries, parameters are available for main query
903
            $versionQueryBuilder = $this->createQueryBuilderForTable($tableName)
904
                ->select('A.t3ver_oid')
905
                ->from($tableName, 'A')
906
                ->where(
907
                    $queryBuilder->expr()->gt('A.t3ver_oid', $onlineVersionParameter),
908
                    $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
909
                    $queryBuilder->expr()->neq('A.t3ver_state', $movePointerParameter)
910
                );
911
            $movePointerQueryBuilder = $this->createQueryBuilderForTable($tableName)
912
                ->select('A.t3ver_oid')
913
                ->from($tableName, 'A')
914
                ->where(
915
                    $queryBuilder->expr()->gt('A.t3ver_oid', $onlineVersionParameter),
916
                    $queryBuilder->expr()->eq('A.t3ver_wsid', $workspaceIdParameter),
917
                    $queryBuilder->expr()->eq('A.t3ver_state', $movePointerParameter)
918
                );
919
            $subQuery = '%s IN (%s)';
920
            // execute main query
921
            $result = $queryBuilder
922
                ->select('B.pid AS pageId')
923
                ->from($tableName, 'B')
924
                ->orWhere(
925
                    sprintf(
926
                        $subQuery,
927
                        $queryBuilder->quoteIdentifier('B.uid'),
928
                        $versionQueryBuilder->getSQL()
929
                    ),
930
                    sprintf(
931
                        $subQuery,
932
                        $queryBuilder->quoteIdentifier('B.t3ver_move_id'),
933
                        $movePointerQueryBuilder->getSQL()
934
                    )
935
                )
936
                ->groupBy('B.pid')
937
                ->execute();
938
939
            $pageIds = [];
940
            while ($row = $result->fetch()) {
941
                $pageIds[$row['pageId']] = true;
942
            }
943
944
            $this->pagesWithVersionsInTable[$workspaceId][$tableName] = $pageIds;
945
946
            $parameters = [
947
                'workspaceId' => $workspaceId,
948
                'tableName' => $tableName,
949
                'pagesWithVersionsInTable' => &$this->pagesWithVersionsInTable,
950
            ];
951
            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...
952
                GeneralUtility::callUserFunction($hookFunction, $parameters, $this);
953
            }
954
        }
955
956
        return $this->pagesWithVersionsInTable[$workspaceId][$tableName];
957
    }
958
959
    /**
960
     * @param string $tableName
961
     * @return QueryBuilder
962
     */
963
    protected function createQueryBuilderForTable(string $tableName)
964
    {
965
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
966
            ->getQueryBuilderForTable($tableName);
967
        $queryBuilder->getRestrictions()
968
            ->removeAll()
969
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
970
        return $queryBuilder;
971
    }
972
973
    /**
974
     * @return LanguageService|null
975
     */
976
    protected static function getLanguageService(): ?LanguageService
977
    {
978
        return $GLOBALS['LANG'] ?? null;
979
    }
980
}
981