Completed
Push — master ( 8c4785...953cb0 )
by
unknown
14:00
created

TreeController::isDragMoveAllowed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types = 1);
3
namespace TYPO3\CMS\Backend\Controller\Page;
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
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use TYPO3\CMS\Backend\Configuration\BackendUserConfiguration;
21
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
25
use TYPO3\CMS\Core\Exception\Page\RootLineException;
26
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27
use TYPO3\CMS\Core\Http\JsonResponse;
28
use TYPO3\CMS\Core\Imaging\Icon;
29
use TYPO3\CMS\Core\Imaging\IconFactory;
30
use TYPO3\CMS\Core\Localization\LanguageService;
31
use TYPO3\CMS\Core\Site\SiteFinder;
32
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
33
use TYPO3\CMS\Core\Type\Bitmask\Permission;
34
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
use TYPO3\CMS\Core\Utility\RootlineUtility;
37
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
38
39
/**
40
 * Controller providing data to the page tree
41
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
42
 */
43
class TreeController
44
{
45
    /**
46
     * Option to use the nav_title field for outputting in the tree items, set via userTS.
47
     *
48
     * @var bool
49
     */
50
    protected $useNavTitle = false;
51
52
    /**
53
     * Option to prefix the page ID when outputting the tree items, set via userTS.
54
     *
55
     * @var bool
56
     */
57
    protected $addIdAsPrefix = false;
58
59
    /**
60
     * Option to prefix the domain name of sys_domains when outputting the tree items, set via userTS.
61
     *
62
     * @var bool
63
     */
64
    protected $addDomainName = false;
65
66
    /**
67
     * Option to add the rootline path above each mount point, set via userTS.
68
     *
69
     * @var bool
70
     */
71
    protected $showMountPathAboveMounts = false;
72
73
    /**
74
     * An array of background colors for a branch in the tree, set via userTS.
75
     *
76
     * @var array
77
     */
78
    protected $backgroundColors = [];
79
80
    /**
81
     * A list of pages not to be shown.
82
     *
83
     * @var array
84
     */
85
    protected $hiddenRecords = [];
86
87
    /**
88
     * Contains the state of all items that are expanded.
89
     *
90
     * @var array
91
     */
92
    protected $expandedState = [];
93
94
    /**
95
     * Instance of the icon factory, to be used for generating the items.
96
     *
97
     * @var IconFactory
98
     */
99
    protected $iconFactory;
100
101
    /**
102
     * Constructor to set up common objects needed in various places.
103
     */
104
    public function __construct()
105
    {
106
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
107
        $this->useNavTitle = (bool)($this->getBackendUser()->getTSConfig()['options.']['pageTree.']['showNavTitle'] ?? false);
108
    }
109
110
    /**
111
     * Returns page tree configuration in JSON
112
     *
113
     * @return ResponseInterface
114
     */
115
    public function fetchConfigurationAction(): ResponseInterface
116
    {
117
        $configuration = [
118
            'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
119
            'allowDragMove' => $this->isDragMoveAllowed(),
120
            'doktypes' => $this->getDokTypes(),
121
            'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE),
122
            'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)),
123
        ];
124
125
        return new JsonResponse($configuration);
126
    }
127
128
    /**
129
     * Returns the list of doktypes to display in page tree toolbar drag area
130
     *
131
     * Note: The list can be filtered by the user TypoScript
132
     * option "options.pageTree.doktypesToShowInNewPageDragArea".
133
     *
134
     * @return array
135
     */
136
    protected function getDokTypes(): array
137
    {
138
        $backendUser = $this->getBackendUser();
139
        $doktypeLabelMap = [];
140
        foreach ($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'] as $doktypeItemConfig) {
141
            if ($doktypeItemConfig[1] === '--div--') {
142
                continue;
143
            }
144
            $doktypeLabelMap[$doktypeItemConfig[1]] = $doktypeItemConfig[0];
145
        }
146
        $doktypes = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['pageTree.']['doktypesToShowInNewPageDragArea'] ?? '', true);
147
        $output = [];
148
        $allowedDoktypes = GeneralUtility::intExplode(',', $backendUser->groupData['pagetypes_select'], true);
149
        $isAdmin = $backendUser->isAdmin();
150
        // Early return if backend user may not create any doktype
151
        if (!$isAdmin && empty($allowedDoktypes)) {
152
            return $output;
153
        }
154
        foreach ($doktypes as $doktype) {
155
            if (!$isAdmin && !in_array($doktype, $allowedDoktypes, true)) {
156
                continue;
157
            }
158
            $label = htmlspecialchars($this->getLanguageService()->sL($doktypeLabelMap[$doktype]));
159
            $output[] = [
160
                'nodeType' => $doktype,
161
                'icon' => $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][$doktype] ?? '',
162
                'title' => $label,
163
                'tooltip' => $label
164
            ];
165
        }
166
        return $output;
167
    }
168
169
    /**
170
     * Returns JSON representing page tree
171
     *
172
     * @param ServerRequestInterface $request
173
     * @return ResponseInterface
174
     */
175
    public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
176
    {
177
        $userTsConfig = $this->getBackendUser()->getTSConfig();
178
        $this->hiddenRecords = GeneralUtility::intExplode(',', $userTsConfig['options.']['hideRecords.']['pages'] ?? '', true);
179
        $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
180
        $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
181
        $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
182
        $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
183
        $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
184
        $this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
185
        if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
186
            $this->expandedState = (array)$this->expandedState->stateHash;
187
        } else {
188
            $this->expandedState = $this->expandedState['stateHash'] ?: [];
189
        }
190
191
        // Fetching a part of a pagetree
192
        if (!empty($request->getQueryParams()['pid'])) {
193
            $entryPoints = [(int)$request->getQueryParams()['pid']];
194
        } else {
195
            $entryPoints = $this->getAllEntryPointPageTrees();
196
        }
197
        $items = [];
198
        foreach ($entryPoints as $page) {
199
            $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
200
        }
201
202
        return new JsonResponse($items);
203
    }
204
205
    /**
206
     * Sets a temporary mount point
207
     *
208
     * @param ServerRequestInterface $request
209
     * @return ResponseInterface
210
     * @throws \RuntimeException
211
     */
212
    public function setTemporaryMountPointAction(ServerRequestInterface $request): ResponseInterface
213
    {
214
        if (empty($request->getParsedBody()['pid'])) {
215
            throw new \RuntimeException(
216
                'Required "pid" parameter is missing.',
217
                1511792197
218
            );
219
        }
220
        $pid = (int)$request->getParsedBody()['pid'];
221
222
        $this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid;
223
        $this->getBackendUser()->writeUC();
224
        $response = [
225
            'mountPointPath' => $this->getMountPointPath($pid)
226
        ];
227
        return new JsonResponse($response);
228
    }
229
230
    /**
231
     * Converts nested tree structure produced by PageTreeRepository to a flat, one level array
232
     * and also adds visual representation information to the data.
233
     *
234
     * @param array $page
235
     * @param int $entryPoint
236
     * @param int $depth
237
     * @param array $inheritedData
238
     * @return array
239
     */
240
    protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0, array $inheritedData = []): array
241
    {
242
        $pageId = (int)$page['uid'];
243
        if (in_array($pageId, $this->hiddenRecords, true)) {
244
            return [];
245
        }
246
247
        $stopPageTree = !empty($page['php_tree_stop']) && $depth > 0;
248
        $identifier = $entryPoint . '_' . $pageId;
249
        $expanded = !empty($page['expanded']) || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier]);
250
        $backgroundColor = !empty($this->backgroundColors[$pageId]) ? $this->backgroundColors[$pageId] : ($inheritedData['backgroundColor'] ?? '');
251
252
        $suffix = '';
253
        $prefix = '';
254
        $nameSourceField = 'title';
255
        $visibleText = $page['title'];
256
        $tooltip = BackendUtility::titleAttribForPages($page, '', false);
257
        if ($pageId !== 0) {
258
            $icon = $this->iconFactory->getIconForRecord('pages', $page, Icon::SIZE_SMALL);
259
        } else {
260
            $icon = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL);
261
        }
262
263
        if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') {
264
            $nameSourceField = 'nav_title';
265
            $visibleText = $page['nav_title'];
266
        }
267
        if (trim($visibleText) === '') {
268
            $visibleText = htmlspecialchars('[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']');
269
        }
270
271
        if ($this->addDomainName && $page['is_siteroot']) {
272
            $domain = $this->getDomainNameForPage($pageId);
273
            $suffix = $domain !== '' ? ' [' . $domain . ']' : '';
274
        }
275
276
        $lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
277
        if (is_array($lockInfo)) {
278
            $tooltip .= ' - ' . $lockInfo['msg'];
279
        }
280
        if ($this->addIdAsPrefix) {
281
            $prefix = htmlspecialchars('[' . $pageId . '] ');
282
        }
283
284
        $items = [];
285
        $item = [
286
            // Used to track if the tree item is collapsed or not
287
            'stateIdentifier' => $identifier,
288
            'identifier' => $pageId,
289
            'depth' => $depth,
290
            'tip' => htmlspecialchars($tooltip),
291
            'icon' => $icon->getIdentifier(),
292
            'name' => $visibleText,
293
            'nameSourceField' => $nameSourceField,
294
            'mountPoint' => $entryPoint,
295
            'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId,
296
            'siblingsCount' => $page['siblingsCount'] ?? 1,
297
            'siblingsPosition' => $page['siblingsPosition'] ?? 1,
298
        ];
299
300
        if (!empty($page['_children'])) {
301
            $item['hasChildren'] = true;
302
        }
303
        if (!empty($prefix)) {
304
            $item['prefix'] = htmlspecialchars($prefix);
305
        }
306
        if (!empty($suffix)) {
307
            $item['suffix'] = htmlspecialchars($suffix);
308
        }
309
        if (is_array($lockInfo)) {
310
            $item['locked'] = true;
311
        }
312
        if ($icon->getOverlayIcon()) {
313
            $item['overlayIcon'] = $icon->getOverlayIcon()->getIdentifier();
314
        }
315
        if ($expanded) {
316
            $item['expanded'] = $expanded;
317
        }
318
        if ($backgroundColor) {
319
            $item['backgroundColor'] = htmlspecialchars($backgroundColor);
320
        }
321
        if ($stopPageTree) {
322
            $item['stopPageTree'] = $stopPageTree;
323
        }
324
        $class = $this->resolvePageCssClassNames($page);
325
        if (!empty($class)) {
326
            $item['class'] = $class;
327
        }
328
        if ($depth === 0) {
329
            $item['isMountPoint'] = true;
330
331
            if ($this->showMountPathAboveMounts) {
332
                $item['readableRootline'] = $this->getMountPointPath($pageId);
333
            }
334
        }
335
336
        $items[] = $item;
337
        if (!$stopPageTree && is_array($page['_children'])) {
338
            $siblingsCount = count($page['_children']);
339
            $siblingsPosition = 0;
340
            foreach ($page['_children'] as $child) {
341
                $child['siblingsCount'] = $siblingsCount;
342
                $child['siblingsPosition'] = ++$siblingsPosition;
343
                $items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
344
            }
345
        }
346
        return $items;
347
    }
348
349
    /**
350
     * Fetches all entry points for the page tree that the user is allowed to see
351
     *
352
     * @return array
353
     */
354
    protected function getAllEntryPointPageTrees(): array
355
    {
356
        $backendUser = $this->getBackendUser();
357
358
        $userTsConfig = $this->getBackendUser()->getTSConfig();
359
        $excludedDocumentTypes = GeneralUtility::intExplode(',', $userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? '', true);
360
361
        $additionalQueryRestrictions = [];
362
        if (!empty($excludedDocumentTypes)) {
363
            $additionalQueryRestrictions[] = $this->retrieveDocumentTypeExclusionRestriction($excludedDocumentTypes);
364
        }
365
366
        $repository = GeneralUtility::makeInstance(
367
            PageTreeRepository::class,
368
            (int)$backendUser->workspace,
0 ignored issues
show
Bug introduced by
(int)$backendUser->workspace of type integer is incompatible with the type array|array<mixed,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

368
            /** @scrutinizer ignore-type */ (int)$backendUser->workspace,
Loading history...
369
            [],
370
            $additionalQueryRestrictions
371
        );
372
373
        $entryPoints = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
374
        if ($entryPoints > 0) {
375
            $entryPoints = [$entryPoints];
376
        } else {
377
            $entryPoints = array_map('intval', $backendUser->returnWebmounts());
378
            $entryPoints = array_unique($entryPoints);
379
            if (empty($entryPoints)) {
380
                // use a virtual root
381
                // the real mount points will be fetched in getNodes() then
382
                // since those will be the "sub pages" of the virtual root
383
                $entryPoints = [0];
384
            }
385
        }
386
        if (empty($entryPoints)) {
387
            return [];
388
        }
389
390
        foreach ($entryPoints as $k => &$entryPoint) {
391
            if (in_array($entryPoint, $this->hiddenRecords, true)) {
392
                unset($entryPoints[$k]);
393
                continue;
394
            }
395
396
            if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
397
                try {
398
                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPoint)->get();
399
                } catch (RootLineException $e) {
400
                    $entryPointRootLine = [];
401
                }
402
                foreach ($entryPointRootLine as $rootLineEntry) {
403
                    $parentUid = $rootLineEntry['uid'];
404
                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPoint])) {
405
                        $this->backgroundColors[$entryPoint] = $this->backgroundColors[$parentUid];
406
                    }
407
                }
408
            }
409
410
            $entryPoint = $repository->getTree($entryPoint, function ($page) use ($backendUser) {
411
                // check each page if the user has permission to access it
412
                return $backendUser->doesUserHaveAccess($page, Permission::PAGE_SHOW);
413
            });
414
            if (!is_array($entryPoint)) {
415
                unset($entryPoints[$k]);
416
            }
417
        }
418
419
        return $entryPoints;
420
    }
421
422
    /**
423
     * @param int[] $excludedDocumentTypes
424
     * @return DocumentTypeExclusionRestriction|null
425
     */
426
    protected function retrieveDocumentTypeExclusionRestriction(array $excludedDocumentTypes): ?DocumentTypeExclusionRestriction
427
    {
428
        if (empty($excludedDocumentTypes)) {
429
            return null;
430
        }
431
432
        return GeneralUtility::makeInstance(DocumentTypeExclusionRestriction::class, $excludedDocumentTypes);
433
    }
434
435
    /**
436
     * Returns the first configured domain name for a page
437
     *
438
     * @param int $pageId
439
     * @return string
440
     */
441
    protected function getDomainNameForPage(int $pageId): string
442
    {
443
        $domain = '';
444
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
445
        try {
446
            $site = $siteFinder->getSiteByRootPageId($pageId);
447
            $domain = (string)$site->getBase();
448
        } catch (SiteNotFoundException $e) {
449
            // No site found
450
        }
451
452
        return $domain;
453
    }
454
455
    /**
456
     * Returns the mount point path for a temporary mount or the given id
457
     *
458
     * @param int $uid
459
     * @return string
460
     */
461
    protected function getMountPointPath(int $uid): string
462
    {
463
        if ($uid <= 0) {
464
            return '';
465
        }
466
        $rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
467
        array_shift($rootline);
468
        $path = [];
469
        foreach ($rootline as $rootlineElement) {
470
            $record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
471
            $text = $record['title'];
472
            if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
473
                $text = $record['nav_title'];
474
            }
475
            $path[] = htmlspecialchars($text);
476
        }
477
        return '/' . implode('/', $path);
478
    }
479
480
    /**
481
     * Fetches possible css class names to be used when a record was modified in a workspace
482
     *
483
     * @param array $page Page record (workspace overlaid)
484
     * @return string CSS class names to be applied
485
     */
486
    protected function resolvePageCssClassNames(array $page): string
487
    {
488
        $classes = [];
489
490
        if ($page['uid'] === 0) {
491
            return '';
492
        }
493
        $workspaceId = (int)$this->getBackendUser()->workspace;
494
        if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
495
            if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
496
                $classes[] = 'ver-element';
497
                $classes[] = 'ver-versions';
498
            } elseif (
499
                $this->getWorkspaceService()->hasPageRecordVersions(
500
                    $workspaceId,
501
                    $page['t3ver_oid'] ?: $page['uid']
502
                )
503
            ) {
504
                $classes[] = 'ver-versions';
505
            }
506
        }
507
508
        return implode(' ', $classes);
509
    }
510
511
    /**
512
     * Check if drag-move in the svg tree is allowed for the user
513
     *
514
     * @return bool
515
     */
516
    protected function isDragMoveAllowed(): bool
517
    {
518
        $backendUser = $this->getBackendUser();
519
        return $backendUser->isAdmin()
520
            || ($backendUser->check('tables_modify', 'pages') && $backendUser->checkLanguageAccess(0));
521
    }
522
523
    /**
524
     * @return WorkspaceService
525
     */
526
    protected function getWorkspaceService(): WorkspaceService
527
    {
528
        return GeneralUtility::makeInstance(WorkspaceService::class);
529
    }
530
531
    /**
532
     * @return BackendUserAuthentication
533
     */
534
    protected function getBackendUser(): BackendUserAuthentication
535
    {
536
        return $GLOBALS['BE_USER'];
537
    }
538
539
    /**
540
     * @return LanguageService|null
541
     */
542
    protected function getLanguageService(): ?LanguageService
543
    {
544
        return $GLOBALS['LANG'] ?? null;
545
    }
546
}
547