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