From a79a780e79a343905c524521e1a6aa66fd8fc8d6 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Mon, 31 Oct 2022 17:57:19 +0100 Subject: [PATCH] [BUGFIX] Resolve modified slugs in versioned pages of preview This change now also fetches versioned records when previewing a workspace, allowing to also query the versioned records if their slug has been changed. This extends WorkspaceRestriction to have a similar behaviour as previously known from PageRepository (pre v8 times), to fetch versioned records. Resolves: #97940 Releases: main, 11.5 Change-Id: I7fe9c081d5a087bf5276510e1a915fc92b72956d Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76355 Tested-by: core-ci <typo3@b13.com> Tested-by: Nikita Hovratov <nikita.h@live.de> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Nikita Hovratov <nikita.h@live.de> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Reviewed-by: Benni Mack <benni@typo3.org> --- .../Restriction/WorkspaceRestriction.php | 54 +++++++++------ .../Routing/PageSlugCandidateProvider.php | 10 +-- .../Restriction/WorkspaceRestrictionTest.php | 25 +++++++ .../SiteHandling/SlugSiteRequestTest.php | 66 +++++++++++++++++++ 4 files changed, 132 insertions(+), 23 deletions(-) diff --git a/typo3/sysext/core/Classes/Database/Query/Restriction/WorkspaceRestriction.php b/typo3/sysext/core/Classes/Database/Query/Restriction/WorkspaceRestriction.php index 74a04c78f3e2..d2d01c898889 100644 --- a/typo3/sysext/core/Classes/Database/Query/Restriction/WorkspaceRestriction.php +++ b/typo3/sysext/core/Classes/Database/Query/Restriction/WorkspaceRestriction.php @@ -29,7 +29,7 @@ use TYPO3\CMS\Core\Versioning\VersionState; * * As workspaces cannot be fully overlaid within ONE query, this query does the following: * - In live context, only fetch published records - * - In a workspace, fetch all LIVE records and all workspace records which do not have "-1" (= all new placeholders get fetched as well) + * - In a workspace, fetch all LIVE records and all workspace records which do not have "1" (= all new placeholders get fetched as well) * * This means, that all records which are fetched need to run through either * - BackendUtility::getRecordWSOL() (when having one or a few records) @@ -38,17 +38,21 @@ use TYPO3\CMS\Core\Versioning\VersionState; */ class WorkspaceRestriction implements QueryRestrictionInterface { - /** - * @var int - */ - protected $workspaceId; + protected int $workspaceId; /** - * @param int $workspaceId + * Used to also query records within a workspace, which is useful for DB queries + * that check for a specific field (e.g. "slug") which might have changed within a workspace. + * Please note that some duplicates might be shown and the "reduce" logic needs to be + * added after querying. Setting this flag might also be a problem when using the DB query + * with limit and offset settings. */ - public function __construct(int $workspaceId = 0) + protected bool $includeAllVersionedRecords; + + public function __construct(int $workspaceId = 0, bool $includeAllVersionedRecords = false) { $this->workspaceId = $workspaceId; + $this->includeAllVersionedRecords = $includeAllVersionedRecords; } /** @@ -77,19 +81,31 @@ class WorkspaceRestriction implements QueryRestrictionInterface } // Always filter out versioned records that have an "offline" record // But include moved records AND newly created records (t3ver_oid=0) - $constraints[] = $expressionBuilder->and( - $workspaceIdExpression, - $expressionBuilder->or( - $expressionBuilder->eq( - $tableAlias . '.t3ver_oid', - 0 - ), - $expressionBuilder->eq( - $tableAlias . '.t3ver_state', - VersionState::MOVE_POINTER + if ($this->includeAllVersionedRecords === false) { + $constraints[] = $expressionBuilder->and( + $workspaceIdExpression, + $expressionBuilder->or( + $expressionBuilder->eq( + $tableAlias . '.t3ver_oid', + 0 + ), + $expressionBuilder->eq( + $tableAlias . '.t3ver_state', + VersionState::MOVE_POINTER + ) + ) + ); + } else { + // Include live records plus records from the given workspace + // but never include versioned records marked as deleted + $constraints[] = $expressionBuilder->and( + $workspaceIdExpression, + $expressionBuilder->neq( + 't3ver_state', + VersionState::DELETE_PLACEHOLDER ) - ) - ); + ); + } } return $expressionBuilder->and(...$constraints); } diff --git a/typo3/sysext/core/Classes/Routing/PageSlugCandidateProvider.php b/typo3/sysext/core/Classes/Routing/PageSlugCandidateProvider.php index 29a1ca61847d..727aaf59a9f6 100644 --- a/typo3/sysext/core/Classes/Routing/PageSlugCandidateProvider.php +++ b/typo3/sysext/core/Classes/Routing/PageSlugCandidateProvider.php @@ -22,7 +22,6 @@ use TYPO3\CMS\Core\Context\LanguageAspect; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; -use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Exception\SiteNotFoundException; @@ -115,13 +114,14 @@ class PageSlugCandidateProvider */ public function getRealPageIdForPageIdAsPossibleCandidate(int $pageId): ?int { + $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id'); $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('pages'); $queryBuilder ->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class)); + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId)); $statement = $queryBuilder ->select('uid', 'l10n_parent') @@ -201,7 +201,7 @@ class PageSlugCandidateProvider ->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId)); + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId, true)); $statement = $queryBuilder ->select('uid', 'sys_language_uid', 'l10n_parent', 'l18n_cfg', 'pid', 'slug', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'doktype', 't3ver_wsid', 't3ver_oid') @@ -221,6 +221,8 @@ class PageSlugCandidateProvider ) // Exact match will be first, that's important ->orderBy('slug', 'desc') + // versioned records should be rendered before the live records + ->addOrderBy('t3ver_wsid', 'desc') // Sort pages that are not MountPoint pages before mount points ->addOrderBy('mount_pid_ol', 'asc') ->addOrderBy('mount_pid', 'asc') @@ -233,7 +235,7 @@ class PageSlugCandidateProvider while ($row = $statement->fetchAssociative()) { $mountPageInformation = null; - $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']); + $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : ($row['t3ver_oid'] ?: $row['uid'])); // When this page was added before via recursion, this page should be skipped if (in_array($pageIdInDefaultLanguage, $excludeUids, true)) { continue; diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php index 54ca51f7edb7..2b4c27b09867 100644 --- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php +++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php @@ -72,4 +72,29 @@ class WorkspaceRestrictionTest extends AbstractRestrictionTestCase $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder); self::assertSame('', (string)$expression); } + + /** + * @test + */ + public function buildExpressionQueriesAllVersionedRecordsWithinAWorkspaceAsWell(): void + { + $GLOBALS['TCA']['aTable']['ctrl'] = [ + 'versioningWS' => true, + ]; + $subject = new WorkspaceRestriction(42, true); + $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder); + self::assertSame('("aTable"."t3ver_wsid" IN (0, 42)) AND ("t3ver_state" <> 2)', (string)$expression); + } + /** + * @test + */ + public function buildExpressionQueriesAllVersionedRecordsWithinLiveStillRemovesDeletedState(): void + { + $GLOBALS['TCA']['aTable']['ctrl'] = [ + 'versioningWS' => true, + ]; + $subject = new WorkspaceRestriction(0, true); + $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder); + self::assertSame('("aTable"."t3ver_wsid" = 0) AND ("t3ver_state" <> 2)', (string)$expression); + } } diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php index b08432bf5ae8..4147284ed836 100644 --- a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php +++ b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php @@ -1187,4 +1187,70 @@ class SlugSiteRequestTest extends AbstractTestCase self::assertSame($expectedStatusCode, $response->getStatusCode()); self::assertSame($expectedHeaders, $response->getHeaders()); } + + public function pageIsRenderedForVersionedPageDataProvider(): \Generator + { + yield 'Live page with logged-in user' => [ + 'url' => 'https://website.local/en-en/welcome', + 'pageTitle' => 'EN: Welcome', + 'Online Page ID' => 1100, + 'Workspace ID' => 0, + 'Backend User ID' => 1, + 'statusCode' => 200, + ]; + yield 'Live page with logged-in user accessed even though versioned page slug was changed' => [ + 'url' => 'https://website.local/en-en/welcome', + 'pageTitle' => 'EN: Welcome to ACME Inc', + 'Online Page ID' => 1100, + 'Workspace ID' => 1, + 'Backend User ID' => 1, + 'statusCode' => 200, + ]; + yield 'Versioned page with logged-in user and modified slug' => [ + 'url' => 'https://website.local/en-en/welcome-modified', + 'pageTitle' => 'EN: Welcome to ACME Inc', + 'Online Page ID' => 1100, + 'Workspace ID' => 1, + 'Backend User ID' => 1, + 'statusCode' => 200, + ]; + yield 'Versioned page without logged-in user renders 404' => [ + 'url' => 'https://website.local/en-en/welcome-modified', + 'pageTitle' => null, + 'Online Page ID' => null, + 'Workspace ID' => 1, + 'Backend User ID' => 0, + 'statusCode' => 404, + ]; + } + + /** + * @test + * @dataProvider pageIsRenderedForVersionedPageDataProvider + */ + public function pageIsRenderedForVersionedPage(string $url, ?string $expectedPageTitle, ?int $expectedPageId, int $workspaceId, int $backendUserId, int $expectedStatusCode): void + { + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/en-en/'), + $this->buildLanguageConfiguration('FR', '/fr-fr/', ['EN']), + $this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']), + ] + ); + $response = $this->executeFrontendSubRequest( + (new InternalRequest($url)), + (new InternalRequestContext()) + ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0) + ->withBackendUserId($backendUserId) + ); + $responseStructure = ResponseContent::fromString( + (string)$response->getBody() + ); + + self::assertSame($expectedStatusCode, $response->getStatusCode()); + self::assertSame($expectedPageId, $responseStructure->getScopePath('page/uid')); + self::assertSame($expectedPageTitle, $responseStructure->getScopePath('page/title')); + } } -- GitLab