From d1370d2c0b77d381200c85568bb837e57b3f7a48 Mon Sep 17 00:00:00 2001 From: Tymoteusz Motylewski <t.motylewski@gmail.com> Date: Mon, 30 Mar 2020 12:55:00 +0200 Subject: [PATCH] [TASK] Provide test for Page\TreeController Add test for TreeController, so we're safe when refactoring or doing performance optimizations. Besides that moved pages in a workspace were not considered when calculating permissions on the rootline due to missing workspace overlays. Resolves: #90831 Releases: 9.5, master Change-Id: Ic3ab08d2502e8c9a3f08e737552c2e1d2a56a66c Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63848 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> --- composer.json | 2 +- composer.lock | 18 +- .../Page/Fixtures/PagesWithBEPermissions.yaml | 137 ++++++ .../Controller/Page/TreeControllerTest.php | 434 ++++++++++++++++++ .../BackendUserAuthentication.php | 2 +- typo3/sysext/core/composer.json | 2 +- 6 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 typo3/sysext/backend/Tests/Functional/Controller/Page/Fixtures/PagesWithBEPermissions.yaml create mode 100644 typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php diff --git a/composer.json b/composer.json index 55d2b407d457..aa25556cecef 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ "phpstan/phpstan": "^0.12.13", "rector/rector": "~0.7", "typo3/cms-styleguide": "~10.0.2", - "typo3/testing-framework": "^6.2.0" + "typo3/testing-framework": "^6.2.2" }, "suggest": { "ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images", diff --git a/composer.lock b/composer.lock index e84c6448a189..b8d3bc50c3e2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e6e138201c43bbc6250fbd836633a21d", + "content-hash": "cd0e12abcab99e0526c2f9707df4f2d1", "packages": [ { "name": "cogpowered/finediff", @@ -8115,8 +8115,8 @@ "authors": [ { "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "role": "Developer", + "email": "arne@blankerts.de" } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", @@ -8259,16 +8259,16 @@ }, { "name": "typo3/testing-framework", - "version": "6.2.0", + "version": "6.2.2", "source": { "type": "git", "url": "https://github.com/TYPO3/testing-framework.git", - "reference": "d308d4263b3268873c8b4028d8b526f2862aca6c" + "reference": "4e14e564f72875ce4c4414dd390c4b5ddb071fe6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/d308d4263b3268873c8b4028d8b526f2862aca6c", - "reference": "d308d4263b3268873c8b4028d8b526f2862aca6c", + "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/4e14e564f72875ce4c4414dd390c4b5ddb071fe6", + "reference": "4e14e564f72875ce4c4414dd390c4b5ddb071fe6", "shasum": "" }, "require": { @@ -8280,7 +8280,7 @@ "typo3/cms-fluid": "10.*.*@dev", "typo3/cms-frontend": "10.*.*@dev", "typo3/cms-recordlist": "10.*.*@dev", - "typo3fluid/fluid": "^2.5" + "typo3fluid/fluid": "^2.5|^3" }, "suggest": { "codeception/codeception": "^4.0", @@ -8315,7 +8315,7 @@ "tests", "typo3" ], - "time": "2020-03-03T13:18:50+00:00" + "time": "2020-04-06T15:19:47+00:00" } ], "aliases": [], diff --git a/typo3/sysext/backend/Tests/Functional/Controller/Page/Fixtures/PagesWithBEPermissions.yaml b/typo3/sysext/backend/Tests/Functional/Controller/Page/Fixtures/PagesWithBEPermissions.yaml new file mode 100644 index 000000000000..a5eb0a3c589e --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Controller/Page/Fixtures/PagesWithBEPermissions.yaml @@ -0,0 +1,137 @@ +__variables: + - &pageStandard 0 + - &pageShortcut 4 + - &pageMount 7 + - &pageFolder 254 + - &contentText 'text' + - &idAcmeRootPage 1000 + - &idAcmeFirstPage 1100 + +entitySettings: + '*': + nodeColumnName: 'pid' + columnNames: {id: 'uid', language: 'sys_language_uid'} + defaultValues: {pid: 0} + page: + isNode: true + tableName: 'pages' + parentColumnName: 'pid' + languageColumnNames: ['l10n_parent', 'l10n_source'] + columnNames: {type: 'doktype', root: 'is_siteroot', mount: 'mount_pid', visitorGroups: 'fe_group'} + defaultValues: {hidden: 0, doktype: *pageStandard, perms_userid: 1, perms_groupid: 9} + valueInstructions: + shortcut: + first: {shortcut: 0, shortcut_mode: 1} + content: + tableName: 'tt_content' + languageColumnNames: ['l18n_parent', 'l10n_source'] + columnNames: {title: 'header', type: 'CType'} + workspace: + tableName: 'sys_workspace' + language: + tableName: 'sys_language' + columnNames: {code: 'language_isocode'} + visitorGroup: + tableName: 'fe_groups' + visitor: + tableName: 'fe_users' + columnNames: {groups: 'usergroup'} + typoscript: + tableName: 'sys_template' + valueInstructions: + type: + site: {root: 1, clear: 1} + beGroup: + tableName: 'be_groups' + +entities: + workspace: + - self: {id: 1, title: 'Workspace'} + language: + - self: {id: 1, title: 'French', code: 'fr'} + - self: {id: 2, title: 'Franco-Canadian', code: 'fr'} + - self: {id: 3, title: 'Spanish', code: 'es'} + beGroup: + - self: {id: 9, title: 'editors', db_mountpoints: '1000,2000', tables_select: 'pages,tt_content', tables_modify: 'pages,tt_content', page_types_select: '1,4,7,254', groupMods: 'web_layout,web_list'} + page: + - self: {id: *idAcmeRootPage, title: 'ACME Inc', type: *pageShortcut, shortcut: 'first', root: true, slug: '/'} + children: + - self: {id: *idAcmeFirstPage, title: 'EN: Welcome', slug: '/welcome', subtitle: 'hello-and-welcome'} + - self: {id: 1200, title: 'EN: Features', slug: '/features'} + children: + - self: {id: 1210, title: 'EN: Frontend Editing', slug: '/features/frontend-editing', perms_userid: 9, perms_groupid: 1, description: "accessible for user, but not for group"} + - self: {id: 1220, title: 'EN: Managing backend', slug: '/features/managing-backend', perms_userid: 1, perms_groupid: 1, description: "not accessible"} + - self: {id: 1230, title: 'EN: Managing content', slug: '/features/managing-content', perms_userid: 9, perms_groupid: 9, description: "accessible for user and group"} + - version: {id: 1240, title: 'EN: Managing data', slug: '/features/managing-data', workspace: 1} + children: + - version: {id: 124010, title: 'EN: Managing complex data', slug: '/features/managing-data/complex', workspace: 1} + - self: {id: 1400, title: 'EN: ACME in your Region', root: true, slug: '/acme-in-your-region'} + languageVariants: + - self: {id: 1401, title: 'FR: ACME in your Region', language: 1, slug: '/acme-dans-votre-region'} + - self: {id: 1402, title: 'FR-CA: ACME in your Region', language: 2, slug: '/acme-dans-votre-quebec'} + children: + - self: {id: 1410, title: 'EN: Groups', slug: '/acme-in-your-region/groups', l18n_cfg: 1} + languageVariants: + - self: {id: 1411, title: 'FR: Groups', language: 1, slug: '/acme-dans-votre-region/groupes'} + - self: {id: 1412, title: 'FR-CA: Groups', language: 2, slug: '/acme-dans-votre-quebec/groupes'} + - self: {id: 1500, title: 'Internal', slug: '/my-acme'} + children: + - self: {id: 1510, title: 'Whitepapers', visitorGroups: -2, extendToSubpages: true, slug: '/my-acme/whitepapers', perms_userid: 1, perms_groupid: 1, description: "not accessible"} + children: + - self: {id: 1511, title: 'Products', slug: '/my-acme/whitepapers/products'} + children: + - self: {id: 151110, title: 'Product 1', slug: '/my-acme/whitepapers/products/product-1'} + versionVariants: + - version: { workspace: 1 } + actions: + - { action: 'move', type: 'toPage', target: 1700 } + - self: {id: 1512, title: 'Solutions', visitorGroups: 10, slug: '/my-acme/whitepapers/solutions'} + children: + - self: {id: 151210, title: 'Solution 1', slug: '/my-acme/whitepapers/solutions/solution-1'} + - self: {id: 1515, title: 'Research', visitorGroups: 20, slug: '/my-acme/whitepapers/research'} + - self: {id: 1520, title: 'Forecasts', visitorGroups: 20, extendToSubpages: true, slug: '/my-acme/forecasts'} + children: + - self: {id: 1521, title: 'Current Year', slug: '/my-acme/forecasts/current-year'} + - self: {id: 1522, title: 'Next Year', slug: '/my-acme/forecasts/next-year'} + - self: {id: 1523, title: 'Five Years', slug: '/my-acme/forecasts/five-years'} + - self: {id: 1530, title: 'Reports', visitorGroups: 20, extendToSubpages: true, slug: '/my-acme/reports'} + languageVariants: + - version: {title: 'FR: Interne', workspace: 1, language: 1, slug: '/my-acme'} + - self: {id: 1600, title: 'About us', slug: '/about', perms_userid: 1, perms_groupid: 1, description: "not accessible"} + - self: {id: 1700, title: 'Announcements & News', type: *pageMount, mount: 7100, slug: '/news'} + - self: {id: 404, title: 'Page not found', slug: '/404'} + entities: + content: + - self: {title: 'EN: Page not found', type: *contentText} + - self: {id: 1930, title: 'Our Blog', type: *pageShortcut, shortcut: 2000, slug: '/blog'} + - version: {id: 1950, title: 'EN: Goodbye', workspace: 1, slug: '/bye'} + children: + - version: {title: 'EN: Really Goodbye', workspace: 1, slug: '/bye/bye'} + - self: {id: 1990, title: 'Storage', type: *pageFolder, slug: '/internal/storage'} + entities: + visitorGroup: + - self: {id: 10, title: 'Customers'} + - self: {id: 20, title: 'Partners'} + visitor: + - self: {id: 1, username: 'john@doe.local', groups: '10'} + - self: {id: 2, username: 'manager@other-inc.local', groups: '20'} + - self: {id: 3, username: 'big-boss@acme-inc.local', groups: '10,20'} + - self: {id: 2000, title: 'ACME Blog', type: *pageShortcut, shortcut: 'first', root: true, slug: '/', perms_userid: 1, perms_groupid: 1, description: "not accessible"} + children: + - self: {id: 2100, title: 'Authors', slug: '/authors'} + children: + - self: {id: 2110, title: 'John Doe', slug: '/john'} + children: + - self: {id: 2111, title: 'About', slug: '/about-john'} + - self: {id: 2120, title: 'Jane Doe', slug: '/jane'} + children: + - self: {id: 2121, title: 'About', slug: '/about-jane'} + - self: {id: 2700, title: 'Announcements & News', type: *pageMount, mount: 7100, slug: '/news'} + - self: {id: 2930, title: 'ACME Inc', type: *pageShortcut, shortcut: 1000, slug: '/acme'} + - self: {id: 7000, title: 'Common Collection', type: *pageFolder, root: true, slug: '/common'} + children: + - self: {id: 7100, title: 'Announcements & News', slug: '/common/news'} + children: + - self: {id: 7110, title: 'Markets', slug: '/common/markets'} + - self: {id: 7120, title: 'Products', slug: '/common/products'} + - self: {id: 7130, title: 'Partners', slug: '/common/partners'} diff --git a/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php new file mode 100644 index 000000000000..51438c0d89e1 --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Controller/Page/TreeControllerTest.php @@ -0,0 +1,434 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\Tests\Functional\Controller\Page; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Controller\Page\TreeController; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\UserAspect; +use TYPO3\CMS\Core\Context\WorkspaceAspect; +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\AccessibleObjectInterface; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * Test case for TYPO3\CMS\Backend\Controller\Page\TreeController + */ +class TreeControllerTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + /** + * @var string[] + */ + protected $coreExtensionsToLoad = ['workspaces']; + + /** + * @var TreeController|AccessibleObjectInterface + */ + private $subject; + + /** + * @var BackendUserAuthentication + */ + private $backendUser; + + /** + * @var BackendUserAuthentication + */ + private $regularBackendUser; + + /** + * @var Context + */ + private $context; + + /** + * The fixture which is used when initializing a backend user + * + * @var string + */ + protected $backendUserFixture = 'EXT:core/Tests/Functional/Fixtures/be_users.xml'; + + protected function setUp(): void + { + parent::setUp(); + //admin user for importing dataset + $this->backendUser = $this->setUpBackendUserFromFixture(1); + $this->setUpDatabase(); + + //regular editor, non admin + $this->backendUser = $this->setUpBackendUser(9); + $this->context = GeneralUtility::makeInstance(Context::class); + $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $this->backendUser)); + + $this->subject = $this->getAccessibleMock(TreeController::class, ['dummy']); + } + + protected function tearDown(): void + { + unset($this->subject, $this->backendUser, $this->context); + parent::tearDown(); + } + + protected function setUpDatabase() + { + Bootstrap::initializeLanguageObject(); + $scenarioFile = __DIR__ . '/Fixtures/PagesWithBEPermissions.yaml'; + $factory = DataHandlerFactory::fromYamlFile($scenarioFile); + $writer = DataHandlerWriter::withBackendUser($this->backendUser); + $writer->invokeFactory($factory); + static::failIfArrayIsNotEmpty( + $writer->getErrors() + ); + } + + /** + * @test + */ + public function getAllEntryPointPageTrees() + { + $actual = $this->subject->_call('getAllEntryPointPageTrees'); + $keepProperties = array_flip(['uid', 'title', '_children']); + $actual = $this->sortTreeArray($actual); + $actual = $this->normalizeTreeArray($actual, $keepProperties); + + $expected = [ + [ + 'uid' => 1000, + 'title' => 'ACME Inc', + '_children' => [ + [ + 'uid' => 1100, + 'title' => 'EN: Welcome', + '_children' => [ + ], + ], + [ + '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', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1500, + 'title' => 'Internal', + '_children' => [ + [ + 'uid' => 1520, + 'title' => 'Forecasts', + '_children' => [ + [ + 'uid' => 1521, + 'title' => 'Current Year', + '_children' => [ + ], + ], + [ + 'uid' => 1522, + 'title' => 'Next Year', + '_children' => [ + ], + ], + [ + 'uid' => 1523, + 'title' => 'Five Years', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1530, + 'title' => 'Reports', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1700, + 'title' => 'Announcements & News', + '_children' => [ + ], + ], + [ + 'uid' => 404, + 'title' => 'Page not found', + '_children' => [ + ], + ], + [ + 'uid' => 1930, + 'title' => 'Our Blog', + '_children' => [ + ], + ], + [ + 'uid' => 1990, + 'title' => 'Storage', + '_children' => [ + ], + ], + ], + ], + ]; + self::assertEquals($expected, $actual); + } + + /** + * @test + */ + public function getAllEntryPointPageTreesInWorkspace() + { + $this->setWorkspace(1); + $actual = $this->subject->_call('getAllEntryPointPageTrees'); + $keepProperties = array_flip(['uid', 'title', '_children']); + $actual = $this->sortTreeArray($actual); + $actual = $this->normalizeTreeArray($actual, $keepProperties); + + $expected = [ + [ + 'uid' => 1000, + 'title' => 'ACME Inc', + '_children' => [ + [ + 'uid' => 1950, + 'title' => 'EN: Goodbye', + '_children' => [ + [ + 'uid' => 10007, + 'title' => 'EN: Really Goodbye', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1100, + 'title' => 'EN: Welcome', + '_children' => [ + ], + ], + [ + 'uid' => 1200, + 'title' => 'EN: Features', + '_children' => [ + [ + 'uid' => 1240, + 'title' => 'EN: Managing data', + '_children' => [ + [ + 'uid' => 124010, + 'title' => 'EN: Managing complex data', + '_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', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1500, + 'title' => 'Internal', + '_children' => [ + [ + 'uid' => 1520, + 'title' => 'Forecasts', + '_children' => [ + [ + 'uid' => 1521, + 'title' => 'Current Year', + '_children' => [ + ], + ], + [ + 'uid' => 1522, + 'title' => 'Next Year', + '_children' => [ + ], + ], + [ + 'uid' => 1523, + 'title' => 'Five Years', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1530, + 'title' => 'Reports', + '_children' => [ + ], + ], + ], + ], + [ + 'uid' => 1700, + 'title' => 'Announcements & News', + '_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' => 151110, + 'title' => 'Product 1', + '_children' => [], + ] + ], + ], + ], + ], + [ + 'uid' => 404, + 'title' => 'Page not found', + '_children' => [ + ], + ], + [ + 'uid' => 1930, + 'title' => 'Our Blog', + '_children' => [ + ], + ], + [ + 'uid' => 1990, + 'title' => 'Storage', + '_children' => [ + ], + ], + ], + ], + ]; + self::assertEquals($expected, $actual); + } + + /** + * @param int $workspaceId + */ + private function setWorkspace(int $workspaceId) + { + $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/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php index 00508fc55d3a..ec15308b8abe 100644 --- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php @@ -413,7 +413,7 @@ class BackendUserAuthentication extends AbstractUserAuthentication } if ($id > 0) { $wM = $this->returnWebmounts(); - $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms); + $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms, true); foreach ($rL as $v) { if ($v['uid'] && in_array($v['uid'], $wM)) { return $v['uid']; diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json index 937264cc0b68..69c764090504 100644 --- a/typo3/sysext/core/composer.json +++ b/typo3/sysext/core/composer.json @@ -69,7 +69,7 @@ "phpspec/prophecy": "^1.7.5", "phpstan/phpstan": "^0.12.13", "typo3/cms-styleguide": "~10.0.2", - "typo3/testing-framework": "^6.2.0" + "typo3/testing-framework": "^6.2.2" }, "suggest": { "ext-fileinfo": "Used for proper file type detection in the file abstraction layer", -- GitLab