Pull merge.
[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     $collection = $this->applyFitOrder($collection);
129
130     if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
131       return $this->applyRouteEnhancers($ret, $request);
132     }
133
134     throw 0 < count($this->allow)
135       ? new MethodNotAllowedException(array_unique($this->allow))
136       : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
137   }
138
139   /**
140    * Tries to match a URL with a set of routes.
141    *
142    * @param string $pathinfo
143    *   The path info to be parsed
144    * @param \Symfony\Component\Routing\RouteCollection $routes
145    *   The set of routes.
146    *
147    * @return array|null
148    *   An array of parameters. NULL when there is no match.
149    */
150   protected function matchCollection($pathinfo, RouteCollection $routes) {
151     // Try a case-sensitive match.
152     $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
153     // Try a case-insensitive match.
154     if ($match === NULL && $routes->count() > 0) {
155       $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
156     }
157     return $match;
158   }
159
160   /**
161    * Tries to match a URL with a set of routes.
162    *
163    * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
164    * supports case-insensitive matching. The static prefix optimization is
165    * removed as this duplicates work done by the query in
166    * RouteProvider::getRoutesByPath().
167    *
168    * @param string $pathinfo
169    *   The path info to be parsed
170    * @param \Symfony\Component\Routing\RouteCollection $routes
171    *   The set of routes.
172    * @param bool $case_sensitive
173    *   Determines if the match should be case-sensitive of not.
174    *
175    * @return array|null
176    *   An array of parameters. NULL when there is no match.
177    *
178    * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
179    * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
180    */
181   protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
182     foreach ($routes as $name => $route) {
183       $compiledRoute = $route->compile();
184
185       // Set the regex to use UTF-8.
186       $regex = $compiledRoute->getRegex() . 'u';
187       if (!$case_sensitive) {
188         $regex = $regex . 'i';
189       }
190       if (!preg_match($regex, $pathinfo, $matches)) {
191         continue;
192       }
193
194       $hostMatches = [];
195       if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
196         $routes->remove($name);
197         continue;
198       }
199
200       // Check HTTP method requirement.
201       if ($requiredMethods = $route->getMethods()) {
202         // HEAD and GET are equivalent as per RFC.
203         if ('HEAD' === $method = $this->context->getMethod()) {
204           $method = 'GET';
205         }
206
207         if (!in_array($method, $requiredMethods)) {
208           $this->allow = array_merge($this->allow, $requiredMethods);
209           $routes->remove($name);
210           continue;
211         }
212       }
213
214       $status = $this->handleRouteRequirements($pathinfo, $name, $route);
215
216       if (self::ROUTE_MATCH === $status[0]) {
217         return $status[1];
218       }
219
220       if (self::REQUIREMENT_MISMATCH === $status[0]) {
221         $routes->remove($name);
222         continue;
223       }
224
225       return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
226     }
227   }
228
229   /**
230    * Returns a collection of potential matching routes for a request.
231    *
232    * @param \Symfony\Component\HttpFoundation\Request $request
233    *   The current request.
234    *
235    * @return \Symfony\Component\Routing\RouteCollection
236    *   The initial fetched route collection.
237    */
238   protected function getInitialRouteCollection(Request $request) {
239     return $this->routeProvider->getRouteCollectionForRequest($request);
240   }
241
242   /**
243    * Apply the route enhancers to the defaults, according to priorities.
244    *
245    * @param array $defaults
246    *   The defaults coming from the final matched route.
247    * @param \Symfony\Component\HttpFoundation\Request $request
248    *   The request.
249    *
250    * @return array
251    *   The request attributes after applying the enhancers. This might consist
252    *   raw values from the URL but also upcasted values, like entity objects,
253    *   from route enhancers.
254    */
255   protected function applyRouteEnhancers($defaults, Request $request) {
256     foreach ($this->enhancers as $enhancer) {
257       if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
258         continue;
259       }
260       $defaults = $enhancer->enhance($defaults, $request);
261     }
262
263     return $defaults;
264   }
265
266   /**
267    * Applies all route filters to a given route collection.
268    *
269    * This method reduces the sets of routes further down, for example by
270    * checking the HTTP method.
271    *
272    * @param \Symfony\Component\Routing\RouteCollection $collection
273    *   The route collection.
274    * @param \Symfony\Component\HttpFoundation\Request $request
275    *   The request.
276    *
277    * @return \Symfony\Component\Routing\RouteCollection
278    *   The filtered/sorted route collection.
279    */
280   protected function applyRouteFilters(RouteCollection $collection, Request $request) {
281     // Route filters are expected to throw an exception themselves if they
282     // end up filtering the list down to 0.
283     foreach ($this->filters as $filter) {
284       $collection = $filter->filter($collection, $request);
285     }
286
287     return $collection;
288   }
289
290   /**
291    * Reapplies the fit order to a RouteCollection object.
292    *
293    * Route filters can reorder route collections. For example, routes with an
294    * explicit _format requirement will be preferred. This can result in a less
295    * fit route being used. For example, as a result of filtering /user/% comes
296    * before /user/login. In order to not break this fundamental property of
297    * routes, we need to reapply the fit order. We also need to ensure that order
298    * within each group of the same fit is preserved.
299    *
300    * @param \Symfony\Component\Routing\RouteCollection $collection
301    *   The route collection.
302    *
303    * @return \Symfony\Component\Routing\RouteCollection
304    *   The reordered route collection.
305    */
306   protected function applyFitOrder(RouteCollection $collection) {
307     $buckets = [];
308     // Sort all the routes by fit descending.
309     foreach ($collection->all() as $name => $route) {
310       $fit = $route->compile()->getFit();
311       $buckets += [$fit => []];
312       $buckets[$fit][] = [$name, $route];
313     }
314     krsort($buckets);
315
316     $flattened = array_reduce($buckets, 'array_merge', []);
317
318     // Add them back onto a new route collection.
319     $collection = new RouteCollection();
320     foreach ($flattened as $pair) {
321       $name = $pair[0];
322       $route = $pair[1];
323       $collection->add($name, $route);
324     }
325     return $collection;
326   }
327
328   /**
329    * {@inheritdoc}
330    */
331   public function getRouteCollection() {
332     return new LazyRouteCollection($this->routeProvider);
333   }
334
335   /**
336    * {@inheritdoc}
337    */
338   public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
339     @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
340     return $this->urlGenerator->generate($name, $parameters, $referenceType);
341   }
342
343 }