[BUGFIX] Add RootPage to Slug Candidates
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / PageRouter.php
index 459b195..f6e0a9f 100644 (file)
@@ -28,13 +28,16 @@ use TYPO3\CMS\Core\Context\LanguageAspect;
 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\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\Routing\Aspect\AspectFactory;
 use TYPO3\CMS\Core\Routing\Aspect\MappableProcessor;
 use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
+use TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface;
 use TYPO3\CMS\Core\Routing\Enhancer\EnhancerFactory;
 use TYPO3\CMS\Core\Routing\Enhancer\EnhancerInterface;
 use TYPO3\CMS\Core\Routing\Enhancer\ResultingInterface;
+use TYPO3\CMS\Core\Routing\Enhancer\RoutingEnhancerInterface;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -104,17 +107,19 @@ class PageRouter implements RouterInterface
      * Finds a RouteResult based on the given request.
      *
      * @param ServerRequestInterface $request
-     * @param RouteResultInterface|RouteResult|null $previousResult
-     * @return RouteResult
+     * @param RouteResultInterface|SiteRouteResult|null $previousResult
+     * @return SiteRouteResult
+     * @throws RouteNotFoundException
      */
-    public function matchRequest(ServerRequestInterface $request, RouteResultInterface $previousResult = null): ?RouteResultInterface
+    public function matchRequest(ServerRequestInterface $request, RouteResultInterface $previousResult = null): RouteResultInterface
     {
-        $slugCandidates = $this->getCandidateSlugsFromRoutePath($previousResult->getTail() ?: '/');
+        $urlPath = $previousResult->getTail();
+        $slugCandidates = $this->getCandidateSlugsFromRoutePath($urlPath ?: '/');
         $language = $previousResult->getLanguage();
         $pageCandidates = $this->getPagesFromDatabaseForCandidates($slugCandidates, $language->getLanguageId());
         // Stop if there are no candidates
         if (empty($pageCandidates)) {
-            return null;
+            throw new RouteNotFoundException('No page candidates found for path "' . $urlPath . '"', 1538389999);
         }
 
         $fullCollection = new RouteCollection();
@@ -129,8 +134,16 @@ class PageRouter implements RouterInterface
                 ['utf8' => true, '_page' => $page]
             );
             $pageCollection->add('default', $defaultRouteForPage);
-            foreach ($this->getEnhancersForPage($pageIdForDefaultLanguage, $language) as $enhancer) {
-                $enhancer->enhanceForMatching($pageCollection);
+            $enhancers = $this->getEnhancersForPage($pageIdForDefaultLanguage, $language);
+            foreach ($enhancers as $enhancer) {
+                if ($enhancer instanceof DecoratingEnhancerInterface) {
+                    $enhancer->decorateForMatching($pageCollection, $urlPath);
+                }
+            }
+            foreach ($enhancers as $enhancer) {
+                if ($enhancer instanceof RoutingEnhancerInterface) {
+                    $enhancer->enhanceForMatching($pageCollection);
+                }
             }
 
             $pageCollection->addNamePrefix('page_' . $page['uid'] . '_');
@@ -139,14 +152,14 @@ class PageRouter implements RouterInterface
 
         $matcher = new PageUriMatcher($fullCollection);
         try {
-            $result = $matcher->match('/' . trim($previousResult->getTail(), '/'));
+            $result = $matcher->match('/' . trim($urlPath, '/'));
             /** @var Route $matchedRoute */
             $matchedRoute = $fullCollection->get($result['_route']);
             return $this->buildPageArguments($matchedRoute, $result, $request->getQueryParams());
         } catch (ResourceNotFoundException $e) {
-            // return nothing
+            // Do nothing
         }
-        return null;
+        throw new RouteNotFoundException('No route found for path "' . $urlPath . '"', 1538389998);
     }
 
     /**
@@ -157,6 +170,7 @@ class PageRouter implements RouterInterface
      * @param string $fragment additional #my-fragment part
      * @param string $type see the RouterInterface for possible types
      * @return UriInterface
+     * @throws InvalidRouteArgumentsException
      */
     public function generateUri($route, array $parameters = [], string $fragment = '', string $type = ''): UriInterface
     {
@@ -197,8 +211,16 @@ class PageRouter implements RouterInterface
 
         // cHash is never considered because cHash is built by this very method.
         unset($originalParameters['cHash']);
-        foreach ($this->getEnhancersForPage($pageId, $language) as $enhancer) {
-            $enhancer->enhanceForGeneration($collection, $originalParameters);
+        $enhancers = $this->getEnhancersForPage($pageId, $language);
+        foreach ($enhancers as $enhancer) {
+            if ($enhancer instanceof RoutingEnhancerInterface) {
+                $enhancer->enhanceForGeneration($collection, $originalParameters);
+            }
+        }
+        foreach ($enhancers as $enhancer) {
+            if ($enhancer instanceof DecoratingEnhancerInterface) {
+                $enhancer->decorateForGeneration($collection, $originalParameters);
+            }
         }
 
         $scheme = $language->getBase()->getScheme();
@@ -244,15 +266,17 @@ class PageRouter implements RouterInterface
             }
         }
 
+        if (!$uri instanceof UriInterface) {
+            throw new InvalidRouteArgumentsException('Uri could not be built for page "' . $pageId . '"', 1538390230);
+        }
+
         if ($pageRouteResult && $pageRouteResult->areDirty()) {
             // for generating URLs this should(!) never happen
             // if it does happen, generator logic has flaws
-            throw new \OverflowException('Route arguments are dirty', 1537613247);
+            throw new InvalidRouteArgumentsException('Route arguments are dirty', 1537613247);
         }
 
-        if ($matchedRoute && $pageRouteResult && $uri instanceof UriInterface
-            && !empty($pageRouteResult->getDynamicArguments())
-        ) {
+        if ($matchedRoute && $pageRouteResult && !empty($pageRouteResult->getDynamicArguments())) {
             $cacheHash = $this->generateCacheHash($pageId, $pageRouteResult);
 
             if (!empty($cacheHash)) {
@@ -308,8 +332,11 @@ class PageRouter implements RouterInterface
         $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
         while ($row = $statement->fetch()) {
             $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
-            if ($siteMatcher->matchByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId()) {
-                $pages[] = $row;
+            try {
+                if ($siteMatcher->matchByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId()) {
+                    $pages[] = $row;
+                }
+            } catch (SiteNotFoundException $e) {
             }
         }
         return $pages;
@@ -321,17 +348,17 @@ class PageRouter implements RouterInterface
      *
      * @param int $pageId
      * @param SiteLanguage $language
-     * @return \Generator|EnhancerInterface[]
+     * @return EnhancerInterface[]
      */
-    protected function getEnhancersForPage(int $pageId, SiteLanguage $language): \Generator
+    protected function getEnhancersForPage(int $pageId, SiteLanguage $language): array
     {
+        $enhancers = [];
         foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
             // Check if there is a restriction to page Ids.
             if (is_array($enhancerConfiguration['limitToPages'] ?? null) && !in_array($pageId, $enhancerConfiguration['limitToPages'])) {
                 continue;
             }
             $enhancerType = $enhancerConfiguration['type'] ?? '';
-            /** @var EnhancerInterface $enhancer */
             $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
             if (!empty($enhancerConfiguration['aspects'] ?? null)) {
                 $aspects = $this->aspectFactory->createAspects(
@@ -340,37 +367,91 @@ class PageRouter implements RouterInterface
                 );
                 $enhancer->setAspects($aspects);
             }
-            yield $enhancer;
+            $enhancers[] = $enhancer;
+        }
+        return $enhancers;
+    }
+
+    /**
+     * Resolves decorating enhancers without having aspects assigned. These
+     * instances are used to pre-process URL path and MUST NOT be used for
+     * actually resolving or generating URL parameters.
+     *
+     * @return DecoratingEnhancerInterface[]
+     */
+    protected function getDecoratingEnhancers(): array
+    {
+        $enhancers = [];
+        foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
+            $enhancerType = $enhancerConfiguration['type'] ?? '';
+            $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
+            if ($enhancer instanceof DecoratingEnhancerInterface) {
+                $enhancers[] = $enhancer;
+            }
         }
+        return $enhancers;
     }
 
     /**
-     * Returns possible URL parts for a string like /home/about-us/offices/
+     * Gets all patterns that can be used to redecorate (undecorate) a
+     * potential previously decorated route path.
+     *
+     * @return string regular expression pattern capable of redecorating
+     */
+    protected function getRoutePathRedecorationPattern(): string
+    {
+        $decoratingEnhancers = $this->getDecoratingEnhancers();
+        if (empty($decoratingEnhancers)) {
+            return '';
+        }
+        $redecorationPatterns = array_map(
+            function (DecoratingEnhancerInterface $decorationEnhancers) {
+                $pattern = $decorationEnhancers->getRoutePathRedecorationPattern();
+                return '(?:' . $pattern . ')';
+            },
+            $decoratingEnhancers
+        );
+        return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
+    }
+
+    /**
+     * Returns possible URL parts for a string like /home/about-us/offices/ or /home/about-us/offices.json
      * to return.
      *
      * /home/about-us/offices/
+     * /home/about-us/offices.json
      * /home/about-us/offices
      * /home/about-us/
      * /home/about-us
      * /home/
      * /home
+     * /
      *
      * @param string $routePath
      * @return array
      */
     protected function getCandidateSlugsFromRoutePath(string $routePath): array
     {
+        $redecorationPattern = $this->getRoutePathRedecorationPattern();
+        if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
+            $decoration = $matches['decoration'];
+            $decorationPattern = preg_quote($decoration, '#');
+            $routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath);
+        }
+
         $candidatePathParts = [];
         $pathParts = GeneralUtility::trimExplode('/', $routePath, true);
         if (empty($pathParts)) {
             return ['/'];
         }
+
         while (!empty($pathParts)) {
             $prefix = '/' . implode('/', $pathParts);
             $candidatePathParts[] = $prefix . '/';
             $candidatePathParts[] = $prefix;
             array_pop($pathParts);
         }
+        $candidatePathParts[] = '/';
         return $candidatePathParts;
     }
 
@@ -431,7 +512,30 @@ class PageRouter implements RouterInterface
         }
         $page = $route->getOption('_page');
         $pageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
-        return new PageArguments($pageId, $routeArguments, [], $remainingQueryParameters);
+        $type = $this->resolveType($route, $remainingQueryParameters);
+        return new PageArguments($pageId, $type, $routeArguments, [], $remainingQueryParameters);
+    }
+
+    /**
+     * Retrieves type from processed route and modifies remaining query parameters.
+     *
+     * @param Route $route
+     * @param array $remainingQueryParameters reference to remaining query parameters
+     * @return string
+     */
+    protected function resolveType(Route $route, array &$remainingQueryParameters): string
+    {
+        $type = $remainingQueryParameters['type'] ?? 0;
+        $decoratedParameters = $route->getOption('_decoratedParameters');
+        if (isset($decoratedParameters['type'])) {
+            $type = $decoratedParameters['type'];
+            unset($decoratedParameters['type']);
+            $remainingQueryParameters = array_replace_recursive(
+                $remainingQueryParameters,
+                $decoratedParameters
+            );
+        }
+        return (string)$type;
     }
 
     /**