Commit b439597c authored by Tymoteusz Motylewski's avatar Tymoteusz Motylewski Committed by Georg Ringer
Browse files

[TASK] Speed up page tree loading

Instead of doing manual queries for each tree level, and checking
for children on each tree level, the page tree now fetches all pages
the user has access to, and checks if this works out.

Resolves: #83233
Releases: master
Change-Id: I45a6b834ef1fe71e5748dfc8de9bcf6dad8172c9
Reviewed-on: https://review.typo3.org/54887


Reviewed-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
parent ebeac423
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "4c0c03e1d8cb80b05f541e6691629430",
"content-hash": "9ccdad435278ac728c4fda07a6e55d58",
"packages": [
{
"name": "cogpowered/finediff",
......@@ -4281,16 +4281,16 @@
},
{
"name": "typo3/testing-framework",
"version": "2.0.0",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/TYPO3/testing-framework.git",
"reference": "cc024cfd812e501430daba493c00d6cdcfb97c83"
"reference": "c99bee70611359bc58a5c3d8a9aab7dd86bd3cb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/cc024cfd812e501430daba493c00d6cdcfb97c83",
"reference": "cc024cfd812e501430daba493c00d6cdcfb97c83",
"url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/c99bee70611359bc58a5c3d8a9aab7dd86bd3cb0",
"reference": "c99bee70611359bc58a5c3d8a9aab7dd86bd3cb0",
"shasum": ""
},
"require": {
......@@ -4340,7 +4340,7 @@
"tests",
"typo3"
],
"time": "2017-12-03T14:49:01+00:00"
"time": "2017-12-06T15:03:20+00:00"
},
{
"name": "webmozart/assert",
......
......@@ -18,18 +18,95 @@ namespace TYPO3\CMS\Backend\Controller\Page;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Controller\UserSettingsController;
use TYPO3\CMS\Backend\Tree\Pagetree\Commands;
use TYPO3\CMS\Backend\Tree\Pagetree\ExtdirectTreeDataProvider;
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
/**
* Controller providing data to the page tree
*/
class TreeController
{
/**
* Option to use the nav_title field for outputting in the tree items, set via userTS.
*
* @var bool
*/
protected $useNavTitle = false;
/**
* Option to prefix the page ID when outputting the tree items, set via userTS.
*
* @var bool
*/
protected $addIdAsPrefix = false;
/**
* Option to prefix the domain name of sys_domains when outputting the tree items, set via userTS.
*
* @var bool
*/
protected $addDomainName = false;
/**
* Option to add the rootline path above each mount point, set via userTS.
*
* @var bool
*/
protected $showMountPathAboveMounts = false;
/**
* An array of background colors for a branch in the tree, set via userTS.
*
* @var array
*/
protected $backgroundColors = [];
/**
* A list of pages not to be shown.
*
* @var array
*/
protected $hiddenRecords = [];
/**
* Contains the state of all items that are expanded.
*
* @var array
*/
protected $expandedState = [];
/**
* Associative array containing all pageIds as key, and domain names as values.
*
* @var array|null
*/
protected $domains = null;
/**
* Instance of the icon factory, to be used for generating the items.
*
* @var IconFactory
*/
protected $iconFactory;
/**
* Constructor to set up common objects needed in various places.
*/
public function __construct()
{
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->useNavTitle = (bool)$this->getBackendUser()->getTSConfigVal('options.pageTree.showNavTitle');
}
/**
* Returns page tree configuration in JSON
......@@ -42,7 +119,7 @@ class TreeController
'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
'doktypes' => $this->getDokTypes(),
'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE),
'temporaryMountPoint' => Commands::getMountPointPath(),
'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)),
];
return GeneralUtility::makeInstance(JsonResponse::class, $configuration);
......@@ -67,7 +144,7 @@ class TreeController
}
$doktypes = GeneralUtility::intExplode(',', $this->getBackendUser()->getTSConfigVal('options.pageTree.doktypesToShowInNewPageDragArea'));
$output = [];
$allowedDoktypes = GeneralUtility::intExplode(',', $GLOBALS['BE_USER']->groupData['pagetypes_select'], true);
$allowedDoktypes = GeneralUtility::intExplode(',', $this->getBackendUser()->groupData['pagetypes_select'], true);
$isAdmin = $this->getBackendUser()->isAdmin();
// Early return if backend user may not create any doktype
if (!$isAdmin && empty($allowedDoktypes)) {
......@@ -92,71 +169,35 @@ class TreeController
* Returns JSON representing page tree
*
* @param ServerRequestInterface $request
* @throws \RuntimeException
* @return ResponseInterface
*/
public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
{
$dataProvider = GeneralUtility::makeInstance(ExtdirectTreeDataProvider::class);
$node = new \stdClass();
$node->id = 'root';
if (!empty($request->getQueryParams()['pid'])) {
$node->id = $request->getQueryParams()['pid'];
$this->hiddenRecords = GeneralUtility::intExplode(',', $this->getBackendUser()->getTSConfigVal('options.hideRecords.pages'), true);
$this->backgroundColors = $this->getBackendUser()->getTSConfigProp('options.pageTree.backgroundColor');
$this->addIdAsPrefix = (bool)$this->getBackendUser()->getTSConfigVal('options.pageTree.showPageIdWithTitle');
$this->addDomainName = (bool)$this->getBackendUser()->getTSConfigVal('options.pageTree.showDomainNameWithTitle');
$this->showMountPathAboveMounts = (bool)$this->getBackendUser()->getTSConfigVal('options.pageTree.showPathAboveMounts');
$userSettingsController = GeneralUtility::makeInstance(UserSettingsController::class);
$this->expandedState = $userSettingsController->process('get', 'BackendComponents.States.Pagetree');
if (is_object($this->expandedState->stateHash)) {
$this->expandedState = (array)$this->expandedState->stateHash;
} else {
$this->expandedState = $this->expandedState['stateHash'] ?: [];
}
$nodeArray = $dataProvider->getNextTreeLevel($node->id, $node);
//@todo refactor the PHP data provider side. Now we're using the old pagetree code and flatten the array afterwards
$items = $this->nodeToFlatArray($nodeArray);
return GeneralUtility::makeInstance(JsonResponse::class, $items);
}
/**
* Converts nested tree structure produced by ExtdirectTreeDataProvider to a flat, one level array
*
* @param array $nodeArray
* @param int $depth
* @param array $inheritedData
* @return array
*/
protected function nodeToFlatArray(array $nodeArray, int $depth = 0, array $inheritedData = []): array
{
$userSettingsController = GeneralUtility::makeInstance(UserSettingsController::class);
$state = $userSettingsController->process('get', 'BackendComponents.States.Pagetree');
// Fetching a part of a pagetree
if (!empty($request->getQueryParams()['pid'])) {
$entryPoints = [(int)$request->getQueryParams()['pid']];
} else {
$entryPoints = $this->getAllEntryPointPageTrees();
}
$items = [];
foreach ($nodeArray as $key => $node) {
$hexId = dechex($node['nodeData']['id']);
$expanded = $node['nodeData']['expanded'] || (isset($state['stateHash'][$hexId]) && $state['stateHash'][$hexId]);
$backgroundColor = !empty($node['nodeData']['backgroundColor']) ? $node['nodeData']['backgroundColor'] : ($inheritedData['backgroundColor'] ?? '');
$items[] = [
'identifier' => $node['nodeData']['id'],
'depth' => $depth,
'hasChildren' => !empty($node['children']),
'icon' => $node['icon'],
'name' => $node['editableText'],
'tip' => $node['qtip'],
'nameSourceField' => $node['t3TextSourceField'],
'alias' => $node['alias'],
'prefix' => $node['prefix'],
'suffix' => $node['suffix'],
'overlayIcon' => $node['overlayIcon'],
'selectable' => true,
'expanded' => (bool)$expanded,
'checked' => false,
'backgroundColor' => htmlspecialchars($backgroundColor),
'stopPageTree' => $node['nodeData']['stopPageTree'],
//used to mark versioned records, see $row['_CSSCLASS'], e.g. ver-element
'class' => (string)$node['cls'],
'readableRootline' => $node['nodeData']['readableRootline'],
'isMountPoint' => $node['nodeData']['isMountPoint'],
'mountPoint' => $node['nodeData']['mountPoint'],
'workspaceId' => $node['nodeData']['workspaceId'],
];
if (!empty($node['children'])) {
$items = array_merge($items, $this->nodeToFlatArray($node['children'], $depth + 1, ['backgroundColor' => $backgroundColor]));
}
foreach ($entryPoints as $page) {
$items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
}
return $items;
return GeneralUtility::makeInstance(JsonResponse::class, $items);
}
/**
......@@ -177,17 +218,241 @@ class TreeController
$pid = (int)$request->getParsedBody()['pid'];
$this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid;
$this->getBackendUser()->writeUC(static::getBackendUser()->uc);
$this->getBackendUser()->writeUC();
$response = [
'mountPointPath' => Commands::getMountPointPath(),
'mountPointPath' => $this->getMountPointPath($pid)
];
return GeneralUtility::makeInstance(JsonResponse::class, $response);
}
/**
* Converts nested tree structure produced by PageTreeRepository to a flat, one level array
* and also adds visual representation information to the data.
*
* @param array $page
* @param int $entryPoint
* @param int $depth
* @param array $inheritedData
* @return array
*/
protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0, array $inheritedData = []): array
{
$pageId = (int)$page['uid'];
if (in_array($pageId, $this->hiddenRecords, true)) {
return [];
}
$stopPageTree = $page['php_tree_stop'] && $depth > 0;
$identifier = $entryPoint . '_' . $pageId;
$expanded = $page['expanded'] || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier]);
$backgroundColor = $this->backgroundColors[$pageId] ?: ($inheritedData['backgroundColor'] ?? '');
$suffix = '';
$prefix = '';
$nameSourceField = 'title';
$visibleText = $page['title'];
$tooltip = BackendUtility::titleAttribForPages($page, '', false);
if ($pageId !== 0) {
$icon = $this->iconFactory->getIconForRecord('pages', $page, Icon::SIZE_SMALL);
} else {
$icon = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL);
}
if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') {
$nameSourceField = 'nav_title';
$visibleText = $page['nav_title'];
}
if (trim($visibleText) === '') {
$visibleText = '[' . $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
}
$visibleText = GeneralUtility::fixed_lgd_cs($visibleText, (int)$this->getBackendUser()->uc['titleLen'] ?: 40);
if ($this->addDomainName) {
$domain = $this->getDomainNameForPage($pageId);
$suffix = $domain !== '' ? ' [' . $domain . ']' : '';
}
$lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
if (is_array($lockInfo)) {
$tooltip .= ' - ' . $lockInfo['msg'];
$prefix = '<span class="typo3-pagetree-status">' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>';
}
if ($this->addIdAsPrefix) {
$prefix .= htmlspecialchars('[' . $pageId . '] ');
}
$items = [];
$items[] = [
// Used to track if the tree item is collapsed or not
'stateIdentifier' => $identifier,
'identifier' => $pageId,
'depth' => $depth,
'tip' => htmlspecialchars($tooltip),
'hasChildren' => !empty($page['_children']),
'icon' => $icon->getIdentifier(),
'name' => htmlspecialchars($visibleText),
'nameSourceField' => $nameSourceField,
'alias' => htmlspecialchars($page['alias'] ?: ''),
'prefix' => htmlspecialchars($prefix),
'suffix' => htmlspecialchars($suffix),
'overlayIcon' => $icon->getOverlayIcon() ? $icon->getOverlayIcon()->getIdentifier() : '',
'selectable' => true,
'expanded' => (bool)$expanded,
'checked' => false,
'backgroundColor' => htmlspecialchars($backgroundColor),
'stopPageTree' => $stopPageTree,
'class' => $this->resolvePageCssClassNames($page),
'readableRootline' => ($depth === 0 && $this->showMountPathAboveMounts ? $this->getMountPointPath($pageId) : ''),
'isMountPoint' => $depth === 0,
'mountPoint' => $entryPoint,
'workspaceId' => $page['t3ver_oid'] ?: $pageId,
];
if (!$stopPageTree) {
foreach ($page['_children'] as $child) {
$items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
}
}
return $items;
}
/**
* Fetches all entry points for the page tree that the user is allowed to see
*
* @return array
*/
protected function getAllEntryPointPageTrees(): array
{
$backendUser = $this->getBackendUser();
$repository = GeneralUtility::makeInstance(PageTreeRepository::class, (int)$backendUser->workspace);
$entryPoints = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
if ($entryPoints > 0) {
$entryPoints = [$entryPoints];
} else {
$entryPoints = array_map('intval', $backendUser->returnWebmounts());
$entryPoints = array_unique($entryPoints);
if (empty($entryPoints)) {
// use a virtual root
// the real mount points will be fetched in getNodes() then
// since those will be the "sub pages" of the virtual root
$entryPoints = [0];
}
}
if (empty($entryPoints)) {
return [];
}
foreach ($entryPoints as $k => &$entryPoint) {
if (in_array($entryPoint, $this->hiddenRecords, true)) {
unset($entryPoints[$k]);
continue;
}
$entryPoint = $repository->getTree($entryPoint, function ($page) use ($backendUser) {
// check each page if the user has permission to access it
return $backendUser->doesUserHaveAccess($page, Permission::PAGE_SHOW);
});
if (!is_array($entryPoint)) {
unset($entryPoints[$k]);
}
}
return $entryPoints;
}
/**
* Returns the first configured domain name for a page
*
* @param int $pageId
* @return string
*/
protected function getDomainNameForPage(int $pageId): string
{
if (!is_array($this->domains)) {
$this->domains = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_domain');
$result = $queryBuilder
->select('domainName', 'pid')
->from('sys_domain')
->where(
$queryBuilder->expr()->neq('redirectTo', $queryBuilder->createNamedParameter('""'))
)
->orderBy('sorting')
->execute()
->fetchAll();
foreach ($result as $domain) {
$domainPid = (int)$domain['pid'];
if (!isset($this->domains[$domainPid])) {
$this->domains[$domainPid] = $domain['domainName'];
}
}
}
return $this->domains[$pageId] ?? '';
}
/**
* Returns the mount point path for a temporary mount or the given id
*
* @param int $uid
* @return string
*/
protected function getMountPointPath(int $uid): string
{
if ($uid <= 0) {
return '';
}
$rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
array_shift($rootline);
$path = [];
foreach ($rootline as $rootlineElement) {
$record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
$text = $record['title'];
if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
$text = $record['nav_title'];
}
$path[] = htmlspecialchars($text);
}
return '/' . implode('/', $path);
}
/**
* Fetches possible css class names to be used when a record was modified in a workspace
*
* @param array $page Page record (workspace overlaid)
* @return string CSS class names to be applied
*/
protected function resolvePageCssClassNames(array $page): string
{
$classes = [];
$workspaceId = (int)$this->getBackendUser()->workspace;
if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
$classes[] = 'ver-element';
$classes[] = 'ver-versions';
} elseif (
$this->getWorkspaceService()->hasPageRecordVersions(
$workspaceId,
$page['t3ver_oid'] ?: $page['uid']
)
) {
$classes[] = 'ver-versions';
}
}
return implode(' ', $classes);
}
/**
* @return WorkspaceService
*/
protected function getWorkspaceService(): WorkspaceService
{
return GeneralUtility::makeInstance(WorkspaceService::class);
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser()
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
......
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Backend\Tree\Repository;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Fetches ALL pages in the page tree, possibly overlaid with the workspace
* in a sorted way.
*
* This works agnostic of the Backend User, allows to be used in FE as well in the future.
*
* @internal this class is not public API yet, as it needs to be proven stable enough first.
*/
class PageTreeRepository
{
/**
* Fields to be queried from the database
*
* @var string[]
*/
protected $fields = [
'uid',
'pid',
'sorting',
'starttime',
'endtime',
'hidden',
'fe_group',
'title',
'nav_title',
'nav_hide',
'alias',
'php_tree_stop',
'doktype',
'is_siteroot',
'module',
't3ver_oid',
't3ver_id',
't3ver_wsid',
't3ver_label',
't3ver_state',
't3ver_stage',
't3ver_tstamp',
't3ver_move_id',
];
/**
* The workspace ID to operate on
*
* @var int
*/
protected $currentWorkspace = 0;
/**
* Full page tree when selected without permissions applied.
*
* @var array
*/
protected $fullPageTree = [];
/**
* @param int $workspaceId the workspace ID to be checked for.