1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Core\Routing;
19
20use Symfony\Component\Routing\Exception\ResourceNotFoundException;
21use TYPO3\CMS\Core\Routing\Aspect\MappableProcessor;
22
23/**
24 * Internal class, which is similar to Symfony's Urlmatcher but without validating
25 * - conditions / expression language
26 * - host matches
27 * - method checks
28 * because this method only works in conjunction with PageRouter.
29 *
30 * @internal
31 */
32class PageUriMatcher
33{
34    /**
35     * @var RouteCollection<string, Route>
36     */
37    protected $routes;
38
39    /**
40     * @var MappableProcessor
41     */
42    protected $mappableProcessor;
43
44    public function __construct(RouteCollection $routes)
45    {
46        $this->routes = $routes;
47        $this->mappableProcessor = new MappableProcessor();
48    }
49
50    /**
51     * Matches a path segment against the route collection
52     *
53     * @param string $urlPath
54     * @return array
55     * @throws ResourceNotFoundException
56     */
57    public function match(string $urlPath)
58    {
59        if ($ret = $this->matchCollection(rawurldecode($urlPath), $this->routes)) {
60            return $ret;
61        }
62        throw new ResourceNotFoundException(
63            sprintf('No routes found for "%s".', $urlPath),
64            1538156220
65        );
66    }
67
68    /**
69     * Tries to match a URL with a set of routes.
70     *
71     * @param string $urlPath The path info to be parsed
72     * @param RouteCollection<string,Route> $routes The set of routes
73     * @return array An array of parameters
74     */
75    protected function matchCollection(string $urlPath, RouteCollection $routes): ?array
76    {
77        foreach ($routes as $name => $route) {
78            $urlPath = $this->getDecoratedRoutePath($route) ?? $urlPath;
79            $compiledRoute = $route->compile();
80
81            // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
82            if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($urlPath, $compiledRoute->getStaticPrefix())) {
83                continue;
84            }
85
86            if (!preg_match($compiledRoute->getRegex(), $urlPath, $matches)) {
87                continue;
88            }
89
90            // custom handling of Mappable instances
91            if (!$this->mappableProcessor->resolve($route, $matches)) {
92                continue;
93            }
94
95            return $this->getAttributes($route, $name, $matches);
96        }
97        return null;
98    }
99
100    /**
101     * Resolves an optional route specific decorated route path that has been
102     * assigned by DecoratingEnhancerInterface instances.
103     *
104     * @param Route $route
105     * @return string|null
106     */
107    protected function getDecoratedRoutePath(Route $route): ?string
108    {
109        if (!$route->hasOption('_decoratedRoutePath')) {
110            return null;
111        }
112        $urlPath = $route->getOption('_decoratedRoutePath');
113        return rawurldecode($urlPath);
114    }
115
116    /**
117     * Returns an array of values to use as request attributes.
118     *
119     * As this method requires the Route object, it is not available
120     * in matchers that do not have access to the matched Route instance
121     * (like the PHP and Apache matcher dumpers).
122     *
123     * @param Route $route The route we are matching against
124     * @param string $name The name of the route
125     * @param array $attributes An array of attributes from the matcher
126     * @return array An array of parameters
127     */
128    protected function getAttributes(Route $route, string $name, array $attributes): array
129    {
130        $defaults = $route->getDefaults();
131        if (isset($defaults['_canonical_route'])) {
132            $name = $defaults['_canonical_route'];
133            unset($defaults['_canonical_route']);
134        }
135        $attributes['_route'] = $name;
136        // store applied default values in route options
137        $relevantDefaults = array_intersect_key($defaults, array_flip($route->compile()->getPathVariables()));
138        // option '_appliedDefaults' contains internal(!) values (default values are not mapped when resolving)
139        // (keys used are deflated and need to be inflated later using VariableProcessor)
140        $route->setOption('_appliedDefaults', array_diff_key($relevantDefaults, $attributes));
141        // side note: $defaults can contain e.g. '_controller'
142        return $this->mergeDefaults($attributes, $defaults);
143    }
144
145    /**
146     * Get merged default parameters.
147     *
148     * @param array $params The parameters
149     * @param array $defaults The defaults
150     * @return array Merged default parameters
151     */
152    protected function mergeDefaults(array $params, array $defaults): array
153    {
154        foreach ($params as $key => $value) {
155            if (!is_int($key) && null !== $value) {
156                $defaults[$key] = $value;
157            }
158        }
159        return $defaults;
160    }
161}
162