annotate core/lib/Drupal/Core/Routing/Router.php @ 13:5fb285c0d0e3

Update Drupal core to 8.4.7 via Composer. Security update; I *think* we've been lucky to get away with this so far, as we don't support self-registration which seems to be used by the so-called "drupalgeddon 2" attack that 8.4.5 was vulnerable to.
author Chris Cannam
date Mon, 23 Apr 2018 09:33:26 +0100
parents 4c8ae668cc8c
children 1fec387a4317
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Routing;
Chris@0 4
Chris@0 5 use Drupal\Core\Path\CurrentPathStack;
Chris@0 6 use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface as BaseRouteEnhancerInterface;
Chris@0 7 use Symfony\Cmf\Component\Routing\LazyRouteCollection;
Chris@0 8 use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface as BaseRouteFilterInterface;
Chris@0 9 use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
Chris@0 10 use Symfony\Component\HttpFoundation\Request;
Chris@0 11 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
Chris@0 12 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
Chris@0 13 use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
Chris@0 14 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
Chris@0 15 use Symfony\Component\Routing\RouteCollection;
Chris@0 16 use Symfony\Component\Routing\RouterInterface;
Chris@0 17
Chris@0 18 /**
Chris@0 19 * Router implementation in Drupal.
Chris@0 20 *
Chris@0 21 * A router determines, for an incoming request, the active controller, which is
Chris@0 22 * a callable that creates a response.
Chris@0 23 *
Chris@0 24 * It consists of several steps, of which each are explained in more details
Chris@0 25 * below:
Chris@0 26 * 1. Get a collection of routes which potentially match the current request.
Chris@0 27 * This is done by the route provider. See ::getInitialRouteCollection().
Chris@0 28 * 2. Filter the collection down further more. For example this filters out
Chris@0 29 * routes applying to other formats: See ::applyRouteFilters()
Chris@0 30 * 3. Find the best matching route out of the remaining ones, by applying a
Chris@0 31 * regex. See ::matchCollection().
Chris@0 32 * 4. Enhance the list of route attributes, for example loading entity objects.
Chris@0 33 * See ::applyRouteEnhancers().
Chris@0 34 *
Chris@0 35 * This implementation uses ideas of the following routers:
Chris@0 36 * - \Symfony\Cmf\Component\Routing\DynamicRouter
Chris@0 37 * - \Drupal\Core\Routing\UrlMatcher
Chris@0 38 * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
Chris@0 39 *
Chris@0 40 * @see \Symfony\Cmf\Component\Routing\DynamicRouter
Chris@0 41 * @see \Drupal\Core\Routing\UrlMatcher
Chris@0 42 * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
Chris@0 43 */
Chris@0 44 class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
Chris@0 45
Chris@0 46 /**
Chris@0 47 * The route provider responsible for the first-pass match.
Chris@0 48 *
Chris@0 49 * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
Chris@0 50 */
Chris@0 51 protected $routeProvider;
Chris@0 52
Chris@0 53 /**
Chris@0 54 * The list of available enhancers.
Chris@0 55 *
Chris@0 56 * @var \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
Chris@0 57 */
Chris@0 58 protected $enhancers = [];
Chris@0 59
Chris@0 60 /**
Chris@0 61 * Cached sorted list of enhancers.
Chris@0 62 *
Chris@0 63 * @var \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
Chris@0 64 */
Chris@0 65 protected $sortedEnhancers;
Chris@0 66
Chris@0 67 /**
Chris@0 68 * The list of available route filters.
Chris@0 69 *
Chris@0 70 * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
Chris@0 71 */
Chris@0 72 protected $filters = [];
Chris@0 73
Chris@0 74 /**
Chris@0 75 * Cached sorted list route filters.
Chris@0 76 *
Chris@0 77 * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
Chris@0 78 */
Chris@0 79 protected $sortedFilters;
Chris@0 80
Chris@0 81 /**
Chris@0 82 * The URL generator.
Chris@0 83 *
Chris@0 84 * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
Chris@0 85 */
Chris@0 86 protected $urlGenerator;
Chris@0 87
Chris@0 88 /**
Chris@0 89 * Constructs a new Router.
Chris@0 90 *
Chris@0 91 * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
Chris@0 92 * The route provider.
Chris@0 93 * @param \Drupal\Core\Path\CurrentPathStack $current_path
Chris@0 94 * The current path stack.
Chris@0 95 * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
Chris@0 96 * The URL generator.
Chris@0 97 */
Chris@0 98 public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
Chris@0 99 parent::__construct($current_path);
Chris@0 100 $this->routeProvider = $route_provider;
Chris@0 101 $this->urlGenerator = $url_generator;
Chris@0 102 }
Chris@0 103
Chris@0 104 /**
Chris@0 105 * Adds a route enhancer to the list of used route enhancers.
Chris@0 106 *
Chris@0 107 * @param \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface $route_enhancer
Chris@0 108 * A route enhancer.
Chris@0 109 * @param int $priority
Chris@0 110 * (optional) The priority of the enhancer. Higher number enhancers will be
Chris@0 111 * used first.
Chris@0 112 *
Chris@0 113 * @return $this
Chris@0 114 */
Chris@0 115 public function addRouteEnhancer(BaseRouteEnhancerInterface $route_enhancer, $priority = 0) {
Chris@0 116 $this->enhancers[$priority][] = $route_enhancer;
Chris@0 117 return $this;
Chris@0 118 }
Chris@0 119
Chris@0 120 /**
Chris@0 121 * Adds a route filter to the list of used route filters.
Chris@0 122 *
Chris@0 123 * @param \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface $route_filter
Chris@0 124 * A route filter.
Chris@0 125 * @param int $priority
Chris@0 126 * (optional) The priority of the filter. Higher number filters will be used
Chris@0 127 * first.
Chris@0 128 *
Chris@0 129 * @return $this
Chris@0 130 */
Chris@0 131 public function addRouteFilter(BaseRouteFilterInterface $route_filter, $priority = 0) {
Chris@0 132 $this->filters[$priority][] = $route_filter;
Chris@0 133
Chris@0 134 return $this;
Chris@0 135 }
Chris@0 136
Chris@0 137 /**
Chris@0 138 * {@inheritdoc}
Chris@0 139 */
Chris@0 140 public function match($pathinfo) {
Chris@0 141 $request = Request::create($pathinfo);
Chris@0 142
Chris@0 143 return $this->matchRequest($request);
Chris@0 144 }
Chris@0 145
Chris@0 146 /**
Chris@0 147 * {@inheritdoc}
Chris@0 148 */
Chris@0 149 public function matchRequest(Request $request) {
Chris@0 150 $collection = $this->getInitialRouteCollection($request);
Chris@0 151 $collection = $this->applyRouteFilters($collection, $request);
Chris@0 152
Chris@0 153 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
Chris@0 154 return $this->applyRouteEnhancers($ret, $request);
Chris@0 155 }
Chris@0 156
Chris@0 157 throw 0 < count($this->allow)
Chris@0 158 ? new MethodNotAllowedException(array_unique($this->allow))
Chris@0 159 : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
Chris@0 160 }
Chris@0 161
Chris@0 162 /**
Chris@0 163 * Tries to match a URL with a set of routes.
Chris@0 164 *
Chris@0 165 * @param string $pathinfo
Chris@0 166 * The path info to be parsed
Chris@0 167 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 168 * The set of routes.
Chris@0 169 *
Chris@0 170 * @return array|null
Chris@0 171 * An array of parameters. NULL when there is no match.
Chris@0 172 */
Chris@0 173 protected function matchCollection($pathinfo, RouteCollection $routes) {
Chris@0 174 // Try a case-sensitive match.
Chris@0 175 $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
Chris@0 176 // Try a case-insensitive match.
Chris@0 177 if ($match === NULL && $routes->count() > 0) {
Chris@0 178 $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
Chris@0 179 }
Chris@0 180 return $match;
Chris@0 181 }
Chris@0 182
Chris@0 183 /**
Chris@0 184 * Tries to match a URL with a set of routes.
Chris@0 185 *
Chris@0 186 * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
Chris@0 187 * supports case-insensitive matching. The static prefix optimization is
Chris@0 188 * removed as this duplicates work done by the query in
Chris@0 189 * RouteProvider::getRoutesByPath().
Chris@0 190 *
Chris@0 191 * @param string $pathinfo
Chris@0 192 * The path info to be parsed
Chris@0 193 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 194 * The set of routes.
Chris@0 195 * @param bool $case_sensitive
Chris@0 196 * Determines if the match should be case-sensitive of not.
Chris@0 197 *
Chris@0 198 * @return array|null
Chris@0 199 * An array of parameters. NULL when there is no match.
Chris@0 200 *
Chris@0 201 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
Chris@0 202 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
Chris@0 203 */
Chris@0 204 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
Chris@0 205 foreach ($routes as $name => $route) {
Chris@0 206 $compiledRoute = $route->compile();
Chris@0 207
Chris@0 208 // Set the regex to use UTF-8.
Chris@0 209 $regex = $compiledRoute->getRegex() . 'u';
Chris@0 210 if (!$case_sensitive) {
Chris@0 211 $regex = $regex . 'i';
Chris@0 212 }
Chris@0 213 if (!preg_match($regex, $pathinfo, $matches)) {
Chris@0 214 continue;
Chris@0 215 }
Chris@0 216
Chris@0 217 $hostMatches = [];
Chris@0 218 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
Chris@0 219 $routes->remove($name);
Chris@0 220 continue;
Chris@0 221 }
Chris@0 222
Chris@0 223 // Check HTTP method requirement.
Chris@0 224 if ($requiredMethods = $route->getMethods()) {
Chris@0 225 // HEAD and GET are equivalent as per RFC.
Chris@0 226 if ('HEAD' === $method = $this->context->getMethod()) {
Chris@0 227 $method = 'GET';
Chris@0 228 }
Chris@0 229
Chris@0 230 if (!in_array($method, $requiredMethods)) {
Chris@0 231 $this->allow = array_merge($this->allow, $requiredMethods);
Chris@0 232 $routes->remove($name);
Chris@0 233 continue;
Chris@0 234 }
Chris@0 235 }
Chris@0 236
Chris@0 237 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
Chris@0 238
Chris@0 239 if (self::ROUTE_MATCH === $status[0]) {
Chris@0 240 return $status[1];
Chris@0 241 }
Chris@0 242
Chris@0 243 if (self::REQUIREMENT_MISMATCH === $status[0]) {
Chris@0 244 $routes->remove($name);
Chris@0 245 continue;
Chris@0 246 }
Chris@0 247
Chris@0 248 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
Chris@0 249 }
Chris@0 250 }
Chris@0 251
Chris@0 252 /**
Chris@0 253 * Returns a collection of potential matching routes for a request.
Chris@0 254 *
Chris@0 255 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 256 * The current request.
Chris@0 257 *
Chris@0 258 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 259 * The initial fetched route collection.
Chris@0 260 */
Chris@0 261 protected function getInitialRouteCollection(Request $request) {
Chris@0 262 return $this->routeProvider->getRouteCollectionForRequest($request);
Chris@0 263 }
Chris@0 264
Chris@0 265 /**
Chris@0 266 * Apply the route enhancers to the defaults, according to priorities.
Chris@0 267 *
Chris@0 268 * @param array $defaults
Chris@0 269 * The defaults coming from the final matched route.
Chris@0 270 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 271 * The request.
Chris@0 272 *
Chris@0 273 * @return array
Chris@0 274 * The request attributes after applying the enhancers. This might consist
Chris@0 275 * raw values from the URL but also upcasted values, like entity objects,
Chris@0 276 * from route enhancers.
Chris@0 277 */
Chris@0 278 protected function applyRouteEnhancers($defaults, Request $request) {
Chris@0 279 foreach ($this->getRouteEnhancers() as $enhancer) {
Chris@0 280 $defaults = $enhancer->enhance($defaults, $request);
Chris@0 281 }
Chris@0 282
Chris@0 283 return $defaults;
Chris@0 284 }
Chris@0 285
Chris@0 286 /**
Chris@0 287 * Sorts the enhancers and flattens them.
Chris@0 288 *
Chris@0 289 * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
Chris@0 290 * The enhancers ordered by priority.
Chris@0 291 */
Chris@0 292 public function getRouteEnhancers() {
Chris@0 293 if (!isset($this->sortedEnhancers)) {
Chris@0 294 $this->sortedEnhancers = $this->sortRouteEnhancers();
Chris@0 295 }
Chris@0 296
Chris@0 297 return $this->sortedEnhancers;
Chris@0 298 }
Chris@0 299
Chris@0 300 /**
Chris@0 301 * Sort enhancers by priority.
Chris@0 302 *
Chris@0 303 * The highest priority number is the highest priority (reverse sorting).
Chris@0 304 *
Chris@0 305 * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
Chris@0 306 * The sorted enhancers.
Chris@0 307 */
Chris@0 308 protected function sortRouteEnhancers() {
Chris@0 309 $sortedEnhancers = [];
Chris@0 310 krsort($this->enhancers);
Chris@0 311
Chris@0 312 foreach ($this->enhancers as $enhancers) {
Chris@0 313 $sortedEnhancers = array_merge($sortedEnhancers, $enhancers);
Chris@0 314 }
Chris@0 315
Chris@0 316 return $sortedEnhancers;
Chris@0 317 }
Chris@0 318
Chris@0 319 /**
Chris@0 320 * Applies all route filters to a given route collection.
Chris@0 321 *
Chris@0 322 * This method reduces the sets of routes further down, for example by
Chris@0 323 * checking the HTTP method.
Chris@0 324 *
Chris@0 325 * @param \Symfony\Component\Routing\RouteCollection $collection
Chris@0 326 * The route collection.
Chris@0 327 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 328 * The request.
Chris@0 329 *
Chris@0 330 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 331 * The filtered/sorted route collection.
Chris@0 332 */
Chris@0 333 protected function applyRouteFilters(RouteCollection $collection, Request $request) {
Chris@0 334 // Route filters are expected to throw an exception themselves if they
Chris@0 335 // end up filtering the list down to 0.
Chris@0 336 foreach ($this->getRouteFilters() as $filter) {
Chris@0 337 $collection = $filter->filter($collection, $request);
Chris@0 338 }
Chris@0 339
Chris@0 340 return $collection;
Chris@0 341 }
Chris@0 342
Chris@0 343 /**
Chris@0 344 * Sorts the filters and flattens them.
Chris@0 345 *
Chris@0 346 * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
Chris@0 347 * The filters ordered by priority
Chris@0 348 */
Chris@0 349 public function getRouteFilters() {
Chris@0 350 if (!isset($this->sortedFilters)) {
Chris@0 351 $this->sortedFilters = $this->sortFilters();
Chris@0 352 }
Chris@0 353
Chris@0 354 return $this->sortedFilters;
Chris@0 355 }
Chris@0 356
Chris@0 357 /**
Chris@0 358 * Sort filters by priority.
Chris@0 359 *
Chris@0 360 * The highest priority number is the highest priority (reverse sorting).
Chris@0 361 *
Chris@0 362 * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
Chris@0 363 * The sorted filters.
Chris@0 364 */
Chris@0 365 protected function sortFilters() {
Chris@0 366 $sortedFilters = [];
Chris@0 367 krsort($this->filters);
Chris@0 368
Chris@0 369 foreach ($this->filters as $filters) {
Chris@0 370 $sortedFilters = array_merge($sortedFilters, $filters);
Chris@0 371 }
Chris@0 372
Chris@0 373 return $sortedFilters;
Chris@0 374 }
Chris@0 375
Chris@0 376 /**
Chris@0 377 * {@inheritdoc}
Chris@0 378 */
Chris@0 379 public function getRouteCollection() {
Chris@0 380 return new LazyRouteCollection($this->routeProvider);
Chris@0 381 }
Chris@0 382
Chris@0 383 /**
Chris@0 384 * {@inheritdoc}
Chris@0 385 */
Chris@0 386 public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
Chris@0 387 @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
Chris@0 388 return $this->urlGenerator->generate($name, $parameters, $referenceType);
Chris@0 389 }
Chris@0 390
Chris@0 391 }