[BUGFIX] Reintroduce removed page tree TSconfig settings
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / Page / TreeController.php
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\PseudoSiteFinder;
32 use TYPO3\CMS\Core\Site\SiteFinder;
33 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
34 use TYPO3\CMS\Core\Type\Bitmask\Permission;
35 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
36 use TYPO3\CMS\Core\Utility\GeneralUtility;
37 use TYPO3\CMS\Core\Utility\RootlineUtility;
38 use TYPO3\CMS\Workspaces\Service\WorkspaceService;
39
40 /**
41 * Controller providing data to the page tree
42 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
43 */
44 class TreeController
45 {
46 /**
47 * Option to use the nav_title field for outputting in the tree items, set via userTS.
48 *
49 * @var bool
50 */
51 protected $useNavTitle = false;
52
53 /**
54 * Option to prefix the page ID when outputting the tree items, set via userTS.
55 *
56 * @var bool
57 */
58 protected $addIdAsPrefix = false;
59
60 /**
61 * Option to prefix the domain name of sys_domains when outputting the tree items, set via userTS.
62 *
63 * @var bool
64 */
65 protected $addDomainName = false;
66
67 /**
68 * Option to add the rootline path above each mount point, set via userTS.
69 *
70 * @var bool
71 */
72 protected $showMountPathAboveMounts = false;
73
74 /**
75 * An array of background colors for a branch in the tree, set via userTS.
76 *
77 * @var array
78 */
79 protected $backgroundColors = [];
80
81 /**
82 * A list of pages not to be shown.
83 *
84 * @var array
85 */
86 protected $hiddenRecords = [];
87
88 /**
89 * Contains the state of all items that are expanded.
90 *
91 * @var array
92 */
93 protected $expandedState = [];
94
95 /**
96 * Instance of the icon factory, to be used for generating the items.
97 *
98 * @var IconFactory
99 */
100 protected $iconFactory;
101
102 /**
103 * Constructor to set up common objects needed in various places.
104 */
105 public function __construct()
106 {
107 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
108 $this->useNavTitle = (bool)($this->getBackendUser()->getTSConfig()['options.']['pageTree.']['showNavTitle'] ?? false);
109 }
110
111 /**
112 * Returns page tree configuration in JSON
113 *
114 * @return ResponseInterface
115 */
116 public function fetchConfigurationAction(): ResponseInterface
117 {
118 $configuration = [
119 'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
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 $visibleText = GeneralUtility::fixed_lgd_cs($visibleText, (int)$this->getBackendUser()->uc['titleLen'] ?: 40);
271
272 if ($this->addDomainName && $page['is_siteroot']) {
273 $domain = $this->getDomainNameForPage($pageId);
274 $suffix = $domain !== '' ? ' [' . $domain . ']' : '';
275 }
276
277 $lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
278 if (is_array($lockInfo)) {
279 $tooltip .= ' - ' . $lockInfo['msg'];
280 }
281 if ($this->addIdAsPrefix) {
282 $prefix = htmlspecialchars('[' . $pageId . '] ');
283 }
284
285 $items = [];
286 $items[] = [
287 // Used to track if the tree item is collapsed or not
288 'stateIdentifier' => $identifier,
289 'identifier' => $pageId,
290 'depth' => $depth,
291 'tip' => htmlspecialchars($tooltip),
292 'hasChildren' => !empty($page['_children']),
293 'icon' => $icon->getIdentifier(),
294 'name' => $visibleText,
295 'nameSourceField' => $nameSourceField,
296 'prefix' => htmlspecialchars($prefix),
297 'suffix' => htmlspecialchars($suffix),
298 'locked' => is_array($lockInfo),
299 'overlayIcon' => $icon->getOverlayIcon() ? $icon->getOverlayIcon()->getIdentifier() : '',
300 'selectable' => true,
301 'expanded' => (bool)$expanded,
302 'checked' => false,
303 'backgroundColor' => htmlspecialchars($backgroundColor),
304 'stopPageTree' => $stopPageTree,
305 'class' => $this->resolvePageCssClassNames($page),
306 'readableRootline' => $depth === 0 && $this->showMountPathAboveMounts ? $this->getMountPointPath($pageId) : '',
307 'isMountPoint' => $depth === 0,
308 'mountPoint' => $entryPoint,
309 'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId,
310 ];
311 if (!$stopPageTree) {
312 foreach ($page['_children'] as $child) {
313 $items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
314 }
315 }
316 return $items;
317 }
318
319 /**
320 * Fetches all entry points for the page tree that the user is allowed to see
321 *
322 * @return array
323 */
324 protected function getAllEntryPointPageTrees(): array
325 {
326 $backendUser = $this->getBackendUser();
327
328 $userTsConfig = $this->getBackendUser()->getTSConfig();
329 $excludedDocumentTypes = GeneralUtility::intExplode(',', $userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? '', true);
330
331 $additionalPageTreeQueryRestrictions = [];
332 if (!empty($excludedDocumentTypes)) {
333 foreach ($excludedDocumentTypes as $excludedDocumentType) {
334 $additionalPageTreeQueryRestrictions[] = new DocumentTypeExclusionRestriction((int)$excludedDocumentType);
335 }
336 }
337
338 $repository = GeneralUtility::makeInstance(PageTreeRepository::class, (int)$backendUser->workspace, [], $additionalPageTreeQueryRestrictions);
339
340 $entryPoints = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
341 if ($entryPoints > 0) {
342 $entryPoints = [$entryPoints];
343 } else {
344 $entryPoints = array_map('intval', $backendUser->returnWebmounts());
345 $entryPoints = array_unique($entryPoints);
346 if (empty($entryPoints)) {
347 // use a virtual root
348 // the real mount points will be fetched in getNodes() then
349 // since those will be the "sub pages" of the virtual root
350 $entryPoints = [0];
351 }
352 }
353 if (empty($entryPoints)) {
354 return [];
355 }
356
357 foreach ($entryPoints as $k => &$entryPoint) {
358 if (in_array($entryPoint, $this->hiddenRecords, true)) {
359 unset($entryPoints[$k]);
360 continue;
361 }
362
363 if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
364 try {
365 $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPoint)->get();
366 } catch (RootLineException $e) {
367 $entryPointRootLine = [];
368 }
369 foreach ($entryPointRootLine as $rootLineEntry) {
370 $parentUid = $rootLineEntry['uid'];
371 if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPoint])) {
372 $this->backgroundColors[$entryPoint] = $this->backgroundColors[$parentUid];
373 }
374 }
375 }
376
377 $entryPoint = $repository->getTree($entryPoint, function ($page) use ($backendUser) {
378 // check each page if the user has permission to access it
379 return $backendUser->doesUserHaveAccess($page, Permission::PAGE_SHOW);
380 });
381 if (!is_array($entryPoint)) {
382 unset($entryPoints[$k]);
383 }
384 }
385
386 return $entryPoints;
387 }
388
389 /**
390 * Returns the first configured domain name for a page
391 *
392 * @param int $pageId
393 * @return string
394 */
395 protected function getDomainNameForPage(int $pageId): string
396 {
397 $domain = '';
398 $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
399 try {
400 $site = $siteFinder->getSiteByRootPageId($pageId);
401 $domain = (string)$site->getBase();
402 } catch (SiteNotFoundException $e) {
403 // No site found, let's see if it is a legacy-pseudo-site
404 $pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
405 try {
406 $site = $pseudoSiteFinder->getSiteByRootPageId($pageId);
407 $domain = trim((string)$site->getBase(), '/');
408 } catch (SiteNotFoundException $e) {
409 // No pseudo-site found either
410 }
411 }
412
413 return $domain;
414 }
415
416 /**
417 * Returns the mount point path for a temporary mount or the given id
418 *
419 * @param int $uid
420 * @return string
421 */
422 protected function getMountPointPath(int $uid): string
423 {
424 if ($uid <= 0) {
425 return '';
426 }
427 $rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
428 array_shift($rootline);
429 $path = [];
430 foreach ($rootline as $rootlineElement) {
431 $record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
432 $text = $record['title'];
433 if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
434 $text = $record['nav_title'];
435 }
436 $path[] = htmlspecialchars($text);
437 }
438 return '/' . implode('/', $path);
439 }
440
441 /**
442 * Fetches possible css class names to be used when a record was modified in a workspace
443 *
444 * @param array $page Page record (workspace overlaid)
445 * @return string CSS class names to be applied
446 */
447 protected function resolvePageCssClassNames(array $page): string
448 {
449 $classes = [];
450
451 if ($page['uid'] === 0) {
452 return '';
453 }
454 $workspaceId = (int)$this->getBackendUser()->workspace;
455 if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
456 if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
457 $classes[] = 'ver-element';
458 $classes[] = 'ver-versions';
459 } elseif (
460 $this->getWorkspaceService()->hasPageRecordVersions(
461 $workspaceId,
462 $page['t3ver_oid'] ?: $page['uid']
463 )
464 ) {
465 $classes[] = 'ver-versions';
466 }
467 }
468
469 return implode(' ', $classes);
470 }
471
472 /**
473 * @return WorkspaceService
474 */
475 protected function getWorkspaceService(): WorkspaceService
476 {
477 return GeneralUtility::makeInstance(WorkspaceService::class);
478 }
479
480 /**
481 * @return BackendUserAuthentication
482 */
483 protected function getBackendUser(): BackendUserAuthentication
484 {
485 return $GLOBALS['BE_USER'];
486 }
487
488 /**
489 * @return LanguageService|null
490 */
491 protected function getLanguageService(): ?LanguageService
492 {
493 return $GLOBALS['LANG'] ?? null;
494 }
495 }