Completed
Push — master ( 097023...80a38d )
by
unknown
14:57
created

WorkspaceService::selectAllVersionsFromPages()   D

Complexity

Conditions 14
Paths 290

Size

Total Lines 122
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

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