diff --git a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php index 509afee22025beabfb1c1eb304efe5212b2c749c..5477103a0e290ff1454b1bf1bf7472d41c34cce9 100644 --- a/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php +++ b/typo3/sysext/backend/Classes/Tree/Repository/PageTreeRepository.php @@ -720,6 +720,11 @@ class PageTreeRepository foreach ($pages as $key => $pageRecord) { $parentPageId = (int)$pageRecord['pid']; $sorting = (int)$pageRecord['sorting']; + // If the page record was already added in another depth level, don't add it another time. + // This may happen, if entry points are intersecting each other (Entry point B is inside entry point A). + if (($groupedAndSortedPagesByPid[$parentPageId][$sorting]['uid'] ?? 0) === $pageRecord['uid']) { + continue; + } 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 d1e8fe6a6001fd2be828588c9063c4a1ec8cc921..fd85bbaeb0506fbf7e2699dbf65f9ccbbb3ed73d 100644 --- a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php +++ b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Tests\Functional\Controller\Page; use TYPO3\CMS\Backend\Controller\Page\TreeController; +use TYPO3\CMS\Backend\Tests\Functional\Tree\Repository\Fixtures\Tree\NormalizeTreeTrait; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\WorkspaceAspect; @@ -35,6 +36,7 @@ use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; class TreeControllerTest extends FunctionalTestCase { use SiteBasedTestTrait; + use NormalizeTreeTrait; protected const LANGUAGE_PRESETS = []; @@ -729,54 +731,4 @@ class TreeControllerTest extends FunctionalTestCase $this->backendUser->workspace = $workspaceId; $this->context->setAspect('workspace', new WorkspaceAspect($workspaceId)); } - - /** - * Sorts tree array by index of each section item recursively. - * - * @param array $tree - * @return array - */ - private function sortTreeArray(array $tree): array - { - ksort($tree); - return array_map( - function (array $item) { - foreach ($item as $propertyName => $propertyValue) { - if (!is_array($propertyValue)) { - continue; - } - $item[$propertyName] = $this->sortTreeArray($propertyValue); - } - return $item; - }, - $tree - ); - } - - /** - * Normalizes a tree array, re-indexes numeric indexes, only keep given properties. - * - * @param array $tree Whole tree array - * @param array $keepProperties (property names to be used as indexes for array_intersect_key()) - * @return array Normalized tree array - */ - private function normalizeTreeArray(array $tree, array $keepProperties): array - { - return array_map( - function (array $item) use ($keepProperties) { - // only keep these property names - $item = array_intersect_key($item, $keepProperties); - foreach ($item as $propertyName => $propertyValue) { - if (!is_array($propertyValue)) { - continue; - } - // process recursively for nested array items (e.g. `_children`) - $item[$propertyName] = $this->normalizeTreeArray($propertyValue, $keepProperties); - } - return $item; - }, - // normalize numeric indexes (remove sorting markers) - array_values($tree) - ); - } } diff --git a/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/PageTree.csv b/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/PageTree.csv new file mode 100644 index 0000000000000000000000000000000000000000..5668f970aa9b19a0f24e4f1c10aebdf17112454f --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/PageTree.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","sorting","title" +,1,0,256,"Home" +,2,1,32,"Main Area" +,20,2,64,"Main Area Sub 1" +,21,2,128,"Main Area Sub 2" +,30,21,64,"Sub Area 1" +,31,21,128,"Sub Area 2" +,3,0,512,"Home 2" diff --git a/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/Tree/NormalizeTreeTrait.php b/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/Tree/NormalizeTreeTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..965d96d2b96666eee6434e1a77243ddc1ab1a436 --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/Tree/NormalizeTreeTrait.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Tree\Repository\Fixtures\Tree; + +trait NormalizeTreeTrait +{ + /** + * Sorts tree array by index of each section item recursively. + */ + private function sortTreeArray(array $tree): array + { + ksort($tree); + return array_map( + function (array $item) { + foreach ($item as $propertyName => $propertyValue) { + if (!is_array($propertyValue)) { + continue; + } + $item[$propertyName] = $this->sortTreeArray($propertyValue); + } + return $item; + }, + $tree + ); + } + + /** + * Normalizes a tree array, re-indexes numeric indexes, only keep given properties. + * + * @param array $tree Whole tree array + * @param array $keepProperties (property names to be used as indexes for array_intersect_key()) + * @return array Normalized tree array + */ + private function normalizeTreeArray(array $tree, array $keepProperties): array + { + return array_map( + function (array $item) use ($keepProperties) { + // only keep these property names + $item = array_intersect_key($item, $keepProperties); + foreach ($item as $propertyName => $propertyValue) { + if (!is_array($propertyValue)) { + continue; + } + // process recursively for nested array items (e.g. `_children`) + $item[$propertyName] = $this->normalizeTreeArray($propertyValue, $keepProperties); + } + return $item; + }, + // normalize numeric indexes (remove sorting markers) + array_values($tree) + ); + } +} diff --git a/typo3/sysext/backend/Tests/Functional/Tree/Repository/PageTreeRepositoryTest.php b/typo3/sysext/backend/Tests/Functional/Tree/Repository/PageTreeRepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..39b53c1a690ffc936ac5b948e685808666d5f5ce --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Tree/Repository/PageTreeRepositoryTest.php @@ -0,0 +1,239 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Tree\Repository; + +use TYPO3\CMS\Backend\Tests\Functional\Tree\Repository\Fixtures\Tree\NormalizeTreeTrait; +use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class PageTreeRepositoryTest extends FunctionalTestCase +{ + use NormalizeTreeTrait; + + public function setUp(): void + { + parent::setUp(); + $this->importCSVDataSet('typo3/sysext/backend/Tests/Functional/Tree/Repository/Fixtures/PageTree.csv'); + $this->setUpBackendUserFromFixture(1); + } + + public function getTreeLevelsReturnsGroupedAndSortedPageTreeArrayDataProvider(): iterable + { + yield 'Single entry point with depth 2' => [ + 'pageTree' => [ + 'uid' => 0, + 'title' => 'Core', + ], + 'depth' => 2, + 'entryPointIds' => [ + 2, + ], + 'expected' => [ + 'uid' => 0, + 'title' => 'Core', + '_children' => [ + [ + 'uid' => 2, + 'title' => 'Main Area', + '_children' => [ + [ + 'uid' => 20, + 'title' => 'Main Area Sub 1', + '_children' => [], + ], + [ + 'uid' => 21, + 'title' => 'Main Area Sub 2', + '_children' => [ + [ + 'uid' => 30, + 'title' => 'Sub Area 1', + '_children' => [], + ], + [ + 'uid' => 31, + 'title' => 'Sub Area 2', + '_children' => [], + ], + ], + ], + ], + ], + ], + ], + ]; + + yield 'Single entry point with depth 1' => [ + 'pageTree' => [ + 'uid' => 0, + 'title' => 'Core', + ], + 'depth' => 1, + 'entryPointIds' => [ + 2, + ], + 'expected' => [ + 'uid' => 0, + 'title' => 'Core', + '_children' => [ + [ + 'uid' => 2, + 'title' => 'Main Area', + '_children' => [ + [ + 'uid' => 20, + 'title' => 'Main Area Sub 1', + '_children' => [], + ], + [ + 'uid' => 21, + 'title' => 'Main Area Sub 2', + '_children' => [], + ], + ], + ], + ], + ], + ]; + + yield 'Two entry points parallel to each other' => [ + 'pageTree' => [ + 'uid' => 0, + 'title' => 'Core', + ], + 'depth' => 2, + 'entryPointIds' => [ + 2, + 3, + ], + 'expected' => [ + 'uid' => 0, + 'title' => 'Core', + '_children' => [ + [ + 'uid' => 2, + 'title' => 'Main Area', + '_children' => [ + [ + 'uid' => 20, + 'title' => 'Main Area Sub 1', + '_children' => [], + ], + [ + 'uid' => 21, + 'title' => 'Main Area Sub 2', + '_children' => [ + [ + 'uid' => 30, + 'title' => 'Sub Area 1', + '_children' => [], + ], + [ + 'uid' => 31, + 'title' => 'Sub Area 2', + '_children' => [], + ], + ], + ], + ], + ], + [ + 'uid' => 3, + 'title' => 'Home 2', + '_children' => [], + ], + ], + ], + ]; + + yield 'Two entry points intersecting each other' => [ + 'pageTree' => [ + 'uid' => 0, + 'title' => 'Core', + ], + 'depth' => 2, + 'entryPointIds' => [ + 2, + 21, + ], + 'expected' => [ + 'uid' => 0, + 'title' => 'Core', + '_children' => [ + [ + 'uid' => 2, + 'title' => 'Main Area', + '_children' => [ + [ + 'uid' => 20, + 'title' => 'Main Area Sub 1', + '_children' => [], + ], + [ + 'uid' => 21, + 'title' => 'Main Area Sub 2', + '_children' => [ + [ + 'uid' => 30, + 'title' => 'Sub Area 1', + '_children' => [], + ], + [ + 'uid' => 31, + 'title' => 'Sub Area 2', + '_children' => [], + ], + ], + ], + ], + ], + [ + 'uid' => 21, + 'title' => 'Main Area Sub 2', + '_children' => [ + [ + 'uid' => 30, + 'title' => 'Sub Area 1', + '_children' => [], + ], + [ + 'uid' => 31, + 'title' => 'Sub Area 2', + '_children' => [], + ], + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider getTreeLevelsReturnsGroupedAndSortedPageTreeArrayDataProvider + * @test + */ + public function getTreeLevelsReturnsGroupedAndSortedPageTreeArray(array $pageTree, int $depth, array $entryPointIds, array $expected): void + { + $pageTreeRepository = new PageTreeRepository(); + $actual = $pageTreeRepository->getTreeLevels($pageTree, $depth, $entryPointIds); + $actual = $this->sortTreeArray([$actual]); + $keepProperties = array_flip(['uid', 'title', '_children']); + $actual = $this->normalizeTreeArray($actual, $keepProperties); + self::assertEquals($expected, $actual[0]); + } +}