Completed
Push — master ( d9b8ee...637b36 )
by
unknown
29:19 queued 11:04
created

TreeController::resolvePageCssClassNames()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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