Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 liveuser 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Routing\Matcher;
13
 
14
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
15
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
18
use Symfony\Component\Routing\Exception\NoConfigurationException;
19
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
20
use Symfony\Component\Routing\RequestContext;
21
use Symfony\Component\Routing\Route;
22
use Symfony\Component\Routing\RouteCollection;
23
 
24
/**
25
 * UrlMatcher matches URL based on a set of routes.
26
 *
27
 * @author Fabien Potencier <fabien@symfony.com>
28
 */
29
class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
30
{
31
    const REQUIREMENT_MATCH = 0;
32
    const REQUIREMENT_MISMATCH = 1;
33
    const ROUTE_MATCH = 2;
34
 
35
    /** @var RequestContext */
36
    protected $context;
37
 
38
    /**
39
     * Collects HTTP methods that would be allowed for the request.
40
     */
41
    protected $allow = [];
42
 
43
    /**
44
     * Collects URI schemes that would be allowed for the request.
45
     *
46
     * @internal
47
     */
48
    protected $allowSchemes = [];
49
 
50
    protected $routes;
51
    protected $request;
52
    protected $expressionLanguage;
53
 
54
    /**
55
     * @var ExpressionFunctionProviderInterface[]
56
     */
57
    protected $expressionLanguageProviders = [];
58
 
59
    public function __construct(RouteCollection $routes, RequestContext $context)
60
    {
61
        $this->routes = $routes;
62
        $this->context = $context;
63
    }
64
 
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function setContext(RequestContext $context)
69
    {
70
        $this->context = $context;
71
    }
72
 
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function getContext()
77
    {
78
        return $this->context;
79
    }
80
 
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function match(string $pathinfo)
85
    {
86
        $this->allow = $this->allowSchemes = [];
87
 
88
        if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) {
89
            return $ret;
90
        }
91
 
92
        if ('/' === $pathinfo && !$this->allow && !$this->allowSchemes) {
93
            throw new NoConfigurationException();
94
        }
95
 
96
        throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
97
    }
98
 
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function matchRequest(Request $request)
103
    {
104
        $this->request = $request;
105
 
106
        $ret = $this->match($request->getPathInfo());
107
 
108
        $this->request = null;
109
 
110
        return $ret;
111
    }
112
 
113
    public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
114
    {
115
        $this->expressionLanguageProviders[] = $provider;
116
    }
117
 
118
    /**
119
     * Tries to match a URL with a set of routes.
120
     *
121
     * @param string $pathinfo The path info to be parsed
122
     *
123
     * @return array An array of parameters
124
     *
125
     * @throws NoConfigurationException  If no routing configuration could be found
126
     * @throws ResourceNotFoundException If the resource could not be found
127
     * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
128
     */
129
    protected function matchCollection(string $pathinfo, RouteCollection $routes)
130
    {
131
        // HEAD and GET are equivalent as per RFC
132
        if ('HEAD' === $method = $this->context->getMethod()) {
133
            $method = 'GET';
134
        }
135
        $supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
136
        $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
137
 
138
        foreach ($routes as $name => $route) {
139
            $compiledRoute = $route->compile();
140
            $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
141
            $requiredMethods = $route->getMethods();
142
 
143
            // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
144
            if ('' !== $staticPrefix && 0 !== strpos($trimmedPathinfo, $staticPrefix)) {
145
                continue;
146
            }
147
            $regex = $compiledRoute->getRegex();
148
 
149
            $pos = strrpos($regex, '$');
150
            $hasTrailingSlash = '/' === $regex[$pos - 1];
151
            $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
152
 
153
            if (!preg_match($regex, $pathinfo, $matches)) {
154
                continue;
155
            }
156
 
157
            $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath());
158
 
159
            if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) {
160
                if ($hasTrailingSlash) {
161
                    $matches = $m;
162
                } else {
163
                    $hasTrailingVar = false;
164
                }
165
            }
166
 
167
            $hostMatches = [];
168
            if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
169
                continue;
170
            }
171
 
172
            $status = $this->handleRouteRequirements($pathinfo, $name, $route);
173
 
174
            if (self::REQUIREMENT_MISMATCH === $status[0]) {
175
                continue;
176
            }
177
 
178
            if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
179
                if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
180
                    return $this->allow = $this->allowSchemes = [];
181
                }
182
                continue;
183
            }
184
 
185
            if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
186
                $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
187
                continue;
188
            }
189
 
190
            if ($requiredMethods && !\in_array($method, $requiredMethods)) {
191
                $this->allow = array_merge($this->allow, $requiredMethods);
192
                continue;
193
            }
194
 
195
            return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : []));
196
        }
197
 
198
        return [];
199
    }
200
 
201
    /**
202
     * Returns an array of values to use as request attributes.
203
     *
204
     * As this method requires the Route object, it is not available
205
     * in matchers that do not have access to the matched Route instance
206
     * (like the PHP and Apache matcher dumpers).
207
     *
208
     * @return array An array of parameters
209
     */
210
    protected function getAttributes(Route $route, string $name, array $attributes)
211
    {
212
        $defaults = $route->getDefaults();
213
        if (isset($defaults['_canonical_route'])) {
214
            $name = $defaults['_canonical_route'];
215
            unset($defaults['_canonical_route']);
216
        }
217
        $attributes['_route'] = $name;
218
 
219
        return $this->mergeDefaults($attributes, $defaults);
220
    }
221
 
222
    /**
223
     * Handles specific route requirements.
224
     *
225
     * @return array The first element represents the status, the second contains additional information
226
     */
227
    protected function handleRouteRequirements(string $pathinfo, string $name, Route $route)
228
    {
229
        // expression condition
230
        if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
231
            return [self::REQUIREMENT_MISMATCH, null];
232
        }
233
 
234
        return [self::REQUIREMENT_MATCH, null];
235
    }
236
 
237
    /**
238
     * Get merged default parameters.
239
     *
240
     * @return array Merged default parameters
241
     */
242
    protected function mergeDefaults(array $params, array $defaults)
243
    {
244
        foreach ($params as $key => $value) {
245
            if (!\is_int($key) && null !== $value) {
246
                $defaults[$key] = $value;
247
            }
248
        }
249
 
250
        return $defaults;
251
    }
252
 
253
    protected function getExpressionLanguage()
254
    {
255
        if (null === $this->expressionLanguage) {
256
            if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
257
                throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
258
            }
259
            $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
260
        }
261
 
262
        return $this->expressionLanguage;
263
    }
264
 
265
    /**
266
     * @internal
267
     */
268
    protected function createRequest(string $pathinfo): ?Request
269
    {
270
        if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
271
            return null;
272
        }
273
 
274
        return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [
275
            'SCRIPT_FILENAME' => $this->context->getBaseUrl(),
276
            'SCRIPT_NAME' => $this->context->getBaseUrl(),
277
        ]);
278
    }
279
}