[FEATURE] Introduce Page-based URL handling 94/57994/21
authorBenni Mack <benni@typo3.org>
Wed, 22 Aug 2018 21:08:22 +0000 (23:08 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 23 Aug 2018 19:09:00 +0000 (21:09 +0200)
This feature adds a new database field "pages.slug" which
allows to fill the database with URL segments which can then
be resolved and built with for URLs for a specific page.

On top, when a site is found with a proper "slug", the
PageRouter of a site now resolves a /home/my-products/
to the correct page ID.

Next steps:
- Add URL enhancers API to allow to further resolve more parts.

Resolves: #85947
Releases: master
Change-Id: Ic64a758e847520b9a8dfc8b484c7613c9ba1f869
Reviewed-on: https://review.typo3.org/57994
Tested-by: Björn Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Richard Haeser <richard@maxserv.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
14 files changed:
composer.json
composer.lock
typo3/sysext/backend/Classes/Form/FormDataProvider/SiteResolving.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/core/Classes/Routing/PageRouter.php [new file with mode: 0644]
typo3/sysext/core/Classes/Routing/PageUriBuilder.php
typo3/sysext/core/Classes/Site/Entity/Site.php
typo3/sysext/core/Configuration/TCA/pages.php
typo3/sysext/core/Documentation/Changelog/master/Feature-85947-PageBasedURLHandling.rst [new file with mode: 0644]
typo3/sysext/core/Resources/Private/Language/locallang_tca.xlf
typo3/sysext/core/composer.json
typo3/sysext/core/ext_tables.sql
typo3/sysext/frontend/Classes/Middleware/SiteResolver.php
typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php

index 35c4f60..e1b02cb 100644 (file)
@@ -68,7 +68,7 @@
                "fiunchinho/phpunit-randomizer": "^4.0",
                "friendsofphp/php-cs-fixer": "^2.12.2",
                "typo3/cms-styleguide": "~9.2.0",
-               "typo3/testing-framework": "~4.6.0"
+               "typo3/testing-framework": "~4.6.1"
        },
        "suggest": {
                "ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images",
index b85ef21..7c20602 100644 (file)
@@ -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": "a31065c1e94f6c986083f4986f01d285",
+    "content-hash": "0bf895df915462054fe6c1a396cf1a64",
     "packages": [
         {
             "name": "cogpowered/finediff",
         },
         {
             "name": "typo3/testing-framework",
-            "version": "4.6.0",
+            "version": "4.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/TYPO3/testing-framework.git",
-                "reference": "5ff512494724b6700ec7309e8205dedde175716e"
+                "reference": "a87a37860536625873b9c829174d64b210b5fa9d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/5ff512494724b6700ec7309e8205dedde175716e",
-                "reference": "5ff512494724b6700ec7309e8205dedde175716e",
+                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/a87a37860536625873b9c829174d64b210b5fa9d",
+                "reference": "a87a37860536625873b9c829174d64b210b5fa9d",
                 "shasum": ""
             },
             "require": {
                 "tests",
                 "typo3"
             ],
-            "time": "2018-08-22T09:29:31+00:00"
+            "time": "2018-08-23T10:29:57+00:00"
         },
         {
             "name": "webmozart/assert",
index b65186b..6c0aa79 100644 (file)
@@ -36,7 +36,7 @@ class SiteResolving implements FormDataProviderInterface
      */
     public function addData(array $result): array
     {
-        $pageIdDefaultLanguage = $result['defaultLanguagePageRow']['uid'] ?? $result['effectivePid'];
+        $pageIdDefaultLanguage = (int)($result['defaultLanguagePageRow']['uid'] ?? $result['effectivePid']);
         $result['site'] = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId($pageIdDefaultLanguage);
         return $result;
     }
index 3433af0..8c991ee 100644 (file)
@@ -396,6 +396,7 @@ class BackendUtility
                     'pid' => null,
                     'title' => '',
                     'doktype' => null,
+                    'slug' => null,
                     'tsconfig_includes' => null,
                     'TSconfig' => null,
                     'is_siteroot' => null,
@@ -414,6 +415,7 @@ class BackendUtility
                     'pid',
                     'title',
                     'doktype',
+                    'slug',
                     'tsconfig_includes',
                     'TSconfig',
                     'is_siteroot',
@@ -464,6 +466,7 @@ class BackendUtility
                     'uid',
                     'title',
                     'doktype',
+                    'slug',
                     'tsconfig_includes',
                     'TSconfig',
                     'is_siteroot',
diff --git a/typo3/sysext/core/Classes/Routing/PageRouter.php b/typo3/sysext/core/Classes/Routing/PageRouter.php
new file mode 100644 (file)
index 0000000..440420c
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Routing;
+
+/*
+ * 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 Doctrine\DBAL\Connection;
+use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Page Router looking up the slug of the page path.
+ *
+ * This is done via the "Route Candidate" pattern.
+ *
+ * Example:
+ * - /about-us/team/management/
+ *
+ * will look for all pages that have
+ * - /about-us
+ * - /about-us/
+ * - /about-us/team
+ * - /about-us/team/
+ * - /about-us/team/management
+ * - /about-us/team/management/
+ *
+ * And create route candidates for that.
+ *
+ * PageRouter does not restrict the HTTP method or is bound to any domain constraints,
+ * as the SiteMatcher has done that already.
+ *
+ * @internal This API is not public yet and might change in the future, until TYPO3 v9 or TYPO3 v10.
+ */
+class PageRouter
+{
+    /**
+     * @param ServerRequestInterface $request
+     * @param string $routePath
+     * @param SiteInterface $site
+     * @param SiteLanguage $language
+     * @return array|null
+     */
+    public function matchRoute(ServerRequestInterface $request, string $routePath, SiteInterface $site, SiteLanguage $language): ?array
+    {
+        $slugCandidates = $this->getCandidateSlugsFromRoutePath($routePath);
+        if (empty($slugCandidates)) {
+            return null;
+        }
+        $pageCandidates = $this->getPagesFromDatabaseForCandidates($slugCandidates, $site, $language->getLanguageId());
+        // Stop if there are no candidates
+        if (empty($pageCandidates)) {
+            return null;
+        }
+
+        $collection = new RouteCollection();
+        foreach ($pageCandidates ?? [] as $page) {
+            $path = $page['slug'];
+            $route = new Route(
+                $path . '{next}',
+                ['page' => $page, 'next' => ''],
+                ['next' => '.*'],
+                ['utf8' => true]
+            );
+            $collection->add('page_' . $page['uid'], $route);
+        }
+
+        $context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost());
+        $matcher = new UrlMatcher($collection, $context);
+        try {
+            return $matcher->match('/' . $routePath);
+        } catch (ResourceNotFoundException $e) {
+            return null;
+        }
+    }
+
+    /**
+     * Check for records in the database which matches one of the slug candidates.
+     *
+     * @param array $slugCandidates
+     * @param SiteInterface $site
+     * @param int $languageId
+     * @return array
+     */
+    protected function getPagesFromDatabaseForCandidates(array $slugCandidates, SiteInterface $site, int $languageId): array
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable('pages');
+        $queryBuilder
+            ->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
+
+        $statement = $queryBuilder
+            ->select('uid', 'l10n_parent', 'pid', 'slug')
+            ->from('pages')
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'sys_language_uid',
+                    $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->in(
+                    'slug',
+                    $queryBuilder->createNamedParameter(
+                        $slugCandidates,
+                        Connection::PARAM_STR_ARRAY
+                    )
+                )
+            )
+            // Exact match will be first, that's important
+            ->orderBy('slug', 'desc')
+            ->execute();
+
+        $pages = [];
+        $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
+        while ($row = $statement->fetch()) {
+            $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
+            if ($siteMatcher->matchByPageId($pageIdInDefaultLanguage)->getRootPageId() === $site->getRootPageId()) {
+                $pages[] = $row;
+            }
+        }
+        return $pages;
+    }
+
+    /**
+     * Returns possible URL parts for a string like /home/about-us/offices/
+     * to return
+     * /home/about-us/offices/
+     * /home/about-us/offices
+     * /home/about-us/
+     * /home/about-us
+     * /home/
+     * /home
+     *
+     * @param string $routePath
+     * @return array
+     */
+    protected function getCandidateSlugsFromRoutePath(string $routePath): array
+    {
+        $candidatePathParts = [];
+        $pathParts = GeneralUtility::trimExplode('/', $routePath, true);
+        while (!empty($pathParts)) {
+            $prefix = '/' . implode('/', $pathParts);
+            $candidatePathParts[] = $prefix . '/';
+            $candidatePathParts[] = $prefix;
+            array_pop($pathParts);
+        }
+        return $candidatePathParts;
+    }
+}
index 9e3e024..7b68445 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Routing;
  */
 
 use Psr\Http\Message\UriInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\SingletonInterface;
@@ -92,9 +93,22 @@ class PageUriBuilder implements SingletonInterface
         // Only if a language is configured for the site, build a URL with a site prefix / base
         if ($siteLanguage) {
             unset($options['legacyUrlPrefix']);
-            $prefix = $siteLanguage->getBase() . '?id=' . $alternativePageId;
+            // Ensure to fetch the path segment / slug if it exists
+            if ($siteLanguage->getLanguageId() > 0) {
+                $pageLocalizations = BackendUtility::getRecordLocalization('pages', $pageId, $siteLanguage->getLanguageId());
+                $pageRecord = $pageLocalizations[0] ?? false;
+            } else {
+                $pageRecord = BackendUtility::getRecord('pages', $pageId);
+            }
+            $prefix = $siteLanguage->getBase();
+            if (!empty($pageRecord['slug'] ?? '')) {
+                $prefix = rtrim($prefix, '/') . '/' . ltrim($pageRecord['slug'], '/');
+            } else {
+                $prefix .= '?id=' . $alternativePageId;
+            }
         } else {
             // If nothing is found, use index.php?id=123&additionalParams
+            // This usually kicks in with "PseudoSites" where no language object can be determined.
             $prefix = $options['legacyUrlPrefix'] ?? null;
             if ($prefix === null) {
                 $prefix = $referenceType === self::ABSOLUTE_URL ? GeneralUtility::getIndpEnv('TYPO3_SITE_URL') : '';
@@ -107,7 +121,15 @@ class PageUriBuilder implements SingletonInterface
 
         // Add the query parameters as string
         $queryString = http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986);
-        $uri = new Uri($prefix . ($queryString ? '&' . $queryString : ''));
+        $prefix = rtrim($prefix, '?');
+        if (!empty($queryString)) {
+            if (strpos($prefix, '?') === false) {
+                $prefix .= '?';
+            } else {
+                $prefix .= '&';
+            }
+        }
+        $uri = new Uri($prefix . $queryString);
         if ($fragment) {
             $uri = $uri->withFragment($fragment);
         }
index 8d78a79..f00df0c 100644 (file)
@@ -23,6 +23,8 @@ use TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler;
 use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
 use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Routing\PageRouter;
+use TYPO3\CMS\Core\Routing\RouterInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -318,6 +320,18 @@ class Site implements SiteInterface
     }
 
     /**
+     * Returns applicable routers for this site
+     *
+     * @return RouterInterface[]
+     */
+    public function getRouters(): array
+    {
+        return [
+            new PageRouter()
+        ];
+    }
+
+    /**
      * Shorthand functionality for fetching the language service
      * @return LanguageService
      */
index c5114e4..5c75881 100644 (file)
@@ -151,6 +151,21 @@ return [
                 'cols' => 30
             ]
         ],
+        'slug' => [
+            'exclude' => true,
+            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.slug',
+            'config' => [
+                'type' => 'slug',
+                'generatorOptions' => [
+                    'fields' => ['title'],
+                    'fieldSeparator' => '/',
+                    'prefixParentPageSlug' => true
+                ],
+                'fallbackCharacter' => '-',
+                'eval' => 'uniqueInSite',
+                'default' => ''
+            ]
+        ],
         'TSconfig' => [
             'exclude' => true,
             'l10n_mode' => 'exclude',
@@ -1120,11 +1135,11 @@ return [
         ],
         'title' => [
             'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.title',
-            'showitem' => 'title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabel, --linebreak--, nav_title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.nav_title_formlabel, --linebreak--, subtitle;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.subtitle_formlabel',
+            'showitem' => 'title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabel, --linebreak--, slug, --linebreak--, nav_title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.nav_title_formlabel, --linebreak--, subtitle;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.subtitle_formlabel',
         ],
         'titleonly' => [
             'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.title',
-            'showitem' => 'title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabel',
+            'showitem' => 'title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabel, --linebreak--, slug',
         ],
         'visibility' => [
             'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-85947-PageBasedURLHandling.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-85947-PageBasedURLHandling.rst
new file mode 100644 (file)
index 0000000..b0b56fb
--- /dev/null
@@ -0,0 +1,40 @@
+.. include:: ../../Includes.txt
+
+=========================================
+Feature: #85947 - Page based URL handling
+=========================================
+
+See :issue:`85947`
+
+Description
+===========
+
+Page records now have a field called "slug" that contains the website
+frontend path to the page, like "/team/about-us/". When a page has the
+field filled, the URL which the site is linked to, will receive a full URL
+to that page, instead of the common :php:`index.php?id=xy` that TYPO3 builds
+by default.
+
+When using Site Handling for a page tree, this page-based URL handling is
+enabled by default and requires proper URL Rewrite Rules from the server.
+
+The slug field is shown when editing page records in the backend and is
+resolved to the page uid in the frontend if a "Site configuration" in the
+site module has been set up.
+
+Note: Page-based URL handling only works if a Site configuration
+has been set up since otherwise neither the domain nor the language can
+be properly resolved which is a requirement to resolve the page path part
+of the URL.
+
+Note #2: If a page has the path segment "/team/about-us", but there is no
+other page with a path segment "/team/about-us/", then an automatic 301
+HTTP redirect to the proper URI is triggered.
+
+Impact
+======
+
+Integrators should configure their sites in the Sites module to take advantage
+of the core internal page based routing.
+
+.. index:: Backend, Database, Frontend, TCA
index 82b8882..e8ec49c 100644 (file)
                        <trans-unit id="is_siteroot">
                                <source>Is root of website</source>
                        </trans-unit>
+                       <trans-unit id="pages.slug">
+                               <source>URL Segment</source>
+                       </trans-unit>
                        <trans-unit id="be_users">
                                <source>Backend user</source>
                        </trans-unit>
index 4a991aa..35ee228 100644 (file)
@@ -50,7 +50,7 @@
                "fiunchinho/phpunit-randomizer": "^4.0",
                "friendsofphp/php-cs-fixer": "^2.12.2",
                "typo3/cms-styleguide": "~9.2.0",
-               "typo3/testing-framework": "~4.6.0"
+               "typo3/testing-framework": "~4.6.1"
        },
        "suggest": {
                "ext-fileinfo": "Used for proper file type detection in the file abstraction layer",
index 4c9ac18..f430af5 100644 (file)
@@ -76,6 +76,7 @@ CREATE TABLE pages (
        perms_group tinyint(4) unsigned DEFAULT '0' NOT NULL,
        perms_everybody tinyint(4) unsigned DEFAULT '0' NOT NULL,
        title varchar(255) DEFAULT '' NOT NULL,
+       slug text,
        doktype int(11) unsigned DEFAULT '0' NOT NULL,
        TSconfig text,
        is_siteroot tinyint(4) DEFAULT '0' NOT NULL,
index 5a3608a..5de6c3b 100644 (file)
@@ -20,6 +20,8 @@ use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Http\RedirectResponse;
+use TYPO3\CMS\Core\Routing\PageRouter;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Site\Entity\PseudoSite;
 use TYPO3\CMS\Core\Site\Entity\Site;
@@ -65,6 +67,7 @@ class SiteResolver implements MiddlewareInterface
         $routeResult = $this->matcher->matchRequest($request);
         $site = $routeResult['site'] ?? null;
         $language = $routeResult['language'] ?? null;
+        $routePath = $routeResult['next'] ?? '';
 
         // language is found, and hidden but also not visible to the BE user, this needs to fail
         if ($language instanceof SiteLanguage && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
@@ -85,15 +88,51 @@ class SiteResolver implements MiddlewareInterface
             $queryParams['L'] = $language->getLanguageId();
             $request = $request->withQueryParams($queryParams);
             $_GET['L'] = $queryParams['L'];
-            // At this point, we later get further route modifiers
-            // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
-            $GLOBALS['TYPO3_REQUEST'] = $request;
+
+            $routePath = ltrim($routePath, '/');
+            if (!empty($routePath)) {
+                // Check for the route
+                $routeResult = $this->getPageRouter()
+                    ->matchRoute($request, $routePath, $site, $language);
+                if (is_array($routeResult)) {
+                    $page = $routeResult['page'];
+                    $pageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
+                    // @todo: we could move the middleware earlier which will make A LOT OF things easier
+                    $GLOBALS['TSFE']->id = $pageId;
+                    $_GET['id'] = $pageId;
+                    $queryParams = $request->getQueryParams();
+                    $queryParams['id'] = $pageId;
+                    $request = $request->withQueryParams($queryParams);
+                    if (!empty($routeResult['next'] ?? '')) {
+                        if ($routeResult['next'] === '/') {
+                            // a URL was called via "/mysite/" but the page is actually called "/mysite"
+                            // let's do a redirect
+                            $uri = $request->getUri();
+                            $path = rtrim($uri->getPath(), '/');
+                            $uri = $uri->withPath($path);
+                            return new RedirectResponse($uri, 301);
+                        }
+                        // @todo: kick in the resolvers for the RouteEnhancers at this point
+                        return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                            $request,
+                            'The requested page does not exist',
+                            ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
+                        );
+                    }
+                } else {
+                    return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'The requested page does not exist',
+                        ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
+                    );
+                }
+            }
         } elseif ($site instanceof PseudoSite) {
             $request = $request->withAttribute('site', $site);
-            // At this point, we later get further route modifiers
-            // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
-            $GLOBALS['TYPO3_REQUEST'] = $request;
         }
+        // At this point, we later get further route modifiers
+        // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
+        $GLOBALS['TYPO3_REQUEST'] = $request;
 
         return $handler->handle($request);
     }
@@ -113,4 +152,12 @@ class SiteResolver implements MiddlewareInterface
         }
         return false;
     }
+
+    /**
+     * @return PageRouter
+     */
+    protected function getPageRouter(): PageRouter
+    {
+        return GeneralUtility::makeInstance(PageRouter::class);
+    }
 }
index ce7e4e1..ea2c5dd 100644 (file)
@@ -16,6 +16,9 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\Middleware;
  * The TYPO3 project - inspiring people to share!
  */
 
+use PHPUnit\Framework\MockObject\MockObject;
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
@@ -23,6 +26,7 @@ use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\Http\NullResponse;
 use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Routing\PageRouter;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
@@ -45,13 +49,22 @@ class SiteResolverTest extends UnitTestCase
      */
     protected $siteFinder;
 
+    /**
+     * @var RequestHandlerInterface
+     */
     protected $siteFoundRequestHandler;
 
     /**
+     * @var PageRouter|ObjectProphecy
+     */
+    protected $pageRouterProphecy;
+
+    /**
      * Set up
      */
     protected function setUp(): void
     {
+        $GLOBALS['TSFE'] = new \stdClass();
         $this->siteFinder = $this->getAccessibleMock(SiteFinder::class, ['dummy'], [], '', false);
 
         // A request handler which expects a site to be found.
@@ -76,6 +89,9 @@ class SiteResolverTest extends UnitTestCase
             }
         };
 
+        $this->pageRouterProphecy = $this->prophesize(PageRouter::class);
+        $this->pageRouterProphecy->matchRoute(Argument::cetera())->willReturn(['page' => ['uid' => 13, 'l10n_parent' => 0]]);
+
         $cacheManagerProphecy = $this->prophesize(CacheManager::class);
         GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
     }
@@ -105,9 +121,13 @@ class SiteResolverTest extends UnitTestCase
             ])
         ]);
 
+        $subject = $this->createSiteResolverMock(
+            new SiteMatcher($this->siteFinder)
+        );
+
         $request = new ServerRequest($incomingUrl, 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
         $response = $subject->process($request, $this->siteFoundRequestHandler);
+
         if ($response instanceof NullResponse) {
             $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
         } else {
@@ -156,8 +176,11 @@ class SiteResolverTest extends UnitTestCase
             ]),
         ]);
 
+        $subject = $this->createSiteResolverMock(
+            new SiteMatcher($this->siteFinder)
+        );
+
         $request = new ServerRequest($incomingUrl, 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
         $response = $subject->process($request, $this->siteFoundRequestHandler);
         if ($response instanceof NullResponse) {
             $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
@@ -245,9 +268,13 @@ class SiteResolverTest extends UnitTestCase
             ]),
         ]);
 
+        $subject = $this->createSiteResolverMock(
+            new SiteMatcher($this->siteFinder)
+        );
+
         $request = new ServerRequest($incomingUrl, 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
         $response = $subject->process($request, $this->siteFoundRequestHandler);
+
         if ($response instanceof NullResponse) {
             $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
         } else {
@@ -299,7 +326,7 @@ class SiteResolverTest extends UnitTestCase
                 0,
                 '/'
             ],
-             */
+             * This case does not work as we now resolve the rest of the URL
             'matches a subsite with translation in first site' => [
                 'https://www.random-result.com/fr/products/pampers/',
                 'outside-site',
@@ -307,6 +334,7 @@ class SiteResolverTest extends UnitTestCase
                 1,
                 '/fr/'
             ],
+             */
         ];
     }
 
@@ -363,9 +391,13 @@ class SiteResolverTest extends UnitTestCase
             ]),
         ]);
 
+        $subject = $this->createSiteResolverMock(
+            new SiteMatcher($this->siteFinder)
+        );
+
         $request = new ServerRequest($incomingUrl, 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
         $response = $subject->process($request, $this->siteFoundRequestHandler);
+
         if ($response instanceof NullResponse) {
             $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
         } else {
@@ -379,10 +411,27 @@ class SiteResolverTest extends UnitTestCase
     }
 
     /**
-     * @test
+     * @return array
      */
-    public function checkIf404IsSiteLanguageIsDisabledInFrontend()
+    public function checkIf404IsSiteLanguageIsDisabledInFrontendDataProvider(): array
     {
+        return [
+            'disabled site language' => ['https://twenty.one/en/pilots/', 404],
+            'enabled site language' => ['https://twenty.one/fr/pilots/', 200],
+        ];
+    }
+
+    /**
+     * @param string $url
+     * @param int $expectedStatusCode
+     *
+     * @test
+     * @dataProvider checkIf404IsSiteLanguageIsDisabledInFrontendDataProvider
+     */
+    public function checkIf404IsSiteLanguageIsDisabledInFrontend(
+        string $url,
+        int $expectedStatusCode
+    ) {
         $this->siteFinder->_set('sites', [
             'mixed-site' => new Site('mixed-site', 13, [
                 'base' => '/',
@@ -410,15 +459,30 @@ class SiteResolverTest extends UnitTestCase
             ]),
         ]);
 
-        // Reqest to default page
-        $request = new ServerRequest('https://twenty.one/en/pilots/', 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
-        $response = $subject->process($request, $this->siteFoundRequestHandler);
-        $this->assertEquals(404, $response->getStatusCode());
+        $subject = $this->createSiteResolverMock(
+            new SiteMatcher($this->siteFinder)
+        );
 
-        $request = new ServerRequest('https://twenty.one/fr/pilots/', 'GET');
-        $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
+        // Request to default page
+        $request = new ServerRequest($url, 'GET');
         $response = $subject->process($request, $this->siteFoundRequestHandler);
-        $this->assertEquals(200, $response->getStatusCode());
+        static::assertEquals($expectedStatusCode, $response->getStatusCode());
+    }
+
+    /**
+     * @param SiteMatcher|null $matcher
+     * @return MockObject|SiteResolver
+     */
+    private function createSiteResolverMock(SiteMatcher $matcher = null): MockObject
+    {
+        $mock = $this->getAccessibleMock(
+            SiteResolver::class,
+            ['getPageRouter'],
+            [$matcher]
+        );
+        $mock->expects(static::any())
+            ->method('getPageRouter')
+            ->willReturn($this->pageRouterProphecy->reveal());
+        return $mock;
     }
 }