Completed
Push — master ( 9dc190...fb61db )
by
unknown
16:04
created

TreeController::getDokTypes()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 12
nop 0
dl 0
loc 31
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller\Page;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use TYPO3\CMS\Backend\Configuration\BackendUserConfiguration;
23
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
24
use TYPO3\CMS\Backend\Utility\BackendUtility;
25
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26
use TYPO3\CMS\Core\Context\Context;
27
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
28
use TYPO3\CMS\Core\Database\Query\Restriction\PagePermissionRestriction;
29
use TYPO3\CMS\Core\Exception\Page\RootLineException;
30
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
31
use TYPO3\CMS\Core\Http\JsonResponse;
32
use TYPO3\CMS\Core\Imaging\Icon;
33
use TYPO3\CMS\Core\Imaging\IconFactory;
34
use TYPO3\CMS\Core\Localization\LanguageService;
35
use TYPO3\CMS\Core\Site\SiteFinder;
36
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
37
use TYPO3\CMS\Core\Type\Bitmask\Permission;
38
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
39
use TYPO3\CMS\Core\Utility\GeneralUtility;
40
use TYPO3\CMS\Core\Utility\RootlineUtility;
41
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
42
43
/**
44
 * Controller providing data to the page tree
45
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
46
 */
47
class TreeController
48
{
49
    /**
50
     * Option to use the nav_title field for outputting in the tree items, set via userTS.
51
     *
52
     * @var bool
53
     */
54
    protected $useNavTitle = false;
55
56
    /**
57
     * Option to prefix the page ID when outputting the tree items, set via userTS.
58
     *
59
     * @var bool
60
     */
61
    protected $addIdAsPrefix = false;
62
63
    /**
64
     * Option to prefix the domain name of sys_domains when outputting the tree items, set via userTS.
65
     *
66
     * @var bool
67
     */
68
    protected $addDomainName = false;
69
70
    /**
71
     * Option to add the rootline path above each mount point, set via userTS.
72
     *
73
     * @var bool
74
     */
75
    protected $showMountPathAboveMounts = false;
76
77
    /**
78
     * An array of background colors for a branch in the tree, set via userTS.
79
     *
80
     * @var array
81
     */
82
    protected $backgroundColors = [];
83
84
    /**
85
     * A list of pages not to be shown.
86
     *
87
     * @var array
88
     */
89
    protected $hiddenRecords = [];
90
91
    /**
92
     * Contains the state of all items that are expanded.
93
     *
94
     * @var array
95
     */
96
    protected $expandedState = [];
97
98
    /**
99
     * Instance of the icon factory, to be used for generating the items.
100
     *
101
     * @var IconFactory
102
     */
103
    protected $iconFactory;
104
105
    /**
106
     * Number of tree levels which should be returned on the first page tree load
107
     *
108
     * @var int
109
     */
110
    protected $levelsToFetch = 2;
111
112
    /**
113
     * When set to true all nodes returend by API will be expanded
114
     * @var bool
115
     */
116
    protected $expandAllNodes = false;
117
118
    /**
119
     * Constructor to set up common objects needed in various places.
120
     */
121
    public function __construct()
122
    {
123
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
124
    }
125
126
    protected function initializeConfiguration()
127
    {
128
        $userTsConfig = $this->getBackendUser()->getTSConfig();
129
        $this->hiddenRecords = GeneralUtility::intExplode(
130
            ',',
131
            $userTsConfig['options.']['hideRecords.']['pages'] ?? '',
132
            true
133
        );
134
        $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
135
        $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
136
        $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
137
        $this->useNavTitle = (bool)($userTsConfig['options.']['pageTree.']['showNavTitle'] ?? false);
138
        $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
139
        $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
140
        $this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
141
        if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
142
            $this->expandedState = (array)$this->expandedState->stateHash;
143
        } else {
144
            $this->expandedState = $this->expandedState['stateHash'] ?: [];
145
        }
146
    }
147
148
    /**
149
     * Returns page tree configuration in JSON
150
     *
151
     * @return ResponseInterface
152
     */
153
    public function fetchConfigurationAction(): ResponseInterface
154
    {
155
        $configuration = [
156
            'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
157
            'allowDragMove' => $this->isDragMoveAllowed(),
158
            'doktypes' => $this->getDokTypes(),
159
            'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE),
160
            'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)),
161
        ];
162
163
        return new JsonResponse($configuration);
164
    }
165
166
    /**
167
     * Returns the list of doktypes to display in page tree toolbar drag area
168
     *
169
     * Note: The list can be filtered by the user TypoScript
170
     * option "options.pageTree.doktypesToShowInNewPageDragArea".
171
     *
172
     * @return array
173
     */
174
    protected function getDokTypes(): array
175
    {
176
        $backendUser = $this->getBackendUser();
177
        $doktypeLabelMap = [];
178
        foreach ($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'] as $doktypeItemConfig) {
179
            if ($doktypeItemConfig[1] === '--div--') {
180
                continue;
181
            }
182
            $doktypeLabelMap[$doktypeItemConfig[1]] = $doktypeItemConfig[0];
183
        }
184
        $doktypes = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['pageTree.']['doktypesToShowInNewPageDragArea'] ?? '', true);
185
        $output = [];
186
        $allowedDoktypes = GeneralUtility::intExplode(',', $backendUser->groupData['pagetypes_select'], true);
187
        $isAdmin = $backendUser->isAdmin();
188
        // Early return if backend user may not create any doktype
189
        if (!$isAdmin && empty($allowedDoktypes)) {
190
            return $output;
191
        }
192
        foreach ($doktypes as $doktype) {
193
            if (!$isAdmin && !in_array($doktype, $allowedDoktypes, true)) {
194
                continue;
195
            }
196
            $label = htmlspecialchars($this->getLanguageService()->sL($doktypeLabelMap[$doktype]));
197
            $output[] = [
198
                'nodeType' => $doktype,
199
                'icon' => $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][$doktype] ?? '',
200
                'title' => $label,
201
                'tooltip' => $label
202
            ];
203
        }
204
        return $output;
205
    }
206
207
    /**
208
     * Returns JSON representing page tree
209
     *
210
     * @param ServerRequestInterface $request
211
     * @return ResponseInterface
212
     */
213
    public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
214
    {
215
        $this->initializeConfiguration();
216
217
        $items = [];
218
        if (!empty($request->getQueryParams()['pid'])) {
219
            // Fetching a part of a page tree
220
            $entryPoints = $this->getAllEntryPointPageTrees((int)$request->getQueryParams()['pid']);
221
            $mountPid = (int)($request->getQueryParams()['mount'] ?? 0);
222
            $parentDepth = (int)($request->getQueryParams()['pidDepth'] ?? 0);
223
            $this->levelsToFetch = $parentDepth + $this->levelsToFetch;
224
            foreach ($entryPoints as $page) {
225
                $items = array_merge($items, $this->pagesToFlatArray($page, $mountPid, $parentDepth));
226
            }
227
        } else {
228
            $entryPoints = $this->getAllEntryPointPageTrees();
229
            foreach ($entryPoints as $page) {
230
                $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
231
            }
232
        }
233
234
        return new JsonResponse($items);
235
    }
236
237
    /**
238
     * Returns JSON representing page tree filtered by keyword
239
     *
240
     * @param ServerRequestInterface $request
241
     * @return ResponseInterface
242
     */
243
    public function filterDataAction(ServerRequestInterface $request): ResponseInterface
244
    {
245
        $searchQuery = $request->getQueryParams()['q'] ?? '';
246
        if (trim($searchQuery) === '') {
247
            return new JsonResponse([]);
248
        }
249
250
        $this->initializeConfiguration();
251
        $this->expandAllNodes = true;
252
253
        $items = [];
254
        $entryPoints = $this->getAllEntryPointPageTrees(0, $searchQuery);
255
256
        foreach ($entryPoints as $page) {
257
            if (!empty($page)) {
258
                $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
259
            }
260
        }
261
262
        return new JsonResponse($items);
263
    }
264
265
    /**
266
     * Sets a temporary mount point
267
     *
268
     * @param ServerRequestInterface $request
269
     * @return ResponseInterface
270
     * @throws \RuntimeException
271
     */
272
    public function setTemporaryMountPointAction(ServerRequestInterface $request): ResponseInterface
273
    {
274
        if (empty($request->getParsedBody()['pid'])) {
275
            throw new \RuntimeException(
276
                'Required "pid" parameter is missing.',
277
                1511792197
278
            );
279
        }
280
        $pid = (int)$request->getParsedBody()['pid'];
281
282
        $this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid;
283
        $this->getBackendUser()->writeUC();
284
        $response = [
285
            'mountPointPath' => $this->getMountPointPath($pid)
286
        ];
287
        return new JsonResponse($response);
288
    }
289
290
    /**
291
     * Converts nested tree structure produced by PageTreeRepository to a flat, one level array
292
     * and also adds visual representation information to the data.
293
     *
294
     * @param array $page
295
     * @param int $entryPoint
296
     * @param int $depth
297
     * @param array $inheritedData
298
     * @return array
299
     */
300
    protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0, array $inheritedData = []): array
301
    {
302
        $backendUser = $this->getBackendUser();
303
        $pageId = (int)$page['uid'];
304
        if (in_array($pageId, $this->hiddenRecords, true)) {
305
            return [];
306
        }
307
        if ($pageId === 0 && !$backendUser->isAdmin()) {
308
            return [];
309
        }
310
311
        $stopPageTree = !empty($page['php_tree_stop']) && $depth > 0;
312
        $identifier = $entryPoint . '_' . $pageId;
313
        $expanded = !empty($page['expanded'])
314
            || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier])
315
            || $this->expandAllNodes;
316
317
        $backgroundColor = !empty($this->backgroundColors[$pageId]) ? $this->backgroundColors[$pageId] : ($inheritedData['backgroundColor'] ?? '');
318
319
        $suffix = '';
320
        $prefix = '';
321
        $nameSourceField = 'title';
322
        $visibleText = $page['title'];
323
        $tooltip = BackendUtility::titleAttribForPages($page, '', false);
324
        if ($pageId !== 0) {
325
            $icon = $this->iconFactory->getIconForRecord('pages', $page, Icon::SIZE_SMALL);
326
        } else {
327
            $icon = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL);
328
        }
329
330
        if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') {
331
            $nameSourceField = 'nav_title';
332
            $visibleText = $page['nav_title'];
333
        }
334
        if (trim($visibleText) === '') {
335
            $visibleText = htmlspecialchars('[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']');
336
        }
337
338
        if ($this->addDomainName && $page['is_siteroot']) {
339
            $domain = $this->getDomainNameForPage($pageId);
340
            $suffix = $domain !== '' ? ' [' . $domain . ']' : '';
341
        }
342
343
        $lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
344
        if (is_array($lockInfo)) {
345
            $tooltip .= ' - ' . $lockInfo['msg'];
346
        }
347
        if ($this->addIdAsPrefix) {
348
            $prefix = htmlspecialchars('[' . $pageId . '] ');
349
        }
350
351
        $items = [];
352
        $item = [
353
            // Used to track if the tree item is collapsed or not
354
            'stateIdentifier' => $identifier,
355
            'identifier' => $pageId,
356
            'depth' => $depth,
357
            'tip' => htmlspecialchars($tooltip),
358
            'icon' => $icon->getIdentifier(),
359
            'name' => $visibleText,
360
            'nameSourceField' => $nameSourceField,
361
            'mountPoint' => $entryPoint,
362
            'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId,
363
            'siblingsCount' => $page['siblingsCount'] ?? 1,
364
            'siblingsPosition' => $page['siblingsPosition'] ?? 1,
365
            'allowDelete' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_DELETE),
366
            'allowEdit' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_EDIT)
367
                && $backendUser->check('tables_modify', 'pages')
368
                && $backendUser->checkLanguageAccess(0)
369
        ];
370
371
        if (!empty($page['_children']) || $this->getPageTreeRepository()->hasChildren($pageId)) {
372
            $item['hasChildren'] = true;
373
            if ($depth >= $this->levelsToFetch) {
374
                $page = $this->getPageTreeRepository()->getTreeLevels($page, 1);
375
            }
376
        }
377
        if (!empty($prefix)) {
378
            $item['prefix'] = htmlspecialchars($prefix);
379
        }
380
        if (!empty($suffix)) {
381
            $item['suffix'] = htmlspecialchars($suffix);
382
        }
383
        if (is_array($lockInfo)) {
384
            $item['locked'] = true;
385
        }
386
        if ($icon->getOverlayIcon()) {
387
            $item['overlayIcon'] = $icon->getOverlayIcon()->getIdentifier();
388
        }
389
        if ($expanded && is_array($page['_children']) && !empty($page['_children'])) {
390
            $item['expanded'] = $expanded;
391
        }
392
        if ($backgroundColor) {
393
            $item['backgroundColor'] = htmlspecialchars($backgroundColor);
394
        }
395
        if ($stopPageTree) {
396
            $item['stopPageTree'] = $stopPageTree;
397
        }
398
        $class = $this->resolvePageCssClassNames($page);
399
        if (!empty($class)) {
400
            $item['class'] = $class;
401
        }
402
        if ($depth === 0) {
403
            $item['isMountPoint'] = true;
404
405
            if ($this->showMountPathAboveMounts) {
406
                $item['readableRootline'] = $this->getMountPointPath($pageId);
407
            }
408
        }
409
410
        $items[] = $item;
411
        if (!$stopPageTree && is_array($page['_children']) && !empty($page['_children']) && ($depth < $this->levelsToFetch || $expanded)) {
412
            $siblingsCount = count($page['_children']);
413
            $siblingsPosition = 0;
414
            $items[key($items)]['loaded'] = true;
415
            foreach ($page['_children'] as $child) {
416
                $child['siblingsCount'] = $siblingsCount;
417
                $child['siblingsPosition'] = ++$siblingsPosition;
418
                $items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
419
            }
420
        }
421
        return $items;
422
    }
423
424
    protected function getPageTreeRepository(): PageTreeRepository
425
    {
426
        $backendUser = $this->getBackendUser();
427
        $userTsConfig = $backendUser->getTSConfig();
428
        $excludedDocumentTypes = GeneralUtility::intExplode(',', $userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? '', true);
429
430
        $additionalQueryRestrictions = [];
431
        if (!empty($excludedDocumentTypes)) {
432
            $additionalQueryRestrictions[] = GeneralUtility::makeInstance(DocumentTypeExclusionRestriction::class, $excludedDocumentTypes);
433
        }
434
        $additionalQueryRestrictions[] = GeneralUtility::makeInstance(PagePermissionRestriction::class, GeneralUtility::makeInstance(Context::class)->getAspect('backend.user'), Permission::PAGE_SHOW);
435
436
        return GeneralUtility::makeInstance(
437
            PageTreeRepository::class,
438
            (int)$backendUser->workspace,
439
            [],
440
            $additionalQueryRestrictions
441
        );
442
    }
443
444
    /**
445
     * Fetches all pages for all tree entry points the user is allowed to see
446
     *
447
     * @param int $startPid
448
     * @param string $query The search query can either be a string to be found in the title or the nav_title of a page or the uid of a page.
449
     * @return array
450
     */
451
    protected function getAllEntryPointPageTrees(int $startPid = 0, string $query = ''): array
452
    {
453
        $backendUser = $this->getBackendUser();
454
        $entryPointId = $startPid > 0 ? $startPid : (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
455
        if ($entryPointId > 0) {
456
            $entryPointIds = [$entryPointId];
457
        } else {
458
            //watch out for deleted pages returned as webmount
459
            $entryPointIds = array_map('intval', $backendUser->returnWebmounts());
460
            $entryPointIds = array_unique($entryPointIds);
461
            if (empty($entryPointIds)) {
462
                // use a virtual root
463
                // the real mount points will be fetched in getNodes() then
464
                // since those will be the "sub pages" of the virtual root
465
                $entryPointIds = [0];
466
            }
467
        }
468
        if (empty($entryPointIds)) {
469
            return [];
470
        }
471
        $repository = $this->getPageTreeRepository();
472
473
        if ($query !== '') {
474
            $this->levelsToFetch = 999;
475
            $repository->fetchFilteredTree($query);
476
        }
477
478
        $entryPointRecords = [];
479
        foreach ($entryPointIds as $k => $entryPointId) {
480
            if (in_array($entryPointId, $this->hiddenRecords, true)) {
481
                continue;
482
            }
483
484
            if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
485
                try {
486
                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPointId)->get();
487
                } catch (RootLineException $e) {
488
                    $entryPointRootLine = [];
489
                }
490
                foreach ($entryPointRootLine as $rootLineEntry) {
491
                    $parentUid = $rootLineEntry['uid'];
492
                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPointId])) {
493
                        $this->backgroundColors[$entryPointId] = $this->backgroundColors[$parentUid];
494
                    }
495
                }
496
            }
497
            if ($entryPointId === 0) {
498
                $entryPointRecord = [
499
                    'uid' => 0,
500
                    'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
501
                ];
502
            } else {
503
                $permClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
504
                $entryPointRecord = BackendUtility::getRecord('pages', $entryPointId, '*', $permClause);
505
506
                if ($entryPointRecord !== null && !$this->getBackendUser()->isInWebMount($entryPointId)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getBackendUser()-...WebMount($entryPointId) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
507
                    $entryPointRecord = null;
508
                }
509
            }
510
            if ($entryPointRecord) {
511
                if ($query === '') {
512
                    $entryPointRecord = $repository->getTreeLevels($entryPointRecord, $this->levelsToFetch);
513
                } else {
514
                    $entryPointRecord = $repository->getTree($entryPointRecord['uid'], null, $entryPointIds);
515
                }
516
            }
517
518
            if (is_array($entryPointRecord) && !empty($entryPointRecord)) {
519
                $entryPointRecords[$k] = $entryPointRecord;
520
            }
521
        }
522
523
        return $entryPointRecords;
524
    }
525
526
    /**
527
     * Returns the first configured domain name for a page
528
     *
529
     * @param int $pageId
530
     * @return string
531
     */
532
    protected function getDomainNameForPage(int $pageId): string
533
    {
534
        $domain = '';
535
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
536
        try {
537
            $site = $siteFinder->getSiteByRootPageId($pageId);
538
            $domain = (string)$site->getBase();
539
        } catch (SiteNotFoundException $e) {
540
            // No site found
541
        }
542
543
        return $domain;
544
    }
545
546
    /**
547
     * Returns the mount point path for a temporary mount or the given id
548
     *
549
     * @param int $uid
550
     * @return string
551
     */
552
    protected function getMountPointPath(int $uid): string
553
    {
554
        if ($uid <= 0) {
555
            return '';
556
        }
557
        $rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
558
        array_shift($rootline);
559
        $path = [];
560
        foreach ($rootline as $rootlineElement) {
561
            $record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
562
            $text = $record['title'];
563
            if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
564
                $text = $record['nav_title'];
565
            }
566
            $path[] = htmlspecialchars($text);
567
        }
568
        return '/' . implode('/', $path);
569
    }
570
571
    /**
572
     * Fetches possible css class names to be used when a record was modified in a workspace
573
     *
574
     * @param array $page Page record (workspace overlaid)
575
     * @return string CSS class names to be applied
576
     */
577
    protected function resolvePageCssClassNames(array $page): string
578
    {
579
        $classes = [];
580
581
        if ($page['uid'] === 0) {
582
            return '';
583
        }
584
        $workspaceId = (int)$this->getBackendUser()->workspace;
585
        if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
586
            if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
587
                $classes[] = 'ver-element';
588
                $classes[] = 'ver-versions';
589
            } elseif (
590
                $this->getWorkspaceService()->hasPageRecordVersions(
591
                    $workspaceId,
592
                    $page['t3ver_oid'] ?: $page['uid']
593
                )
594
            ) {
595
                $classes[] = 'ver-versions';
596
            }
597
        }
598
599
        return implode(' ', $classes);
600
    }
601
602
    /**
603
     * Check if drag-move in the svg tree is allowed for the user
604
     *
605
     * @return bool
606
     */
607
    protected function isDragMoveAllowed(): bool
608
    {
609
        $backendUser = $this->getBackendUser();
610
        return $backendUser->isAdmin()
611
            || ($backendUser->check('tables_modify', 'pages') && $backendUser->checkLanguageAccess(0));
612
    }
613
614
    /**
615
     * @return WorkspaceService
616
     */
617
    protected function getWorkspaceService(): WorkspaceService
618
    {
619
        return GeneralUtility::makeInstance(WorkspaceService::class);
620
    }
621
622
    /**
623
     * @return BackendUserAuthentication
624
     */
625
    protected function getBackendUser(): BackendUserAuthentication
626
    {
627
        return $GLOBALS['BE_USER'];
628
    }
629
630
    /**
631
     * @return LanguageService|null
632
     */
633
    protected function getLanguageService(): ?LanguageService
634
    {
635
        return $GLOBALS['LANG'] ?? null;
636
    }
637
}
638