Completed
Push — master ( d32f9d...c64bee )
by
unknown
26:12
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
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
     * Constructor to set up common objects needed in various places.
107
     */
108
    public function __construct()
109
    {
110
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
111
        $this->useNavTitle = (bool)($this->getBackendUser()->getTSConfig()['options.']['pageTree.']['showNavTitle'] ?? false);
112
    }
113
114
    /**
115
     * Returns page tree configuration in JSON
116
     *
117
     * @return ResponseInterface
118
     */
119
    public function fetchConfigurationAction(): ResponseInterface
120
    {
121
        $configuration = [
122
            'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
123
            'allowDragMove' => $this->isDragMoveAllowed(),
124
            'doktypes' => $this->getDokTypes(),
125
            'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE),
126
            'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)),
127
        ];
128
129
        return new JsonResponse($configuration);
130
    }
131
132
    /**
133
     * Returns the list of doktypes to display in page tree toolbar drag area
134
     *
135
     * Note: The list can be filtered by the user TypoScript
136
     * option "options.pageTree.doktypesToShowInNewPageDragArea".
137
     *
138
     * @return array
139
     */
140
    protected function getDokTypes(): array
141
    {
142
        $backendUser = $this->getBackendUser();
143
        $doktypeLabelMap = [];
144
        foreach ($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'] as $doktypeItemConfig) {
145
            if ($doktypeItemConfig[1] === '--div--') {
146
                continue;
147
            }
148
            $doktypeLabelMap[$doktypeItemConfig[1]] = $doktypeItemConfig[0];
149
        }
150
        $doktypes = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['pageTree.']['doktypesToShowInNewPageDragArea'] ?? '', true);
151
        $output = [];
152
        $allowedDoktypes = GeneralUtility::intExplode(',', $backendUser->groupData['pagetypes_select'], true);
153
        $isAdmin = $backendUser->isAdmin();
154
        // Early return if backend user may not create any doktype
155
        if (!$isAdmin && empty($allowedDoktypes)) {
156
            return $output;
157
        }
158
        foreach ($doktypes as $doktype) {
159
            if (!$isAdmin && !in_array($doktype, $allowedDoktypes, true)) {
160
                continue;
161
            }
162
            $label = htmlspecialchars($this->getLanguageService()->sL($doktypeLabelMap[$doktype]));
163
            $output[] = [
164
                'nodeType' => $doktype,
165
                'icon' => $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][$doktype] ?? '',
166
                'title' => $label,
167
                'tooltip' => $label
168
            ];
169
        }
170
        return $output;
171
    }
172
173
    /**
174
     * Returns JSON representing page tree
175
     *
176
     * @param ServerRequestInterface $request
177
     * @return ResponseInterface
178
     */
179
    public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
180
    {
181
        $userTsConfig = $this->getBackendUser()->getTSConfig();
182
        $this->hiddenRecords = GeneralUtility::intExplode(',', $userTsConfig['options.']['hideRecords.']['pages'] ?? '', true);
183
        $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
184
        $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
185
        $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
186
        $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
187
        $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
188
        $this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
189
        if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
190
            $this->expandedState = (array)$this->expandedState->stateHash;
191
        } else {
192
            $this->expandedState = $this->expandedState['stateHash'] ?: [];
193
        }
194
195
        // Fetching a part of a pagetree
196
        if (!empty($request->getQueryParams()['pid'])) {
197
            $entryPoints = [(int)$request->getQueryParams()['pid']];
198
        } else {
199
            $entryPoints = $this->getAllEntryPointPageTrees();
200
        }
201
        $items = [];
202
        foreach ($entryPoints as $page) {
203
            $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
204
        }
205
206
        return new JsonResponse($items);
207
    }
208
209
    /**
210
     * Sets a temporary mount point
211
     *
212
     * @param ServerRequestInterface $request
213
     * @return ResponseInterface
214
     * @throws \RuntimeException
215
     */
216
    public function setTemporaryMountPointAction(ServerRequestInterface $request): ResponseInterface
217
    {
218
        if (empty($request->getParsedBody()['pid'])) {
219
            throw new \RuntimeException(
220
                'Required "pid" parameter is missing.',
221
                1511792197
222
            );
223
        }
224
        $pid = (int)$request->getParsedBody()['pid'];
225
226
        $this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid;
227
        $this->getBackendUser()->writeUC();
228
        $response = [
229
            'mountPointPath' => $this->getMountPointPath($pid)
230
        ];
231
        return new JsonResponse($response);
232
    }
233
234
    /**
235
     * Converts nested tree structure produced by PageTreeRepository to a flat, one level array
236
     * and also adds visual representation information to the data.
237
     *
238
     * @param array $page
239
     * @param int $entryPoint
240
     * @param int $depth
241
     * @param array $inheritedData
242
     * @return array
243
     */
244
    protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0, array $inheritedData = []): array
245
    {
246
        $backendUser = $this->getBackendUser();
247
        $pageId = (int)$page['uid'];
248
        if (in_array($pageId, $this->hiddenRecords, true)) {
249
            return [];
250
        }
251
        if ($pageId === 0 && !$backendUser->isAdmin()) {
252
            return [];
253
        }
254
255
        $stopPageTree = !empty($page['php_tree_stop']) && $depth > 0;
256
        $identifier = $entryPoint . '_' . $pageId;
257
        $expanded = !empty($page['expanded']) || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier]);
258
        $backgroundColor = !empty($this->backgroundColors[$pageId]) ? $this->backgroundColors[$pageId] : ($inheritedData['backgroundColor'] ?? '');
259
260
        $suffix = '';
261
        $prefix = '';
262
        $nameSourceField = 'title';
263
        $visibleText = $page['title'];
264
        $tooltip = BackendUtility::titleAttribForPages($page, '', false);
265
        if ($pageId !== 0) {
266
            $icon = $this->iconFactory->getIconForRecord('pages', $page, Icon::SIZE_SMALL);
267
        } else {
268
            $icon = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL);
269
        }
270
271
        if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') {
272
            $nameSourceField = 'nav_title';
273
            $visibleText = $page['nav_title'];
274
        }
275
        if (trim($visibleText) === '') {
276
            $visibleText = htmlspecialchars('[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']');
277
        }
278
279
        if ($this->addDomainName && $page['is_siteroot']) {
280
            $domain = $this->getDomainNameForPage($pageId);
281
            $suffix = $domain !== '' ? ' [' . $domain . ']' : '';
282
        }
283
284
        $lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
285
        if (is_array($lockInfo)) {
286
            $tooltip .= ' - ' . $lockInfo['msg'];
287
        }
288
        if ($this->addIdAsPrefix) {
289
            $prefix = htmlspecialchars('[' . $pageId . '] ');
290
        }
291
292
        $items = [];
293
        $item = [
294
            // Used to track if the tree item is collapsed or not
295
            'stateIdentifier' => $identifier,
296
            'identifier' => $pageId,
297
            'depth' => $depth,
298
            'tip' => htmlspecialchars($tooltip),
299
            'icon' => $icon->getIdentifier(),
300
            'name' => $visibleText,
301
            'nameSourceField' => $nameSourceField,
302
            'mountPoint' => $entryPoint,
303
            'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId,
304
            'siblingsCount' => $page['siblingsCount'] ?? 1,
305
            'siblingsPosition' => $page['siblingsPosition'] ?? 1,
306
            'allowDelete' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_DELETE),
307
            'allowEdit' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_EDIT)
308
                && $backendUser->check('tables_modify', 'pages')
309
                && $backendUser->checkLanguageAccess(0)
310
        ];
311
312
        if (!empty($page['_children'])) {
313
            $item['hasChildren'] = true;
314
        }
315
        if (!empty($prefix)) {
316
            $item['prefix'] = htmlspecialchars($prefix);
317
        }
318
        if (!empty($suffix)) {
319
            $item['suffix'] = htmlspecialchars($suffix);
320
        }
321
        if (is_array($lockInfo)) {
322
            $item['locked'] = true;
323
        }
324
        if ($icon->getOverlayIcon()) {
325
            $item['overlayIcon'] = $icon->getOverlayIcon()->getIdentifier();
326
        }
327
        if ($expanded) {
328
            $item['expanded'] = $expanded;
329
        }
330
        if ($backgroundColor) {
331
            $item['backgroundColor'] = htmlspecialchars($backgroundColor);
332
        }
333
        if ($stopPageTree) {
334
            $item['stopPageTree'] = $stopPageTree;
335
        }
336
        $class = $this->resolvePageCssClassNames($page);
337
        if (!empty($class)) {
338
            $item['class'] = $class;
339
        }
340
        if ($depth === 0) {
341
            $item['isMountPoint'] = true;
342
343
            if ($this->showMountPathAboveMounts) {
344
                $item['readableRootline'] = $this->getMountPointPath($pageId);
345
            }
346
        }
347
348
        $items[] = $item;
349
        if (!$stopPageTree && is_array($page['_children'])) {
350
            $siblingsCount = count($page['_children']);
351
            $siblingsPosition = 0;
352
            foreach ($page['_children'] as $child) {
353
                $child['siblingsCount'] = $siblingsCount;
354
                $child['siblingsPosition'] = ++$siblingsPosition;
355
                $items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
356
            }
357
        }
358
        return $items;
359
    }
360
361
    /**
362
     * Fetches all entry points for the page tree that the user is allowed to see
363
     *
364
     * @return array
365
     */
366
    protected function getAllEntryPointPageTrees(): array
367
    {
368
        $backendUser = $this->getBackendUser();
369
        $userTsConfig = $backendUser->getTSConfig();
370
        $excludedDocumentTypes = GeneralUtility::intExplode(',', $userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? '', true);
371
372
        $additionalQueryRestrictions = [];
373
        if (!empty($excludedDocumentTypes)) {
374
            $additionalQueryRestrictions[] = GeneralUtility::makeInstance(DocumentTypeExclusionRestriction::class, $excludedDocumentTypes);
375
        }
376
        $additionalQueryRestrictions[] = GeneralUtility::makeInstance(PagePermissionRestriction::class, GeneralUtility::makeInstance(Context::class)->getAspect('backend.user'), Permission::PAGE_SHOW);
377
378
        $repository = GeneralUtility::makeInstance(
379
            PageTreeRepository::class,
380
            (int)$backendUser->workspace,
381
            [],
382
            $additionalQueryRestrictions
383
        );
384
385
        $entryPoints = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
386
        if ($entryPoints > 0) {
387
            $entryPoints = [$entryPoints];
388
        } else {
389
            $entryPoints = array_map('intval', $backendUser->returnWebmounts());
390
            $entryPoints = array_unique($entryPoints);
391
            if (empty($entryPoints)) {
392
                // use a virtual root
393
                // the real mount points will be fetched in getNodes() then
394
                // since those will be the "sub pages" of the virtual root
395
                $entryPoints = [0];
396
            }
397
        }
398
        if (empty($entryPoints)) {
399
            return [];
400
        }
401
402
        foreach ($entryPoints as $k => &$entryPoint) {
403
            if (in_array($entryPoint, $this->hiddenRecords, true)) {
404
                unset($entryPoints[$k]);
405
                continue;
406
            }
407
408
            if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
409
                try {
410
                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPoint)->get();
411
                } catch (RootLineException $e) {
412
                    $entryPointRootLine = [];
413
                }
414
                foreach ($entryPointRootLine as $rootLineEntry) {
415
                    $parentUid = $rootLineEntry['uid'];
416
                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPoint])) {
417
                        $this->backgroundColors[$entryPoint] = $this->backgroundColors[$parentUid];
418
                    }
419
                }
420
            }
421
422
            $entryPoint = $repository->getTree($entryPoint, null, $entryPoints);
423
            if (!is_array($entryPoint)) {
424
                unset($entryPoints[$k]);
425
            }
426
        }
427
428
        return $entryPoints;
429
    }
430
431
    /**
432
     * Returns the first configured domain name for a page
433
     *
434
     * @param int $pageId
435
     * @return string
436
     */
437
    protected function getDomainNameForPage(int $pageId): string
438
    {
439
        $domain = '';
440
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
441
        try {
442
            $site = $siteFinder->getSiteByRootPageId($pageId);
443
            $domain = (string)$site->getBase();
444
        } catch (SiteNotFoundException $e) {
445
            // No site found
446
        }
447
448
        return $domain;
449
    }
450
451
    /**
452
     * Returns the mount point path for a temporary mount or the given id
453
     *
454
     * @param int $uid
455
     * @return string
456
     */
457
    protected function getMountPointPath(int $uid): string
458
    {
459
        if ($uid <= 0) {
460
            return '';
461
        }
462
        $rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
463
        array_shift($rootline);
464
        $path = [];
465
        foreach ($rootline as $rootlineElement) {
466
            $record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
467
            $text = $record['title'];
468
            if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
469
                $text = $record['nav_title'];
470
            }
471
            $path[] = htmlspecialchars($text);
472
        }
473
        return '/' . implode('/', $path);
474
    }
475
476
    /**
477
     * Fetches possible css class names to be used when a record was modified in a workspace
478
     *
479
     * @param array $page Page record (workspace overlaid)
480
     * @return string CSS class names to be applied
481
     */
482
    protected function resolvePageCssClassNames(array $page): string
483
    {
484
        $classes = [];
485
486
        if ($page['uid'] === 0) {
487
            return '';
488
        }
489
        $workspaceId = (int)$this->getBackendUser()->workspace;
490
        if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
491
            if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
492
                $classes[] = 'ver-element';
493
                $classes[] = 'ver-versions';
494
            } elseif (
495
                $this->getWorkspaceService()->hasPageRecordVersions(
496
                    $workspaceId,
497
                    $page['t3ver_oid'] ?: $page['uid']
498
                )
499
            ) {
500
                $classes[] = 'ver-versions';
501
            }
502
        }
503
504
        return implode(' ', $classes);
505
    }
506
507
    /**
508
     * Check if drag-move in the svg tree is allowed for the user
509
     *
510
     * @return bool
511
     */
512
    protected function isDragMoveAllowed(): bool
513
    {
514
        $backendUser = $this->getBackendUser();
515
        return $backendUser->isAdmin()
516
            || ($backendUser->check('tables_modify', 'pages') && $backendUser->checkLanguageAccess(0));
517
    }
518
519
    /**
520
     * @return WorkspaceService
521
     */
522
    protected function getWorkspaceService(): WorkspaceService
523
    {
524
        return GeneralUtility::makeInstance(WorkspaceService::class);
525
    }
526
527
    /**
528
     * @return BackendUserAuthentication
529
     */
530
    protected function getBackendUser(): BackendUserAuthentication
531
    {
532
        return $GLOBALS['BE_USER'];
533
    }
534
535
    /**
536
     * @return LanguageService|null
537
     */
538
    protected function getLanguageService(): ?LanguageService
539
    {
540
        return $GLOBALS['LANG'] ?? null;
541
    }
542
}
543