ee2dc28acd041f38549f84c272d61a874fab2cb3
[yaffs-website] / web / core / lib / Drupal / Core / Routing / Router.php
1 <?php
2
3 namespace Drupal\Core\Routing;
4
5 use Drupal\Core\Path\CurrentPathStack;
6 use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
7 use Symfony\Cmf\Component\Routing\LazyRouteCollection;
8 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
9 use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
10 use Symfony\Component\HttpFoundation\Request;
11 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
12 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
13 use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
14 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
15 use Symfony\Component\Routing\RouteCollection;
16 use Symfony\Component\Routing\RouterInterface;
17
18 /**
19  * Router implementation in Drupal.
20  *
21  * A router determines, for an incoming request, the active controller, which is
22  * a callable that creates a response.
23  *
24  * It consists of several steps, of which each are explained in more details
25  * below:
26  * 1. Get a collection of routes which potentially match the current request.
27  *    This is done by the route provider. See ::getInitialRouteCollection().
28  * 2. Filter the collection down further more. For example this filters out
29  *    routes applying to other formats: See ::applyRouteFilters()
30  * 3. Find the best matching route out of the remaining ones, by applying a
31  *    regex. See ::matchCollection().
32  * 4. Enhance the list of route attributes, for example loading entity objects.
33  *    See ::applyRouteEnhancers().
34  *
35  * This implementation uses ideas of the following routers:
36  * - \Symfony\Cmf\Component\Routing\DynamicRouter
37  * - \Drupal\Core\Routing\UrlMatcher
38  * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
39  *
40  * @see \Symfony\Cmf\Component\Routing\DynamicRouter
41  * @see \Drupal\Core\Routing\UrlMatcher
42  * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
43  */
44 class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
45
46   /**
47    * The route provider responsible for the first-pass match.
48    *
49    * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
50    */
51   protected $routeProvider;
52
53   /**
54    * The list of available enhancers.
55    *
56    * @var \Drupal\Core\Routing\EnhancerInterface[]
57    */
58   protected $enhancers = [];
59
60   /**
61    * The list of available route filters.
62    *
63    * @var \Drupal\Core\Routing\FilterInterface[]
64    */
65   protected $filters = [];
66
67   /**
68    * The URL generator.
69    *
70    * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
71    */
72   protected $urlGenerator;
73
74   /**
75    * Constructs a new Router.
76    *
77    * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
78    *   The route provider.
79    * @param \Drupal\Core\Path\CurrentPathStack $current_path
80    *   The current path stack.
81    * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
82    *   The URL generator.
83    */
84   public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
85     parent::__construct($current_path);
86     $this->routeProvider = $route_provider;
87     $this->urlGenerator = $url_generator;
88   }
89
90   /**
91    * Adds a route filter.
92    *
93    * @param \Drupal\Core\Routing\FilterInterface $route_filter
94    *   The route filter.
95    */
96   public function addRouteFilter(FilterInterface $route_filter) {
97     $this->filters[] = $route_filter;
98   }
99
100   /**
101    * Adds a route enhancer.
102    *
103    * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
104    *   The route enhancer.
105    */
106   public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
107     $this->enhancers[] = $route_enhancer;
108   }
109
110   /**
111    * {@inheritdoc}
112    */
113   public function match($pathinfo) {
114     $request = Request::create($pathinfo);
115
116     return $this->matchRequest($request);
117   }
118
119   /**
120    * {@inheritdoc}
121    */
122   public function matchRequest(Request $request) {
123     $collection = $this->getInitialRouteCollection($request);
124     if ($collection->count() === 0) {
125       throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
126     }
127     $collection = $this->applyRouteFilters($collection, $request);
128
129     if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
130       return $this->applyRouteEnhancers($ret, $request);
131     }
132
133     throw 0 < count($this->allow)
134       ? new MethodNotAllowedException(array_unique($this->allow))
135       : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
136   }
137
138   /**
139    * Tries to match a URL with a set of routes.
140    *
141    * @param string $pathinfo
142    *   The path info to be parsed
143    * @param \Symfony\Component\Routing\RouteCollection $routes
144    *   The set of routes.
145    *
146    * @return array|null
147    *   An array of parameters. NULL when there is no match.
148    */
149   protected function matchCollection($pathinfo, RouteCollection $routes) {
150     // Try a case-sensitive match.
151     $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
152     // Try a case-insensitive match.
153     if ($match === NULL && $routes->count() > 0) {
154       $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
155     }
156     return $match;
157   }
158
159   /**
160    * Tries to match a URL with a set of routes.
161    *
162    * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
163    * supports case-insensitive matching. The static prefix optimization is
164    * removed as this duplicates work done by the query in
165    * RouteProvider::getRoutesByPath().
166    *
167    * @param string $pathinfo
168    *   The path info to be parsed
169    * @param \Symfony\Component\Routing\RouteCollection $routes
170    *   The set of routes.
171    * @param bool $case_sensitive
172    *   Determines if the match should be case-sensitive of not.
173    *
174    * @return array|null
175    *   An array of parameters. NULL when there is no match.
176    *
177    * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
178    * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
179    */
180   protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
181     foreach ($routes as $name => $route) {
182       $compiledRoute = $route->compile();
183
184       // Set the regex to use UTF-8.
185       $regex = $compiledRoute->getRegex() . 'u';
186       if (!$case_sensitive) {
187         $regex = $regex . 'i';
188       }
189       if (!preg_match($regex, $pathinfo, $matches)) {
190         continue;
191       }
192
193       $hostMatches = [];
194       if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
195         $routes->remove($name);
196         continue;
197       }
198
199       // Check HTTP method requirement.
200       if ($requiredMethods = $route->getMethods()) {
201         // HEAD and GET are equivalent as per RFC.
202         if ('HEAD' === $method = $this->context->getMethod()) {
203           $method = 'GET';
204         }
205
206         if (!in_array($method, $requiredMethods)) {
207           $this->allow = array_merge($this->allow, $requiredMethods);
208           $routes->remove($name);
209           continue;
210         }
211       }
212
213       $status = $this->handleRouteRequirements($pathinfo, $name, $route);
214
215       if (self::ROUTE_MATCH === $status[0]) {
216         return $status[1];
217       }
218
219       if (self::REQUIREMENT_MISMATCH === $status[0]) {
220         $routes->remove($name);
221         continue;
222       }
223
224       return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
225     }
226   }
227
228   /**
229    * Returns a collection of potential matching routes for a request.
230    *
231    * @param \Symfony\Component\HttpFoundation\Request $request
232    *   The current request.
233    *
234    * @return \Symfony\Component\Routing\RouteCollection
235    *   The initial fetched route collection.
236    */
237   protected function getInitialRouteCollection(Request $request) {
238     return $this->routeProvider->getRouteCollectionForRequest($request);
239   }
240
241   /**
242    * Apply the route enhancers to the defaults, according to priorities.
243    *
244    * @param array $defaults
245    *   The defaults coming from the final matched route.
246    * @param \Symfony\Component\HttpFoundation\Request $request
247    *   The request.
248    *
249    * @return array
250    *   The request attributes after applying the enhancers. This might consist
251    *   raw values from the URL but also upcasted values, like entity objects,
252    *   from route enhancers.
253    */
254   protected function applyRouteEnhancers($defaults, Request $request) {
255     foreach ($this->enhancers as $enhancer) {
256       if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
257         continue;
258       }
259       $defaults = $enhancer->enhance($defaults, $request);
260     }
261
262     return $defaults;
263   }
264
265   /**
266    * Applies all route filters to a given route collection.
267    *
268    * This method reduces the sets of routes further down, for example by
269    * checking the HTTP method.
270    *
271    * @param \Symfony\Component\Routing\RouteCollection $collection
272    *   The route collection.
273    * @param \Symfony\Component\HttpFoundation\Request $request
274    *   The request.
275    *
276    * @return \Symfony\Component\Routing\RouteCollection
277    *   The filtered/sorted route collection.
278    */
279   protected function applyRouteFilters(RouteCollection $collection, Request $request) {
280     // Route filters are expected to throw an exception themselves if they
281     // end up filtering the list down to 0.
282     foreach ($this->filters as $filter) {
283       $collection = $filter->filter($collection, $request);
284     }
285
286     return $collection;
287   }
288
289   /**
290    * {@inheritdoc}
291    */
292   public function getRouteCollection() {
293     return new LazyRouteCollection($this->routeProvider);
294   }
295
296   /**
297    * {@inheritdoc}
298    */
299   public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
300     @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
301     return $this->urlGenerator->generate($name, $parameters, $referenceType);
302   }
303
304 }