From 94456d3e218cd4c5d431c3d61beeace7e9a978f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech> Date: Sat, 10 Jun 2023 16:57:08 +0200 Subject: [PATCH] [BUGFIX] Resolve page with trailing slash requested without one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With #89091 pages with slugs containing trailing slash have been enabled to be resolved when requested with trailing slash in the request uri and have been a logical follow-up of #86055. In between an issue with language fallback chain have been found, which allowed that a valid translated page could be resolved with the default language slug even and reported as #88715 and #96010. These issues have been solved with https://review.typo3.org/c/Packages/TYPO3.CMS/+/75101 which partly broke the original implemented behaviour for pages with trailing slash slugs requested without a trailing slash. This change now ensures pages are resolved containing a trailing slash in their "slug" like "/my-page/subpage/" if requested without the trailing slash like "https://domain.tld/my-page/subpage". Additionally, tests are added to cover this case along with other possible cases. This should detect future regressions. The docblock return annotation for `PageRouter::matchRequest()` is changed to the correct returned value, removing ignore pattern for the phpstan baseline instead of increasing the counter. Note: This change ensures that a page can be resolved in any constellation with trailing slash in record slug/not in record slug and requested with trailing slash/not requested slug. In both cases, already working and the now fixed variant are serving both potential duplicate content - if no correct cannonical url is provided. Tackling the duplicate issue should be done in a dedicated change for both cases. Used command(s): > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #100990 Related: #96010 Related: #88715 Related: #89091 Related: #86055 Releases: main, 12.4, 11.5 Change-Id: I9f26c4500e2f812e8727b4b565570fcc579bf3e6 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79679 Tested-by: core-ci <typo3@b13.com> Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Stefan Bürk <stefan@buerk.tech> --- Build/phpstan/phpstan-baseline.neon | 5 - .../core/Classes/Routing/PageRouter.php | 23 +- .../SiteHandling/SlugSiteRequestTest.php | 1423 ++++++++++++++++- 3 files changed, 1356 insertions(+), 95 deletions(-) diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index 60af1e2f539a..f4fb5e158185 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -1060,11 +1060,6 @@ parameters: count: 1 path: ../../typo3/sysext/core/Classes/Routing/PageRouter.php - - - message: "#^Method TYPO3\\\\CMS\\\\Core\\\\Routing\\\\PageRouter\\:\\:matchRequest\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Routing\\\\SiteRouteResult but returns TYPO3\\\\CMS\\\\Core\\\\Routing\\\\PageArguments\\.$#" - count: 3 - path: ../../typo3/sysext/core/Classes/Routing/PageRouter.php - - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(TYPO3\\\\CMS\\\\Core\\\\Routing\\\\Aspect\\\\AspectInterface\\)\\: mixed\\)\\|null, 'count' given\\.$#" count: 1 diff --git a/typo3/sysext/core/Classes/Routing/PageRouter.php b/typo3/sysext/core/Classes/Routing/PageRouter.php index 759c4eb40953..6e05a40d1693 100644 --- a/typo3/sysext/core/Classes/Routing/PageRouter.php +++ b/typo3/sysext/core/Classes/Routing/PageRouter.php @@ -92,7 +92,7 @@ class PageRouter implements RouterInterface * Finds a RouteResult based on the given request. * * @param RouteResultInterface|SiteRouteResult|null $previousResult - * @return SiteRouteResult + * @return RouteResultInterface|PageArguments * @throws RouteNotFoundException */ public function matchRequest(ServerRequestInterface $request, RouteResultInterface $previousResult = null): RouteResultInterface @@ -182,9 +182,9 @@ class PageRouter implements RouterInterface return $this->buildPageArguments($matchedRoute, $result, $request->getQueryParams()); } } catch (ResourceNotFoundException $e) { - // Second try, look for /my-page even though the request was called via /my-page/ and the slash - // was not part of the slug, but let's then check again - if (substr($prefixedUrlPath, -1) === '/') { + if (str_ends_with($prefixedUrlPath, '/')) { + // Second try, look for /my-page even though the request was called via /my-page/ and the slash + // was not part of the slug, but let's then check again try { $result = $matcher->match(rtrim($prefixedUrlPath, '/')); /** @var Route $matchedRoute */ @@ -197,6 +197,21 @@ class PageRouter implements RouterInterface } catch (ResourceNotFoundException $e) { // Do nothing } + } else { + // Second try, look for /my-page/ even though the request was called via /my-page and the slash + // was part of the slug, but let's then check again + try { + $result = $matcher->match($prefixedUrlPath . '/'); + /** @var Route $matchedRoute */ + $matchedRoute = $fullCollection->get($result['_route']); + // Only use route if page language variant matches current language, otherwise + // handle it as route not found. + if ($this->isRouteReallyValidForLanguage($matchedRoute, $language)) { + return $this->buildPageArguments($matchedRoute, $result, $request->getQueryParams()); + } + } catch (ResourceNotFoundException $e) { + // Do nothing + } } } throw new RouteNotFoundException('No route found for path "' . $urlPath . '"', 1538389998); diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php index db1c003e6de0..3770327c0b11 100644 --- a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php +++ b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php @@ -546,7 +546,7 @@ final class SlugSiteRequestTest extends AbstractTestCase /** * @test */ - public function pageIsRenderedWithTrailingSlash(): void + public function pageWithTrailingSlashSlugIsRenderedIfRequestedWithSlash(): void { $uri = 'https://website.us/features/frontend-editing/'; @@ -566,6 +566,75 @@ final class SlugSiteRequestTest extends AbstractTestCase self::assertSame('EN: Frontend Editing', $responseStructure->getScopePath('page/title')); } + /** + * @test + */ + public function pageWithTrailingSlashSlugIsRenderedIfRequestedWithoutSlash(): void + { + $uri = 'https://website.us/features/frontend-editing'; + + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'), + $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']), + $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']), + ] + ); + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + $responseStructure = ResponseContent::fromString((string)$response->getBody()); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('EN: Frontend Editing', $responseStructure->getScopePath('page/title')); + } + + /** + * @test + */ + public function pageWithoutTrailingSlashSlugIsRenderedIfRequestedWithSlash(): void + { + $uri = 'https://website.us/features/'; + + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'), + $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']), + $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']), + ] + ); + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + $responseStructure = ResponseContent::fromString((string)$response->getBody()); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('EN: Features', $responseStructure->getScopePath('page/title')); + } + + /** + * @test + */ + public function pageWithoutTrailingSlashSlugIsRenderedIfRequestedWithoutSlash(): void + { + $uri = 'https://website.us/features'; + + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'), + $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']), + $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']), + ] + ); + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + $responseStructure = ResponseContent::fromString((string)$response->getBody()); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('EN: Features', $responseStructure->getScopePath('page/title')); + } + public static function restrictedPageIsRenderedDataProvider(): array { $instructions = [ @@ -1263,7 +1332,11 @@ final class SlugSiteRequestTest extends AbstractTestCase public static function defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExistsDataProvider(): \Generator { - yield 'Default slug with default base resolves' => [ + // ---------------------------------------------------------------- + // #1 page slug without trailing slash, request with trailing slash + // ---------------------------------------------------------------- + + yield '#1 Default slug with default base resolves' => [ 'uri' => 'https://website.local/welcome/', 'recordUpdates' => [], 'fallbackIdentifiers' => [ @@ -1274,7 +1347,7 @@ final class SlugSiteRequestTest extends AbstractTestCase 'expectedPageTitle' => 'EN: Welcome', ]; - yield 'FR slug with FR base resolves' => [ + yield '#1 FR slug with FR base resolves' => [ 'uri' => 'https://website.local/fr-fr/bienvenue/', 'recordUpdates' => [], 'fallbackIdentifiers' => [ @@ -1286,7 +1359,7 @@ final class SlugSiteRequestTest extends AbstractTestCase ]; // Using default language slug with language base should be page not found if language page is active. - yield 'Default slug with default base do not resolve' => [ + yield '#1 Default slug with default base do not resolve' => [ 'uri' => 'https://website.local/fr-fr/welcome/', 'recordUpdates' => [], 'fallbackIdentifiers' => [ @@ -1298,7 +1371,7 @@ final class SlugSiteRequestTest extends AbstractTestCase ]; // Using default language slug with language base resolves for inactive / hidden language page - yield 'Default slug with default base but inactive language page resolves' => [ + yield '#1 Default slug with default base but inactive language page resolves' => [ 'uri' => 'https://website.local/fr-fr/welcome/', 'recordUpdates' => [ 'pages' => [ @@ -1320,59 +1393,53 @@ final class SlugSiteRequestTest extends AbstractTestCase 'expectedStatusCode' => 200, 'expectedPageTitle' => 'EN: Welcome', ]; - } - - /** - * @link https://forge.typo3.org/issues/96010 - * @test - * @dataProvider defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExistsDataProvider - */ - public function defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExists(string $uri, array $recordUpdates, array $fallbackIdentifiers, string $fallbackType, int $expectedStatusCode, ?string $expectedPageTitle): void - { - $this->writeSiteConfiguration( - 'website-local', - $this->buildSiteConfiguration(1000, 'https://website.local/'), - [ - $this->buildDefaultLanguageConfiguration('EN', '/'), - $this->buildLanguageConfiguration('FR', 'https://website.local/fr-fr/', ['EN']), - ] - ); - if ($recordUpdates !== []) { - foreach ($recordUpdates as $table => $records) { - foreach ($records as $record) { - $this->getConnectionPool()->getConnectionForTable($table) - ->update( - $table, - $record['data'] ?? [], - $record['identifiers'] ?? [], - $record['types'] ?? [] - ); - } - } - } - - $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); - $responseStructure = ResponseContent::fromString( - (string)$response->getBody() - ); - self::assertSame( - $expectedStatusCode, - $response->getStatusCode() - ); - if ($expectedPageTitle !== null) { - self::assertSame( - $expectedPageTitle, - $responseStructure->getScopePath('page/title') - ); - } - } + // ------------------------------------------------------------- + // #2 page slug with trailing slash, request with trailing slash + // ------------------------------------------------------------- - public static function defaultLanguagePageNotResolvedForSiteLanguageBaseWithNonDefaultLanguageShorterUriIfLanguagePageExistsDataProvider(): \Generator - { - yield 'Default slug with default base resolves' => [ - 'uri' => 'https://website.local/en-en/welcome/', - 'recordUpdates' => [], + yield '#2 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], 'fallbackIdentifiers' => [ 'EN', ], @@ -1381,9 +1448,48 @@ final class SlugSiteRequestTest extends AbstractTestCase 'expectedPageTitle' => 'EN: Welcome', ]; - yield 'FR slug with FR base resolves' => [ - 'uri' => 'https://website.local/bienvenue/', - 'recordUpdates' => [], + yield '#2 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/fr-fr/bienvenue/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], 'fallbackIdentifiers' => [ 'EN', ], @@ -1393,42 +1499,1187 @@ final class SlugSiteRequestTest extends AbstractTestCase ]; // Using default language slug with language base should be page not found if language page is active. - yield 'Default slug with default base do not resolve' => [ - 'uri' => 'https://website.local/welcome/', - 'recordUpdates' => [], - 'fallbackIdentifiers' => [ - 'EN', + yield '#2 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/fr-fr/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], ], - 'fallbackType' => 'strict', - 'expectedStatusCode' => 404, - 'expectedPageTitle' => null, - ]; - - // Using default language slug with language base should be page not found if language page is active. - yield 'Default slug with default base do not resolve strict without fallback' => [ - 'uri' => 'https://website.local/welcome/', - 'recordUpdates' => [], - 'fallbackIdentifiers' => [], - 'fallbackType' => 'fallback', - 'expectedStatusCode' => 404, - 'expectedPageTitle' => null, - ]; - - // Using default language slug with language base should be page not found if language page is active. - yield 'Default slug with default base do not resolve fallback' => [ - 'uri' => 'https://website.local/welcome/', - 'recordUpdates' => [], 'fallbackIdentifiers' => [ 'EN', ], - 'fallbackType' => 'fallback', + 'fallbackType' => 'strict', 'expectedStatusCode' => 404, 'expectedPageTitle' => null, ]; // Using default language slug with language base resolves for inactive / hidden language page - yield 'Default slug with default base but inactive language page resolves' => [ - 'uri' => 'https://website.local/welcome/', + yield '#2 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/fr-fr/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + // ---------------------------------------------------------------- + // #3 page slug with trailing slash, request without trailing slash + // ---------------------------------------------------------------- + + yield '#3 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#3 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/fr-fr/bienvenue', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#3 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/fr-fr/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#3 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/fr-fr/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + // ------------------------------------------------------------------- + // #4 page slug without trailing slash, request without trailing slash + // ------------------------------------------------------------------- + + yield '#4 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#4 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/fr-fr/bienvenue', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#4 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/fr-fr/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#4 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/fr-fr/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + } + + /** + * @link https://forge.typo3.org/issues/96010 + * @test + * @dataProvider defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExistsDataProvider + */ + public function defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExists(string $uri, array $recordUpdates, array $fallbackIdentifiers, string $fallbackType, int $expectedStatusCode, ?string $expectedPageTitle): void + { + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + $this->buildLanguageConfiguration('FR', 'https://website.local/fr-fr/', ['EN']), + ] + ); + if ($recordUpdates !== []) { + foreach ($recordUpdates as $table => $records) { + foreach ($records as $record) { + $this->getConnectionPool()->getConnectionForTable($table) + ->update( + $table, + $record['data'] ?? [], + $record['identifiers'] ?? [], + $record['types'] ?? [] + ); + } + } + } + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + $responseStructure = ResponseContent::fromString( + (string)$response->getBody() + ); + + self::assertSame( + $expectedStatusCode, + $response->getStatusCode() + ); + if ($expectedPageTitle !== null) { + self::assertSame( + $expectedPageTitle, + $responseStructure->getScopePath('page/title') + ); + } + } + + public static function defaultLanguagePageNotResolvedForSiteLanguageBaseWithNonDefaultLanguageShorterUriIfLanguagePageExistsDataProvider(): \Generator + { + // ---------------------------------------------------------------- + // #1 page slug without trailing slash, request with trailing slash + // ---------------------------------------------------------------- + + yield '#1 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/en-en/welcome/', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#1 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/bienvenue/', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#1 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#1 Default slug with default base do not resolve strict without fallback' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#1 Default slug with default base do not resolve fallback' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#1 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + // ------------------------------------------------------------- + // #2 page slug with trailing slash, request with trailing slash + // ------------------------------------------------------------- + + yield '#2 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/en-en/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#2 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/bienvenue/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#2 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#2 Default slug with default base do not resolve strict without fallback' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#2 Default slug with default base do not resolve fallback' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#2 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/welcome/', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + // ---------------------------------------------------------------- + // #3 page slug with trailing slash, request without trailing slash + // ---------------------------------------------------------------- + + yield '#3 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/en-en/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#3 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/bienvenue', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#3 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#3 Default slug with default base do not resolve strict without fallback' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#3 Default slug with default base do not resolve fallback' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#3 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [ + 'pages' => [ + [ + 'data' => [ + 'slug' => '/welcome/', + ], + 'identifiers' => [ + 'uid' => 1100, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + 'hidden' => 1, + ], + 'identifiers' => [ + 'uid' => 1101, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1102, + ], + 'types' => [], + ], + [ + 'data' => [ + 'slug' => '/简-bienvenue/', + ], + 'identifiers' => [ + 'uid' => 1103, + ], + 'types' => [], + ], + ], + ], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + // ------------------------------------------------------------------- + // #4 page slug without trailing slash, request without trailing slash + // ------------------------------------------------------------------- + + yield '#4 Default slug with default base resolves' => [ + 'uri' => 'https://website.local/en-en/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'EN: Welcome', + ]; + + yield '#4 FR slug with FR base resolves' => [ + 'uri' => 'https://website.local/bienvenue', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 200, + 'expectedPageTitle' => 'FR: Welcome', + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#4 Default slug with default base do not resolve' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'strict', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#4 Default slug with default base do not resolve strict without fallback' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base should be page not found if language page is active. + yield '#4 Default slug with default base do not resolve fallback' => [ + 'uri' => 'https://website.local/welcome', + 'recordUpdates' => [], + 'fallbackIdentifiers' => [ + 'EN', + ], + 'fallbackType' => 'fallback', + 'expectedStatusCode' => 404, + 'expectedPageTitle' => null, + ]; + + // Using default language slug with language base resolves for inactive / hidden language page + yield '#4 Default slug with default base but inactive language page resolves' => [ + 'uri' => 'https://website.local/welcome', 'recordUpdates' => [ 'pages' => [ [ -- GitLab