Passed
Push — master ( 1e85f0...74899e )
by
unknown
14:10
created

WorkspaceService::getNewVersionsForPages()   C

Complexity

Conditions 13
Paths 98

Size

Total Lines 106
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 59
nc 98
nop 5
dl 0
loc 106
rs 6.6166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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