diff --git a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php index 464181a9d67a920e3ad2045090df28ab8c1482ad..b035ecaefd80e800406a3c3b68826a1056b94cd9 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 d4d6ed45dc6c93c76a9a9593a6c43de7779b95c2..ea5f25f12b996b459b0644f51a67d9be5039001d 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 a3955dc73411db08f09c8ba86f7a4d0af04602a3..37053a5033dbca5952fb53217707b0931da589bc 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 bd7c8da35a6bbb33b40fe86cc072a5c19c6dcad9..9ce08e22355e343136c286e6a09c5bf684ee2c6e 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 56b0f3160e3b4dea0c7074367bfac2db20eac0c5..3ca16ece4bf0b8e29ea7c04a9be46531c8b6dfd5 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 83cb935dc497e3f09a6f2af942e8fda6836d3647..5a6185c22165fc9074d8a09791b79154ebda1cfa 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 e066862f370ddb7ae470544b193a58f57fe1b666..f7e32822bf8c46535dccc96ea39ce4cba5e3c760 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 bca830fd11044fea04225781194e5ed1d45325e9..9527969fb920ff49c1ee111fe6061bd6f7681c46 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>