From 4f5e778da18b642d1acc6de871c3fd96f82eef75 Mon Sep 17 00:00:00 2001
From: Benjamin Franzke <bfr@qbus.de>
Date: Mon, 15 Nov 2021 10:21:06 +0100
Subject: [PATCH] [BUGFIX] Always display root page in page tree

This applies to rendering of database mounts (from groups or
from workspaces): They are now shown as children of the virtual
root page (uid=0) as it used to be in TYPO3 v8.

This is to allow workspace users to get an overview of all
changes of all mounts by navigating to the virtual page uid=0.

Releases: master, 11.5, 10.4
Resolves: #95854
Related: #95972
Related: #91145
Change-Id: I7f6370f327711396193cf56b63f15876350c2559
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72028
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Jochen <rothjochen@gmail.com>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Jochen <rothjochen@gmail.com>
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
---
 .../Controller/Page/TreeController.php        | 104 +++---
 .../Tree/Repository/PageTreeRepository.php    |  50 ++-
 .../Controller/Page/TreeControllerTest.php    | 336 ++++++++++--------
 3 files changed, 277 insertions(+), 213 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
index f2ff74207d10..be8b6fbf69d9 100644
--- a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
+++ b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php
@@ -498,28 +498,19 @@ class TreeController
     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];
+        if ($startPid === 0) {
+            $startPid = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
+        }
+
+        $entryPointIds = null;
+        if ($startPid > 0) {
+            $entryPointIds = [$startPid];
         } elseif (!empty($this->alternativeEntryPoints)) {
             $entryPointIds = $this->alternativeEntryPoints;
-        } else {
-            //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
-                $entryPointIds = [0];
-            }
-        }
-        if (empty($entryPointIds)) {
-            return [];
         }
-        $repository = $this->getPageTreeRepository();
 
-        $permClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
+        $repository = $this->getPageTreeRepository();
+        $permClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
         if ($query !== '') {
             $this->levelsToFetch = 999;
             $repository->fetchFilteredTree(
@@ -528,55 +519,72 @@ class TreeController
                 $permClause
             );
         }
-
         $entryPointRecords = [];
-        foreach ($entryPointIds as $k => $entryPointId) {
-            if (in_array($entryPointId, $this->hiddenRecords, true)) {
-                continue;
-            }
+        if ($entryPointIds === null) {
+            $rootRecord = [
+                'uid' => 0,
+                'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3',
+            ];
 
-            if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
-                try {
-                    $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[$entryPointId])) {
-                        $this->backgroundColors[$entryPointId] = $this->backgroundColors[$parentUid];
-                    }
-                }
-            }
-            if ($entryPointId === 0) {
-                $entryPointRecord = [
-                    'uid' => 0,
-                    'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3',
-                ];
+            //watch out for deleted pages returned as webmount
+            $mountPoints = array_map('intval', $backendUser->returnWebmounts());
+            $mountPoints = array_unique($mountPoints);
+            $mountPoints = array_filter($mountPoints, fn ($id) => !in_array($id, $this->hiddenRecords, true));
+
+            if ($query !== '') {
+                $rootRecord = $repository->getTree(0, null, $mountPoints, true);
             } else {
+                $rootRecord = $repository->getTreeLevels($rootRecord, $this->levelsToFetch, $mountPoints);
+            }
+            $entryPointRecords[] = $rootRecord;
+        } else {
+            $entryPointIds = array_filter($entryPointIds, fn ($id) => !in_array($id, $this->hiddenRecords, true));
+            $this->calculateBackgroundColors($entryPointIds);
+            foreach ($entryPointIds as $k => $entryPointId) {
                 $entryPointRecord = BackendUtility::getRecordWSOL('pages', $entryPointId, '*', $permClause);
 
-                if ($entryPointRecord !== null && !$this->getBackendUser()->isInWebMount($entryPointId)) {
+                if ($entryPointRecord !== null && !$backendUser->isInWebMount($entryPointId)) {
                     $entryPointRecord = null;
                 }
-            }
-            if ($entryPointRecord) {
+                if ($entryPointRecord === null) {
+                    continue;
+                }
+
                 $entryPointRecord['uid'] = (int)$entryPointRecord['uid'];
                 if ($query === '') {
                     $entryPointRecord = $repository->getTreeLevels($entryPointRecord, $this->levelsToFetch);
                 } else {
-                    $entryPointRecord = $repository->getTree((int)$entryPointRecord['uid'], null, $entryPointIds, true);
+                    $entryPointRecord = $repository->getTree($entryPointRecord['uid'], null, $entryPointIds, true);
                 }
-            }
 
-            if (is_array($entryPointRecord) && !empty($entryPointRecord)) {
-                $entryPointRecords[$k] = $entryPointRecord;
+                if (is_array($entryPointRecord) && !empty($entryPointRecord)) {
+                    $entryPointRecords[$k] = $entryPointRecord;
+                }
             }
         }
 
         return $entryPointRecords;
     }
 
+    protected function calculateBackgroundColors(array $pageIds)
+    {
+        foreach ($pageIds as $k => $pageId) {
+            if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
+                try {
+                    $entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
+                } catch (RootLineException $e) {
+                    $entryPointRootLine = [];
+                }
+                foreach ($entryPointRootLine as $rootLineEntry) {
+                    $parentUid = $rootLineEntry['uid'];
+                    if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$pageId])) {
+                        $this->backgroundColors[$pageId] = $this->backgroundColors[$parentUid];
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Returns the first configured domain name for a page
      *
diff --git a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
index 4c832fd01cf4..2688de6f7330 100644
--- a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
+++ b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php
@@ -168,12 +168,21 @@ class PageTreeRepository
      *
      * @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
+     * @param array $entryPointIds entryPointIds to include
      * @return array An array with page records and their children
      */
-    public function getTreeLevels(array $pageTree, int $depth): array
+    public function getTreeLevels(array $pageTree, int $depth, array $entryPointIds = []): array
     {
-        $parentPageIds = [$pageTree['uid']];
         $groupedAndSortedPagesByPid = [];
+
+        if (count($entryPointIds) > 0) {
+            $pageRecords = $this->getPageRecords($entryPointIds);
+            $groupedAndSortedPagesByPid = $this->groupAndSortPages($pageRecords, $groupedAndSortedPagesByPid, 0);
+            $parentPageIds = $entryPointIds;
+        } else {
+            $parentPageIds = [$pageTree['uid']];
+        }
+
         for ($i = 0; $i < $depth; $i++) {
             if (empty($parentPageIds)) {
                 break;
@@ -188,13 +197,19 @@ class PageTreeRepository
         return $pageTree;
     }
 
+    protected function getChildPageRecords(array $parentPageIds): array
+    {
+        return $this->getPageRecords([], $parentPageIds);
+    }
+
     /**
-     * Retrieve the page records based on the given parent page ids
+     * Retrieve the page records based on the given page or parent page ids
      *
+     * @param array $pageIds
      * @param array $parentPageIds
      * @return array
      */
-    protected function getChildPageRecords(array $parentPageIds): array
+    protected function getPageRecords(array $pageIds = [], array $parentPageIds = []): array
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
             ->getQueryBuilderForTable('pages');
@@ -209,16 +224,29 @@ class PageTreeRepository
             }
         }
 
-        $pageRecords = $queryBuilder
+        $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))
+                $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
             )
             ->andWhere(
                 QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW))
-            )
+            );
+
+        if (count($pageIds) > 0) {
+            $queryBuilder->andWhere(
+                $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY))
+            );
+        }
+
+        if (count($parentPageIds) > 0) {
+            $queryBuilder->andWhere(
+                $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($parentPageIds, Connection::PARAM_INT_ARRAY))
+            );
+        }
+
+        $pageRecords = $queryBuilder
             ->execute()
             ->fetchAllAssociative();
 
@@ -685,12 +713,16 @@ class PageTreeRepository
      *
      * @param array $pages
      * @param array $groupedAndSortedPagesByPid
+     * @param int|null $forcePid
      * @return array
      */
-    protected function groupAndSortPages(array $pages, $groupedAndSortedPagesByPid = []): array
+    protected function groupAndSortPages(array $pages, $groupedAndSortedPagesByPid = [], ?int $forcePid = null): array
     {
         foreach ($pages as $key => $pageRecord) {
             $parentPageId = (int)$pageRecord['pid'];
+            if ($forcePid !== null) {
+                $parentPageId = $forcePid;
+            }
             $sorting = (int)$pageRecord['sorting'];
             while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
                 $sorting++;
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
index 22ae31a04b48..c1382561a904 100644
--- a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php
@@ -111,99 +111,105 @@ class TreeControllerTest extends FunctionalTestCase
 
         $expected = [
             [
-                'uid' => 1000,
-                'title' => 'ACME Inc',
+                'uid' => 0,
+                'title' => 'New TYPO3 site',
                 '_children' => [
                     [
-                        'uid' => 1100,
-                        'title' => 'EN: Welcome',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1200,
-                        'title' => 'EN: Features',
+                        'uid' => 1000,
+                        'title' => 'ACME Inc',
                         '_children' => [
                             [
-                                'uid' => 1210,
-                                'title' => 'EN: Frontend Editing',
+                                'uid' => 1100,
+                                'title' => 'EN: Welcome',
                                 '_children' => [
                                 ],
                             ],
                             [
-                                'uid' => 1230,
-                                'title' => 'EN: Managing content',
+                                'uid' => 1200,
+                                'title' => 'EN: Features',
                                 '_children' => [
+                                    [
+                                        'uid' => 1210,
+                                        'title' => 'EN: Frontend Editing',
+                                        '_children' => [
+                                        ],
+                                    ],
+                                    [
+                                        'uid' => 1230,
+                                        'title' => 'EN: Managing content',
+                                        '_children' => [
+                                        ],
+                                    ],
                                 ],
                             ],
-                        ],
-                    ],
-                    [
-                        'uid' => 1400,
-                        'title' => 'EN: ACME in your Region',
-                        '_children' => [
                             [
-                                'uid' => 1410,
-                                'title' => 'EN: Groups',
+                                'uid' => 1400,
+                                'title' => 'EN: ACME in your Region',
                                 '_children' => [
+                                    [
+                                        'uid' => 1410,
+                                        'title' => 'EN: Groups',
+                                        '_children' => [
+                                        ],
+                                    ],
                                 ],
                             ],
-                        ],
-                    ],
-                    [
-                        'uid' => 1500,
-                        'title' => 'Internal',
-                        '_children' => [
                             [
-                                'uid' => 1520,
-                                'title' => 'Forecasts',
-                                '_children' => [],
+                                'uid' => 1500,
+                                'title' => 'Internal',
+                                '_children' => [
+                                    [
+                                        'uid' => 1520,
+                                        'title' => 'Forecasts',
+                                        '_children' => [],
+                                    ],
+                                    [
+                                        'uid' => 1530,
+                                        'title' => 'Reports',
+                                        '_children' => [
+                                        ],
+                                    ],
+                                ],
                             ],
                             [
-                                'uid' => 1530,
-                                'title' => 'Reports',
+                                'uid' => 1700,
+                                'title' => 'Announcements & News',
+                                '_children' => [
+                                ],
+                            ],
+                            [
+                                'uid' => 404,
+                                'title' => 'Page not found',
+                                '_children' => [
+                                ],
+                            ],
+                            [
+                                'uid' => 1930,
+                                'title' => 'Our Blog',
+                                '_children' => [
+                                ],
+                            ],
+                            [
+                                'uid' => 1990,
+                                'title' => 'Storage',
                                 '_children' => [
                                 ],
                             ],
                         ],
                     ],
                     [
-                        'uid' => 1700,
-                        'title' => 'Announcements & News',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 404,
-                        'title' => 'Page not found',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1930,
-                        'title' => 'Our Blog',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1990,
-                        'title' => 'Storage',
+                        'uid' => 8110,
+                        'title' => 'Europe',
                         '_children' => [
+                            [
+                                'uid' => 811000,
+                                'title' => 'France',
+                                '_children' => [],
+                            ],
                         ],
                     ],
                 ],
             ],
-            [
-                'uid' => 8110,
-                'title' => 'Europe',
-                '_children' => [
-                    [
-                        'uid' => 811000,
-                        'title' => 'France',
-                        '_children' => [],
-                    ],
-                ],
-            ],
         ];
         self::assertEquals($expected, $actual);
     }
@@ -220,27 +226,33 @@ class TreeControllerTest extends FunctionalTestCase
 
         $expected = [
             [
-                'uid' => 1000,
-                'title' => 'ACME Inc',
-                '_children' => [
+                'uid' => 0,
+                'title' => 'New TYPO3 site',
+                '_children' =>[
                     [
-                        'uid' => 1400,
-                        'title' => 'EN: ACME in your Region',
+                        'uid' => 1000,
+                        'title' => 'ACME Inc',
                         '_children' => [
                             [
-                                'uid' => 1410,
-                                'title' => 'EN: Groups',
+                                'uid' => 1400,
+                                'title' => 'EN: ACME in your Region',
                                 '_children' => [
+                                    [
+                                        'uid' => 1410,
+                                        'title' => 'EN: Groups',
+                                        '_children' => [
+                                        ],
+                                    ],
                                 ],
                             ],
                         ],
                     ],
-                ],
-            ],
-            [
-                'uid' => 8110,
-                'title' => 'Europe',
-                '_children' => [
+                    [
+                        'uid' => 8110,
+                        'title' => 'Europe',
+                        '_children' => [
+                        ],
+                    ],
                 ],
             ],
         ];
@@ -321,107 +333,113 @@ class TreeControllerTest extends FunctionalTestCase
 
         $expected = [
             [
-                'uid' => 1000,
-                'title' => 'ACME Inc',
-                '_children' => [
+                'uid' => 0,
+                'title' => 'New TYPO3 site',
+                '_children' =>[
                     [
-                        'uid' => 1950,
-                        'title' => 'EN: Goodbye',
+                        'uid' => 1000,
+                        'title' => 'ACME Inc',
                         '_children' => [
                             [
-                                'uid' => 10015,
-                                'title' => 'EN: Really Goodbye',
+                                'uid' => 1950,
+                                'title' => 'EN: Goodbye',
                                 '_children' => [
+                                    [
+                                        'uid' => 10015,
+                                        'title' => 'EN: Really Goodbye',
+                                        '_children' => [
+                                        ],
+                                    ],
                                 ],
                             ],
-                        ],
-                    ],
-                    [
-                        'uid' => 1100,
-                        'title' => 'EN: Welcome',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1200,
-                        'title' => 'EN: Features modified',
-                        '_children' => [
                             [
-                                'uid' => 1240,
-                                'title' => 'EN: Managing data',
-                                '_children' => [],
+                                'uid' => 1100,
+                                'title' => 'EN: Welcome',
+                                '_children' => [
+                                ],
                             ],
                             [
-                                'uid' => 1230,
-                                'title' => 'EN: Managing content',
+                                'uid' => 1200,
+                                'title' => 'EN: Features modified',
                                 '_children' => [
+                                    [
+                                        'uid' => 1240,
+                                        'title' => 'EN: Managing data',
+                                        '_children' => [],
+                                    ],
+                                    [
+                                        'uid' => 1230,
+                                        'title' => 'EN: Managing content',
+                                        '_children' => [
+                                        ],
+                                    ],
                                 ],
                             ],
-                        ],
-                    ],
-                    [
-                        'uid' => 1500,
-                        'title' => 'Internal',
-                        '_children' => [
                             [
-                                'uid' => 1520,
-                                'title' => 'Forecasts',
-                                '_children' => [],
+                                'uid' => 1500,
+                                'title' => 'Internal',
+                                '_children' => [
+                                    [
+                                        'uid' => 1520,
+                                        'title' => 'Forecasts',
+                                        '_children' => [],
+                                    ],
+                                    [
+                                        'uid' => 1530,
+                                        'title' => 'Reports',
+                                        '_children' => [
+                                        ],
+                                    ],
+                                ],
                             ],
                             [
-                                'uid' => 1530,
-                                'title' => 'Reports',
+                                'uid' => 1700,
+                                'title' => 'Announcements & News',
                                 '_children' => [
+                                    [
+                                        // page moved in workspace 1
+                                        // from pid 8110 to pid 1700 (visible now)
+                                        'uid' => 811000,
+                                        'title' => 'France',
+                                        '_children' => [],
+                                    ],
+                                    [
+                                        // page with sub-pages moved in workspace 1
+                                        // from pid 1510 (missing permissions) to pid 1700 (visible now)
+                                        'uid' => 1511,
+                                        'title' => 'Products',
+                                        '_children' => [],
+                                    ],
                                 ],
                             ],
-                        ],
-                    ],
-                    [
-                        'uid' => 1700,
-                        'title' => 'Announcements & News',
-                        '_children' => [
                             [
-                                // page moved in workspace 1
-                                // from pid 8110 to pid 1700 (visible now)
-                                'uid' => 811000,
-                                'title' => 'France',
-                                '_children' => [],
+                                'uid' => 404,
+                                'title' => 'Page not found',
+                                '_children' => [
+                                ],
                             ],
                             [
-                                // page with sub-pages moved in workspace 1
-                                // from pid 1510 (missing permissions) to pid 1700 (visible now)
-                                'uid' => 1511,
-                                'title' => 'Products',
-                                '_children' => [],
+                                'uid' => 1930,
+                                'title' => 'Our Blog',
+                                '_children' => [
+                                ],
+                            ],
+                            [
+                                'uid' => 1990,
+                                'title' => 'Storage',
+                                '_children' => [
+                                ],
                             ],
                         ],
                     ],
                     [
-                        'uid' => 404,
-                        'title' => 'Page not found',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1930,
-                        'title' => 'Our Blog',
-                        '_children' => [
-                        ],
-                    ],
-                    [
-                        'uid' => 1990,
-                        'title' => 'Storage',
+                        'uid' => 8110,
+                        'title' => 'Europe',
                         '_children' => [
                         ],
                     ],
                 ],
             ],
-            [
-                'uid' => 8110,
-                'title' => 'Europe',
-                '_children' => [
-                ],
-            ],
         ];
         self::assertEquals($expected, $actual);
     }
@@ -502,14 +520,20 @@ class TreeControllerTest extends FunctionalTestCase
 
         $expected = [
             [
-                'uid' => 1000,
-                'title' => 'ACME Inc',
-                '_children' => $expectedChildren,
-            ],
-            [
-                'uid' => 8110,
-                'title' => 'Europe',
-                '_children' => [
+                'uid' => 0,
+                'title' => 'New TYPO3 site',
+                '_children' =>[
+                    [
+                        'uid' => 1000,
+                        'title' => 'ACME Inc',
+                        '_children' => $expectedChildren,
+                    ],
+                    [
+                        'uid' => 8110,
+                        'title' => 'Europe',
+                        '_children' => [
+                        ],
+                    ],
                 ],
             ],
         ];
-- 
GitLab