From fb61db41d5fec1c791c3541567ae97c11006ae45 Mon Sep 17 00:00:00 2001
From: Tymoteusz Motylewski <t.motylewski@gmail.com>
Date: Wed, 23 Oct 2019 19:28:21 +0200
Subject: [PATCH] [BUGFIX] Limit amount of data fetched by the page tree

Page tree will fetch just 2 levels of pages plus pages which
are expanded on the initial load.
Next levels are fetched on demand via Ajax when expanding the node.
Search work server side now (hit enter). To clear search, click on "x"
button.
If you select a page when filtering, it's kept selected after
removing the filter.

Releases: master, 10.4, 9.5
Resolves: #88943
Resolves: #88098
Resolves: #88259
Change-Id: Ie83839ce801c509f24c1e2c1dc516bce9599d55e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62086
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Sybille Peters <sypets@gmx.de>
Tested-by: Dennis Prinse <dennis@dennisprinse.com>
Tested-by: Marcus Schwemer <ms@schwemer.de>
Tested-by: Uwe Trotzek <trotzek@citeq.de>
Tested-by: Richard Haeser <richard@maxserv.com>
Reviewed-by: Marcus Schwemer <ms@schwemer.de>
Reviewed-by: Richard Haeser <richard@maxserv.com>
---
 .../Controller/Page/TreeController.php        | 187 +++++++---
 .../Tree/Repository/PageTreeRepository.php    | 338 ++++++++++++++++++
 .../Configuration/Backend/AjaxRoutes.php      |   6 +
 .../Public/JavaScript/PageTree/PageTree.js    | 107 ++++++
 .../JavaScript/PageTree/PageTreeElement.js    |   3 +
 .../JavaScript/PageTree/PageTreeToolbar.js    |  39 +-
 .../Controller/Page/TreeControllerTest.php    | 153 +++++---
 .../Private/Language/locallang_core.xlf       |   2 +-
 8 files changed, 703 insertions(+), 132 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
index 464181a9d67a..b035ecaefd80 100644
--- a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
+++ b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
@@ -102,13 +102,47 @@ class TreeController
      */
     protected $iconFactory;
 
+    /**
+     * Number of tree levels which should be returned on the first page tree load
+     *
+     * @var int
+     */
+    protected $levelsToFetch = 2;
+
+    /**
+     * When set to true all nodes returend by API will be expanded
+     * @var bool
+     */
+    protected $expandAllNodes = false;
+
     /**
      * Constructor to set up common objects needed in various places.
      */
     public function __construct()
     {
         $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        $this->useNavTitle = (bool)($this->getBackendUser()->getTSConfig()['options.']['pageTree.']['showNavTitle'] ?? false);
+    }
+
+    protected function initializeConfiguration()
+    {
+        $userTsConfig = $this->getBackendUser()->getTSConfig();
+        $this->hiddenRecords = GeneralUtility::intExplode(
+            ',',
+            $userTsConfig['options.']['hideRecords.']['pages'] ?? '',
+            true
+        );
+        $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
+        $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
+        $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
+        $this->useNavTitle = (bool)($userTsConfig['options.']['pageTree.']['showNavTitle'] ?? false);
+        $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
+        $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
+        $this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
+        if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
+            $this->expandedState = (array)$this->expandedState->stateHash;
+        } else {
+            $this->expandedState = $this->expandedState['stateHash'] ?: [];
+        }
     }
 
     /**
@@ -178,29 +212,51 @@ class TreeController
      */
     public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
     {
-        $userTsConfig = $this->getBackendUser()->getTSConfig();
-        $this->hiddenRecords = GeneralUtility::intExplode(',', $userTsConfig['options.']['hideRecords.']['pages'] ?? '', true);
-        $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
-        $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
-        $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
-        $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
-        $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
-        $this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
-        if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
-            $this->expandedState = (array)$this->expandedState->stateHash;
-        } else {
-            $this->expandedState = $this->expandedState['stateHash'] ?: [];
-        }
+        $this->initializeConfiguration();
 
-        // Fetching a part of a pagetree
+        $items = [];
         if (!empty($request->getQueryParams()['pid'])) {
-            $entryPoints = [(int)$request->getQueryParams()['pid']];
+            // Fetching a part of a page tree
+            $entryPoints = $this->getAllEntryPointPageTrees((int)$request->getQueryParams()['pid']);
+            $mountPid = (int)($request->getQueryParams()['mount'] ?? 0);
+            $parentDepth = (int)($request->getQueryParams()['pidDepth'] ?? 0);
+            $this->levelsToFetch = $parentDepth + $this->levelsToFetch;
+            foreach ($entryPoints as $page) {
+                $items = array_merge($items, $this->pagesToFlatArray($page, $mountPid, $parentDepth));
+            }
         } else {
             $entryPoints = $this->getAllEntryPointPageTrees();
+            foreach ($entryPoints as $page) {
+                $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
+            }
+        }
+
+        return new JsonResponse($items);
+    }
+
+    /**
+     * Returns JSON representing page tree filtered by keyword
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     */
+    public function filterDataAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $searchQuery = $request->getQueryParams()['q'] ?? '';
+        if (trim($searchQuery) === '') {
+            return new JsonResponse([]);
         }
+
+        $this->initializeConfiguration();
+        $this->expandAllNodes = true;
+
         $items = [];
+        $entryPoints = $this->getAllEntryPointPageTrees(0, $searchQuery);
+
         foreach ($entryPoints as $page) {
-            $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
+            if (!empty($page)) {
+                $items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
+            }
         }
 
         return new JsonResponse($items);
@@ -254,7 +310,10 @@ class TreeController
 
         $stopPageTree = !empty($page['php_tree_stop']) && $depth > 0;
         $identifier = $entryPoint . '_' . $pageId;
-        $expanded = !empty($page['expanded']) || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier]);
+        $expanded = !empty($page['expanded'])
+            || (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier])
+            || $this->expandAllNodes;
+
         $backgroundColor = !empty($this->backgroundColors[$pageId]) ? $this->backgroundColors[$pageId] : ($inheritedData['backgroundColor'] ?? '');
 
         $suffix = '';
@@ -309,8 +368,11 @@ class TreeController
                 && $backendUser->checkLanguageAccess(0)
         ];
 
-        if (!empty($page['_children'])) {
+        if (!empty($page['_children']) || $this->getPageTreeRepository()->hasChildren($pageId)) {
             $item['hasChildren'] = true;
+            if ($depth >= $this->levelsToFetch) {
+                $page = $this->getPageTreeRepository()->getTreeLevels($page, 1);
+            }
         }
         if (!empty($prefix)) {
             $item['prefix'] = htmlspecialchars($prefix);
@@ -324,7 +386,7 @@ class TreeController
         if ($icon->getOverlayIcon()) {
             $item['overlayIcon'] = $icon->getOverlayIcon()->getIdentifier();
         }
-        if ($expanded) {
+        if ($expanded && is_array($page['_children']) && !empty($page['_children'])) {
             $item['expanded'] = $expanded;
         }
         if ($backgroundColor) {
@@ -346,9 +408,10 @@ class TreeController
         }
 
         $items[] = $item;
-        if (!$stopPageTree && is_array($page['_children'])) {
+        if (!$stopPageTree && is_array($page['_children']) && !empty($page['_children']) && ($depth < $this->levelsToFetch || $expanded)) {
             $siblingsCount = count($page['_children']);
             $siblingsPosition = 0;
+            $items[key($items)]['loaded'] = true;
             foreach ($page['_children'] as $child) {
                 $child['siblingsCount'] = $siblingsCount;
                 $child['siblingsPosition'] = ++$siblingsPosition;
@@ -358,12 +421,7 @@ class TreeController
         return $items;
     }
 
-    /**
-     * Fetches all entry points for the page tree that the user is allowed to see
-     *
-     * @return array
-     */
-    protected function getAllEntryPointPageTrees(): array
+    protected function getPageTreeRepository(): PageTreeRepository
     {
         $backendUser = $this->getBackendUser();
         $userTsConfig = $backendUser->getTSConfig();
@@ -375,57 +433,94 @@ class TreeController
         }
         $additionalQueryRestrictions[] = GeneralUtility::makeInstance(PagePermissionRestriction::class, GeneralUtility::makeInstance(Context::class)->getAspect('backend.user'), Permission::PAGE_SHOW);
 
-        $repository = GeneralUtility::makeInstance(
+        return GeneralUtility::makeInstance(
             PageTreeRepository::class,
             (int)$backendUser->workspace,
             [],
             $additionalQueryRestrictions
         );
+    }
 
-        $entryPoints = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
-        if ($entryPoints > 0) {
-            $entryPoints = [$entryPoints];
+    /**
+     * Fetches all pages for all tree entry points the user is allowed to see
+     *
+     * @param int $startPid
+     * @param string $query The search query can either be a string to be found in the title or the nav_title of a page or the uid of a page.
+     * @return array
+     */
+    protected function getAllEntryPointPageTrees(int $startPid = 0, string $query = ''): array
+    {
+        $backendUser = $this->getBackendUser();
+        $entryPointId = $startPid > 0 ? $startPid : (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
+        if ($entryPointId > 0) {
+            $entryPointIds = [$entryPointId];
         } else {
-            $entryPoints = array_map('intval', $backendUser->returnWebmounts());
-            $entryPoints = array_unique($entryPoints);
-            if (empty($entryPoints)) {
+            //watch out for deleted pages returned as webmount
+            $entryPointIds = array_map('intval', $backendUser->returnWebmounts());
+            $entryPointIds = array_unique($entryPointIds);
+            if (empty($entryPointIds)) {
                 // 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];
+                $entryPointIds = [0];
             }
         }
-        if (empty($entryPoints)) {
+        if (empty($entryPointIds)) {
             return [];
         }
+        $repository = $this->getPageTreeRepository();
+
+        if ($query !== '') {
+            $this->levelsToFetch = 999;
+            $repository->fetchFilteredTree($query);
+        }
 
-        foreach ($entryPoints as $k => &$entryPoint) {
-            if (in_array($entryPoint, $this->hiddenRecords, true)) {
-                unset($entryPoints[$k]);
+        $entryPointRecords = [];
+        foreach ($entryPointIds as $k => $entryPointId) {
+            if (in_array($entryPointId, $this->hiddenRecords, true)) {
                 continue;
             }
 
             if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
                 try {
-                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPoint)->get();
+                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPointId)->get();
                 } catch (RootLineException $e) {
                     $entryPointRootLine = [];
                 }
                 foreach ($entryPointRootLine as $rootLineEntry) {
                     $parentUid = $rootLineEntry['uid'];
-                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPoint])) {
-                        $this->backgroundColors[$entryPoint] = $this->backgroundColors[$parentUid];
+                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPointId])) {
+                        $this->backgroundColors[$entryPointId] = $this->backgroundColors[$parentUid];
                     }
                 }
             }
+            if ($entryPointId === 0) {
+                $entryPointRecord = [
+                    'uid' => 0,
+                    'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
+                ];
+            } else {
+                $permClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
+                $entryPointRecord = BackendUtility::getRecord('pages', $entryPointId, '*', $permClause);
+
+                if ($entryPointRecord !== null && !$this->getBackendUser()->isInWebMount($entryPointId)) {
+                    $entryPointRecord = null;
+                }
+            }
+            if ($entryPointRecord) {
+                if ($query === '') {
+                    $entryPointRecord = $repository->getTreeLevels($entryPointRecord, $this->levelsToFetch);
+                } else {
+                    $entryPointRecord = $repository->getTree($entryPointRecord['uid'], null, $entryPointIds);
+                }
+            }
 
-            $entryPoint = $repository->getTree($entryPoint, null, $entryPoints);
-            if (!is_array($entryPoint)) {
-                unset($entryPoints[$k]);
+            if (is_array($entryPointRecord) && !empty($entryPointRecord)) {
+                $entryPointRecords[$k] = $entryPointRecord;
             }
         }
 
-        return $entryPoints;
+        return $entryPointRecords;
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
index d4d6ed45dc6c..ea5f25f12b99 100644
--- a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
+++ b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
@@ -17,11 +17,15 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Tree\Repository;
 
+use Doctrine\DBAL\Connection;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryHelper;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
 use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
 
@@ -119,6 +123,7 @@ class PageTreeRepository
      *
      * @param int $entryPoint the page ID to fetch the tree for
      * @param callable $callback a callback to be used to check for permissions and filter out pages not to be included.
+     * @param array $dbMounts
      * @return array
      */
     public function getTree(int $entryPoint, callable $callback = null, array $dbMounts = []): array
@@ -155,6 +160,133 @@ class PageTreeRepository
         }
     }
 
+    /**
+     * Get the page tree based on a given page record and a given depth
+     *
+     * @param array $pageTree The page record of the top level page you want to get the page tree of
+     * @param int $depth Number of levels to fetch
+     * @return array An array with page records and their children
+     */
+    public function getTreeLevels(array $pageTree, int $depth): array
+    {
+        $parentPageIds = [$pageTree['uid']];
+        $groupedAndSortedPagesByPid = [];
+        for ($i = 0; $i < $depth; $i++) {
+            if (empty($parentPageIds)) {
+                break;
+            }
+            $pageRecords = $this->getChildPages($parentPageIds);
+
+            $groupedAndSortedPagesByPid = $this->groupAndSortPages($pageRecords, $groupedAndSortedPagesByPid);
+
+            $parentPageIds = array_column($pageRecords, 'uid');
+        }
+        $this->addChildrenToPage($pageTree, $groupedAndSortedPagesByPid);
+        return $pageTree;
+    }
+
+    /**
+     * Get the child pages from the given parent pages
+     *
+     * @param array $parentPageIds
+     * @return array
+     */
+    protected function getChildPages(array $parentPageIds): array
+    {
+        $pageRecords = $this->getChildPageRecords($parentPageIds);
+
+        foreach ($pageRecords as &$pageRecord) {
+            $pageRecord['uid'] = (int)$pageRecord['uid'];
+
+            if ($this->currentWorkspace > 0) {
+                if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
+                    $liveRecord = BackendUtility::getRecord('pages', $pageRecord['t3ver_move_id']);
+                    $pageRecord['uid'] = (int)$pageRecord['t3ver_move_id'];
+                    $pageRecord['title'] = $liveRecord['title'];
+                }
+
+                if ((int)$pageRecord['t3ver_oid'] > 0) {
+                    $liveRecord = BackendUtility::getRecord('pages', $pageRecord['t3ver_oid']);
+
+                    $pageRecord['uid'] = (int)$pageRecord['t3ver_oid'];
+                    $pageRecord['pid'] = (int)$liveRecord['pid'];
+                }
+            }
+        }
+        unset($pageRecord);
+
+        return $pageRecords;
+    }
+
+    /**
+     * Retrieve the page records based on the given parent page ids
+     *
+     * @param array $parentPageIds
+     * @return array
+     */
+    protected function getChildPageRecords(array $parentPageIds): array
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable('pages');
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
+            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->currentWorkspace));
+
+        if (!empty($this->additionalQueryRestrictions)) {
+            foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
+                $queryBuilder->getRestrictions()->add($additionalQueryRestriction);
+            }
+        }
+
+        $pageRecords = $queryBuilder
+            ->select(...$this->fields)
+            ->from('pages')
+            ->where(
+                $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
+                $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($parentPageIds, Connection::PARAM_INT_ARRAY))
+            )
+            ->execute()
+            ->fetchAll();
+
+        // This is necessary to resolve all IDs in a workspace
+        if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
+            $livePageIds = array_column($pageRecords, 'uid');
+            // Resolve placeholders of workspace versions
+            $resolver = GeneralUtility::makeInstance(
+                PlainDataResolver::class,
+                'pages',
+                $livePageIds
+            );
+            $resolver->setWorkspaceId($this->currentWorkspace);
+            $resolver->setKeepDeletePlaceholder(false);
+            $resolver->setKeepMovePlaceholder(false);
+            $resolver->setKeepLiveIds(false);
+            $recordIds = $resolver->get();
+
+            if (!empty($recordIds)) {
+                $queryBuilder->getRestrictions()->removeAll();
+                $pageRecords = $queryBuilder
+                    ->select(...$this->fields)
+                    ->from('pages')
+                    ->where(
+                        $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY))
+                    )
+                    ->execute()
+                    ->fetchAll();
+            }
+        }
+
+        return $pageRecords;
+    }
+
+    public function hasChildren(int $pid): bool
+    {
+        $pageRecords = $this->getChildPageRecords([$pid]);
+
+        return !empty($pageRecords);
+    }
+
     /**
      * Fetch all non-deleted pages, regardless of permissions. That's why it's internal.
      *
@@ -313,4 +445,210 @@ class PageTreeRepository
         }
         return [];
     }
+
+    /**
+     * Retrieve the page tree based on the given search filter
+     *
+     * @param string $searchFilter
+     * @return array
+     */
+    public function fetchFilteredTree(string $searchFilter): array
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable('pages');
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
+        if ($this->getBackendUser()->workspace === 0) {
+            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class));
+        }
+
+        if (!empty($this->additionalQueryRestrictions)) {
+            foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
+                $queryBuilder->getRestrictions()->add($additionalQueryRestriction);
+            }
+        }
+
+        $expressionBuilder = $queryBuilder->expr();
+
+        $queryBuilder = $queryBuilder
+            ->select(...$this->fields)
+            ->from('pages')
+            ->where(
+                // Only show records in default language
+                $expressionBuilder->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
+            );
+
+        $queryBuilder->where(
+            QueryHelper::stripLogicalOperatorPrefix($this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW))
+        );
+        $searchParts = $expressionBuilder->orX();
+        if (is_numeric($searchFilter) && $searchFilter > 0) {
+            $searchParts->add(
+                $expressionBuilder->eq('uid', $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_INT))
+            );
+        }
+        $searchFilter = '%' . $queryBuilder->escapeLikeWildcards($searchFilter) . '%';
+
+        $searchWhereAlias = $expressionBuilder->orX(
+            $expressionBuilder->like(
+                'nav_title',
+                $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
+            ),
+            $expressionBuilder->like(
+                'title',
+                $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
+            )
+        );
+        $searchParts->add($searchWhereAlias);
+
+        $queryBuilder->andWhere($searchParts);
+        $pageRecords = $queryBuilder->execute()
+            ->fetchAll();
+
+        $pages = [];
+        foreach ($pageRecords as $page) {
+            $pages[$page['uid']] = $page;
+        }
+
+        if ($this->getBackendUser()->workspace !== 0) {
+            foreach (array_unique(array_column($pages, 't3ver_oid')) as $t3verOid) {
+                if ($t3verOid !== 0) {
+                    unset($pages[$t3verOid]);
+                }
+            }
+        }
+        unset($pageRecords);
+
+        $pages = $this->filterPagesOnMountPoints($pages, $this->getAllowedMountPoints());
+
+        $groupedAndSortedPagesByPid = $this->groupAndSortPages($pages);
+
+        $this->fullPageTree = [
+            'uid' => 0,
+            'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
+        ];
+        $this->addChildrenToPage($this->fullPageTree, $groupedAndSortedPagesByPid);
+
+        return $this->fullPageTree;
+    }
+
+    /**
+     * Filter all records outside of the allowed mount points
+     *
+     * @param array $pages
+     * @param array $mountPoints
+     * @return array
+     */
+    protected function filterPagesOnMountPoints(array $pages, array $mountPoints): array
+    {
+        foreach ($pages as $key => $pageRecord) {
+            $rootline = BackendUtility::BEgetRootLine(
+                $pageRecord['uid'],
+                '',
+                $this->getBackendUser()->workspace != 0,
+                $this->fields
+            );
+            $rootline = array_reverse($rootline);
+            if (!in_array(0, $mountPoints, true)) {
+                $isInsideMountPoints = false;
+                foreach ($rootline as $rootlineElement) {
+                    if (in_array((int)$rootlineElement['uid'], $mountPoints, true)) {
+                        $isInsideMountPoints = true;
+                        break;
+                    }
+                }
+                if (!$isInsideMountPoints) {
+                    unset($pages[$key]);
+                    //skip records outside of the allowed mount points
+                    continue;
+                }
+            }
+
+            $inFilteredRootline = false;
+            $amountOfRootlineElements = count($rootline);
+            for ($i = 0; $i < $amountOfRootlineElements; ++$i) {
+                $rootlineElement = $rootline[$i];
+                $rootlineElement['uid'] = (int)$rootlineElement['uid'];
+                $isInWebMount = false;
+                if ($rootlineElement['uid'] > 0) {
+                    $isInWebMount = (int)$this->getBackendUser()->isInWebMount($rootlineElement);
+                }
+
+                if (!$isInWebMount
+                    || ($rootlineElement['uid'] === (int)$mountPoints[0]
+                        && $rootlineElement['uid'] !== $isInWebMount)
+                ) {
+                    continue;
+                }
+                if ($this->getBackendUser()->isAdmin() ||($rootlineElement['uid'] === $isInWebMount
+                        && in_array($rootlineElement['uid'], $mountPoints, true))
+                ) {
+                    $inFilteredRootline = true;
+                }
+                if (!$inFilteredRootline) {
+                    continue;
+                }
+
+                if (!isset($pages[$rootlineElement['uid']])) {
+                    $pages[$rootlineElement['uid']] = $rootlineElement;
+                }
+            }
+        }
+        // Make sure the mountpoints show up in page tree even when parent pages are not accessible pages
+        foreach ($mountPoints as $mountPoint) {
+            if ($mountPoint !== 0) {
+                if (!array_key_exists($mountPoint, $pages)) {
+                    $pages[$mountPoint] = BackendUtility::getRecord('pages', $mountPoint);
+                }
+                $pages[$mountPoint]['pid'] = 0;
+            }
+        }
+
+        return $pages;
+    }
+
+    /**
+     * Group pages by parent page and sort pages based on sorting property
+     *
+     * @param array $pages
+     * @param array $groupedAndSortedPagesByPid
+     * @return array
+     */
+    protected function groupAndSortPages(array $pages, $groupedAndSortedPagesByPid = []): array
+    {
+        foreach ($pages as $key => $pageRecord) {
+            $parentPageId = (int)$pageRecord['pid'];
+            $sorting = (int)$pageRecord['sorting'];
+            while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
+                $sorting++;
+            }
+            $groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
+        }
+
+        return $groupedAndSortedPagesByPid;
+    }
+
+    /**
+     * Get allowed mountpoints. Returns temporary mountpoint when temporary mountpoint is used
+     * @return array
+     */
+    protected function getAllowedMountPoints(): array
+    {
+        $mountPoints = (int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0);
+        if (!$mountPoints) {
+            $mountPoints = array_map('intval', $this->getBackendUser()->returnWebmounts());
+            return array_unique($mountPoints);
+        }
+        return [$mountPoints];
+    }
+
+    /**
+     * @return BackendUserAuthentication
+     */
+    protected function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
 }
diff --git a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
index a3955dc73411..37053a5033db 100644
--- a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
+++ b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
@@ -95,6 +95,12 @@ return [
         'target' => Controller\Page\TreeController::class . '::fetchDataAction'
     ],
 
+    // Get data for page tree
+    'page_tree_filter' => [
+        'path' => '/page/tree/filterData',
+        'target' => Controller\Page\TreeController::class . '::filterDataAction'
+    ],
+
     // Get page tree configuration
     'page_tree_configuration' => [
         'path' => '/page/tree/fetchConfiguration',
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js
index bd7c8da35a6b..9ce08e22355e 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js
@@ -32,12 +32,14 @@ define(['jquery',
      */
     var PageTree = function() {
       SvgTree.call(this);
+      this.originalNodes = [];
       this.settings.defaultProperties = {
         hasChildren: false,
         nameSourceField: 'title',
         prefix: '',
         suffix: '',
         locked: false,
+        loaded: false,
         overlayIcon: '',
         selectable: true,
         expanded: false,
@@ -51,6 +53,7 @@ define(['jquery',
     };
 
     PageTree.prototype = Object.create(SvgTree.prototype);
+
     var _super_ = SvgTree.prototype;
 
     /**
@@ -204,6 +207,16 @@ define(['jquery',
       return this.nodes[0];
     };
 
+    /**
+     * Finds node by its stateIdentifier (e.g. "0_360")
+     * @return {Node}
+     */
+    PageTree.prototype.getNodeByIdentifier = function(identifier) {
+      return this.nodes.find(function (node) {
+        return node.stateIdentifier === identifier;
+      });
+    };
+
     /**
      * Observer for the selectedNode event
      *
@@ -282,10 +295,59 @@ define(['jquery',
     };
 
     PageTree.prototype.showChildren = function(node) {
+      this.loadChildrenOfNode(node);
       _super_.showChildren(node);
       Persistent.set('BackendComponents.States.Pagetree.stateHash.' + node.stateIdentifier, 1);
     };
 
+    /**
+     * Loads child nodes via Ajax (used when expanding a collapesed node)
+     *
+     * @param parentNode
+     * @return {boolean}
+     */
+    PageTree.prototype.loadChildrenOfNode = function(parentNode) {
+      if (parentNode.loaded) {
+        return;
+      }
+      var _this = this;
+      _this.nodesAddPlaceholder();
+      d3.json(_this.settings.dataUrl + '&pid=' + parentNode.identifier + '&mount=' + parentNode.mountPoint + '&pidDepth=' + parentNode.depth, function(error, json) {
+          if (error) {
+            var title = TYPO3.lang.pagetree_networkErrorTitle;
+            var desc = TYPO3.lang.pagetree_networkErrorDesc;
+
+            if (error && error.target && (error.target.status || error.target.statusText)) {
+              title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
+            }
+
+            Notification.error(
+              title,
+              desc);
+
+            _this.nodesRemovePlaceholder();
+            throw error;
+          }
+
+          var nodes = Array.isArray(json) ? json : [];
+          //first element is a parent
+          nodes.shift();
+          var index = _this.nodes.indexOf(parentNode) + 1;
+          //adding fetched node after parent
+          nodes.forEach(function (node, offset) {
+            _this.nodes.splice(index + offset, 0, node);
+          });
+
+          parentNode.loaded = true;
+          _this.setParametersNode();
+          _this.prepareDataForVisibleNodes();
+          _this.update();
+          _this.nodesRemovePlaceholder();
+          _this.switchFocusNode(parentNode);
+        });
+
+    };
+
     PageTree.prototype.updateNodeBgClass = function(nodeBg) {
       return _super_.updateNodeBgClass.call(this, nodeBg).call(this.dragDrop.drag());
     };
@@ -388,6 +450,51 @@ define(['jquery',
         });
     };
 
+    PageTree.prototype.filterTree = function(searchQuery) {
+      var _this = this;
+      _this.nodesAddPlaceholder();
+
+      d3.json(_this.settings.filterUrl + '&q=' + searchQuery, function(error, json) {
+        if (error) {
+          var title = TYPO3.lang.pagetree_networkErrorTitle;
+          var desc = TYPO3.lang.pagetree_networkErrorDesc;
+
+          if (error && error.target && (error.target.status || error.target.statusText)) {
+            title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
+          }
+
+          Notification.error(
+            title,
+            desc);
+
+          _this.nodesRemovePlaceholder();
+          throw error;
+        }
+
+        var nodes = Array.isArray(json) ? json : [];
+        if (nodes.length > 0) {
+          if (_this.originalNodes.length === 0) {
+            _this.originalNodes = JSON.stringify(_this.nodes);
+          }
+          _this.replaceData(nodes);
+        }
+        _this.nodesRemovePlaceholder();
+      });
+    };
+
+    PageTree.prototype.resetFilter = function() {
+        if (this.originalNodes.length > 0) {
+          var currentlySelected = this.getSelectedNodes()[0];
+          this.nodes = JSON.parse(this.originalNodes);
+          this.originalNodes = '';
+          if (currentlySelected) {
+            this.selectNode(this.getNodeByIdentifier(currentlySelected.stateIdentifier));
+          }
+        } else {
+          this.refreshTree();
+        }
+    };
+
     PageTree.prototype.setTemporaryMountPoint = function(pid) {
       var params = 'pid=' + pid;
       var _this = this;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js
index 56b0f3160e3b..3ca16ece4bf0 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js
@@ -71,11 +71,14 @@ define(['jquery',
         });
 
         var dataUrl = top.TYPO3.settings.ajaxUrls.page_tree_data;
+        var filterUrl = top.TYPO3.settings.ajaxUrls.page_tree_filter;
+
         var configurationUrl = top.TYPO3.settings.ajaxUrls.page_tree_configuration;
 
         $.ajax({url: configurationUrl}).done(function(configuration) {
           tree.initialize($element.find('#typo3-pagetree-tree'), $.extend(configuration, {
             dataUrl: dataUrl,
+            filterUrl: filterUrl,
             showIcons: true
           }));
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeToolbar.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeToolbar.js
index 83cb935dc497..5a6185c22165 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeToolbar.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeToolbar.js
@@ -66,13 +66,6 @@ define(['jquery',
        * @type {jQuery}
        */
       this.template = null;
-
-      /**
-       * Nodes stored encoded before tree gets filtered
-       *
-       * @type {string}
-       */
-      this.originalNodes = '';
     };
 
     /**
@@ -182,7 +175,9 @@ define(['jquery',
           if (input) {
             input.clearable({
               onClear: function (input) {
-                $(input).trigger('input');
+                _this.tree.resetFilter();
+                _this.tree.prepareDataForVisibleNodes();
+                _this.tree.update();
               }
             });
           }
@@ -204,8 +199,10 @@ define(['jquery',
         }
       });
 
-      $toolbar.find(this.settings.searchInput).on('input', function() {
-        _this.search.call(_this, this);
+      $toolbar.find(this.settings.searchInput).on('keypress', function(e) {
+        if(e.keyCode === 13 || e.which === 13) {
+          _this.search.call(_this, this);
+        }
       });
 
       $toolbar.find('[data-toggle="tooltip"]').tooltip();
@@ -232,29 +229,11 @@ define(['jquery',
     TreeToolbar.prototype.search = function(input) {
       var _this = this;
       var name = $(input).val().trim();
-
       if (name !== '') {
-        if (this.originalNodes.length === 0) {
-          this.originalNodes = JSON.stringify(this.tree.nodes);
-        }
-
-        this.tree.nodes[0].expanded = false;
-        this.tree.nodes.forEach(function (node) {
-          var regex = new RegExp(name, 'i');
-          if (node.identifier.toString() === name || regex.test(node.name) || regex.test(node.alias || '')) {
-            _this.showParents(node);
-            node.expanded = true;
-            node.hidden = false;
-          } else if (node.depth !== 0) {
-            node.hidden = true;
-            node.expanded = false;
-          }
-        });
+        _this.tree.filterTree(name);
       } else {
-        this.tree.nodes = JSON.parse(this.originalNodes);
-        this.originalNodes = '';
+        _this.tree.resetFilter();
       }
-
       this.tree.prepareDataForVisibleNodes();
       this.tree.update();
     };
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
index e066862f370d..f7e32822bf8c 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
@@ -160,26 +160,7 @@ class TreeControllerTest extends FunctionalTestCase
                             [
                                 'uid' => 1520,
                                 'title' => 'Forecasts',
-                                '_children' => [
-                                    [
-                                        'uid' => 1521,
-                                        'title' => 'Current Year',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                    [
-                                        'uid' => 1522,
-                                        'title' => 'Next Year',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                    [
-                                        'uid' => 1523,
-                                        'title' => 'Five Years',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                ],
+                                '_children' => [],
                             ],
                             [
                                 'uid' => 1530,
@@ -230,6 +211,67 @@ class TreeControllerTest extends FunctionalTestCase
         self::assertEquals($expected, $actual);
     }
 
+    /**
+     * @test
+     */
+    public function getSubtreeForAccessiblePage()
+    {
+        $actual = $this->subject->_call('getAllEntryPointPageTrees', 1200);
+        $keepProperties = array_flip(['uid', 'title', '_children']);
+        $actual = $this->sortTreeArray($actual);
+        $actual = $this->normalizeTreeArray($actual, $keepProperties);
+
+        $expected = [
+            [
+                'uid' => 1200,
+                'title' => 'EN: Features',
+                '_children' => [
+                    [
+                        'uid' => 1210,
+                        'title' => 'EN: Frontend Editing',
+                        '_children' => [
+                        ],
+                    ],
+                    [
+                        'uid' => 1230,
+                        'title' => 'EN: Managing content',
+                        '_children' => [
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        self::assertEquals($expected, $actual);
+    }
+
+    /**
+     * @test
+     */
+    public function getSubtreeForNonAccessiblePage()
+    {
+        $actual = $this->subject->_call('getAllEntryPointPageTrees', 1510);
+        $keepProperties = array_flip(['uid', 'title', '_children']);
+        $actual = $this->sortTreeArray($actual);
+        $actual = $this->normalizeTreeArray($actual, $keepProperties);
+
+        $expected = [];
+        self::assertEquals($expected, $actual);
+    }
+
+    /**
+     * @test
+     */
+    public function getSubtreeForPageOutsideMountPoint()
+    {
+        $actual = $this->subject->_call('getAllEntryPointPageTrees', 7000);
+        $keepProperties = array_flip(['uid', 'title', '_children']);
+        $actual = $this->sortTreeArray($actual);
+        $actual = $this->normalizeTreeArray($actual, $keepProperties);
+
+        $expected = [];
+        self::assertEquals($expected, $actual);
+    }
+
     /**
      * @test
      */
@@ -271,14 +313,7 @@ class TreeControllerTest extends FunctionalTestCase
                             [
                                 'uid' => 1240,
                                 'title' => 'EN: Managing data',
-                                '_children' => [
-                                    [
-                                        'uid' => 124010,
-                                        'title' => 'EN: Managing complex data',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                ],
+                                '_children' => [],
                             ],
                             [
                                 'uid' => 1210,
@@ -313,26 +348,7 @@ class TreeControllerTest extends FunctionalTestCase
                             [
                                 'uid' => 1520,
                                 'title' => 'Forecasts',
-                                '_children' => [
-                                    [
-                                        'uid' => 1521,
-                                        'title' => 'Current Year',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                    [
-                                        'uid' => 1522,
-                                        'title' => 'Next Year',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                    [
-                                        'uid' => 1523,
-                                        'title' => 'Five Years',
-                                        '_children' => [
-                                        ],
-                                    ],
-                                ],
+                                '_children' => [],
                             ],
                             [
                                 'uid' => 1530,
@@ -351,13 +367,7 @@ class TreeControllerTest extends FunctionalTestCase
                                 // from pid 1510 (missing permissions) to pid 1700 (visible now)
                                 'uid' => 1511,
                                 'title' => 'Products',
-                                '_children' => [
-                                    [
-                                        'uid' => 151110,
-                                        'title' => 'Product 1',
-                                        '_children' => [],
-                                    ]
-                                ],
+                                '_children' => [],
                             ],
                         ],
                     ],
@@ -396,6 +406,39 @@ class TreeControllerTest extends FunctionalTestCase
         self::assertEquals($expected, $actual);
     }
 
+    /**
+     * @test
+     */
+    public function getSubtreeForAccessiblePageInWorkspace()
+    {
+        $actual = $this->subject->_call('getAllEntryPointPageTrees', 1200);
+        $keepProperties = array_flip(['uid', 'title', '_children']);
+        $actual = $this->sortTreeArray($actual);
+        $actual = $this->normalizeTreeArray($actual, $keepProperties);
+
+        $expected = [
+            [
+                'uid' => 1200,
+                'title' => 'EN: Features',
+                '_children' => [
+                    [
+                        'uid' => 1210,
+                        'title' => 'EN: Frontend Editing',
+                        '_children' => [
+                        ],
+                    ],
+                    [
+                        'uid' => 1230,
+                        'title' => 'EN: Managing content',
+                        '_children' => [
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        self::assertEquals($expected, $actual);
+    }
+
     /**
      * @param int $workspaceId
      */
diff --git a/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf b/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf
index bca830fd1104..9527969fb920 100644
--- a/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf
+++ b/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf
@@ -1072,7 +1072,7 @@ Do you want to refresh it now?</source>
 				<source>Filter tree</source>
 			</trans-unit>
 			<trans-unit id="tree.searchTermInfo" resname="tree.searchTermInfo">
-				<source>Enter search term</source>
+				<source>Enter search term and hit enter</source>
 			</trans-unit>
 			<trans-unit id="warning.install_password" resname="warning.install_password">
 				<source>The Install Tool is still using the default password "joh316". Update this within the %sAbout section%s of the Install Tool.</source>
-- 
GitLab