Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / lib / Drupal / Core / Routing / RouteProvider.php
1 <?php
2
3 namespace Drupal\Core\Routing;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Language\LanguageManagerInterface;
11 use Drupal\Core\Path\CurrentPathStack;
12 use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
13 use Drupal\Core\State\StateInterface;
14 use Symfony\Cmf\Component\Routing\PagedRouteCollection;
15 use Symfony\Cmf\Component\Routing\PagedRouteProviderInterface;
16 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17 use Symfony\Component\HttpFoundation\Request;
18 use Symfony\Component\Routing\Exception\RouteNotFoundException;
19 use Symfony\Component\Routing\RouteCollection;
20 use Drupal\Core\Database\Connection;
21
22 /**
23  * A Route Provider front-end for all Drupal-stored routes.
24  */
25 class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface {
26
27   /**
28    * The database connection from which to read route information.
29    *
30    * @var \Drupal\Core\Database\Connection
31    */
32   protected $connection;
33
34   /**
35    * The name of the SQL table from which to read the routes.
36    *
37    * @var string
38    */
39   protected $tableName;
40
41   /**
42    * The state.
43    *
44    * @var \Drupal\Core\State\StateInterface
45    */
46   protected $state;
47
48   /**
49    * A cache of already-loaded routes, keyed by route name.
50    *
51    * @var \Symfony\Component\Routing\Route[]
52    */
53   protected $routes = [];
54
55   /**
56    * A cache of already-loaded serialized routes, keyed by route name.
57    *
58    * @var string[]
59    */
60   protected $serializedRoutes = [];
61
62   /**
63    * The current path.
64    *
65    * @var \Drupal\Core\Path\CurrentPathStack
66    */
67   protected $currentPath;
68
69   /**
70    * The cache backend.
71    *
72    * @var \Drupal\Core\Cache\CacheBackendInterface
73    */
74   protected $cache;
75
76   /**
77    * The cache tag invalidator.
78    *
79    * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
80    */
81   protected $cacheTagInvalidator;
82
83   /**
84    * A path processor manager for resolving the system path.
85    *
86    * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
87    */
88   protected $pathProcessor;
89
90   /**
91    * The language manager.
92    *
93    * @var \Drupal\Core\Language\LanguageManagerInterface
94    */
95   protected $languageManager;
96
97   /**
98    * Cache ID prefix used to load routes.
99    */
100   const ROUTE_LOAD_CID_PREFIX = 'route_provider.route_load:';
101
102   /**
103    * Constructs a new PathMatcher.
104    *
105    * @param \Drupal\Core\Database\Connection $connection
106    *   A database connection object.
107    * @param \Drupal\Core\State\StateInterface $state
108    *   The state.
109    * @param \Drupal\Core\Path\CurrentPathStack $current_path
110    *   The current path.
111    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
112    *   The cache backend.
113    * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
114    *   The path processor.
115    * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator
116    *   The cache tag invalidator.
117    * @param string $table
118    *   (Optional) The table in the database to use for matching. Defaults to 'router'
119    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
120    *   (Optional) The language manager.
121    */
122   public function __construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $table = 'router', LanguageManagerInterface $language_manager = NULL) {
123     $this->connection = $connection;
124     $this->state = $state;
125     $this->currentPath = $current_path;
126     $this->cache = $cache_backend;
127     $this->cacheTagInvalidator = $cache_tag_invalidator;
128     $this->pathProcessor = $path_processor;
129     $this->tableName = $table;
130     $this->languageManager = $language_manager ?: \Drupal::languageManager();
131   }
132
133   /**
134    * Finds routes that may potentially match the request.
135    *
136    * This may return a mixed list of class instances, but all routes returned
137    * must extend the core symfony route. The classes may also implement
138    * RouteObjectInterface to link to a content document.
139    *
140    * This method may not throw an exception based on implementation specific
141    * restrictions on the url. That case is considered a not found - returning
142    * an empty array. Exceptions are only used to abort the whole request in
143    * case something is seriously broken, like the storage backend being down.
144    *
145    * Note that implementations may not implement an optimal matching
146    * algorithm, simply a reasonable first pass.  That allows for potentially
147    * very large route sets to be filtered down to likely candidates, which
148    * may then be filtered in memory more completely.
149    *
150    * @param \Symfony\Component\HttpFoundation\Request $request
151    *   A request against which to match.
152    *
153    * @return \Symfony\Component\Routing\RouteCollection
154    *   RouteCollection with all urls that could potentially match $request.
155    *   Empty collection if nothing can match. The collection will be sorted from
156    *   highest to lowest fit (match of path parts) and then in ascending order
157    *   by route name for routes with the same fit.
158    */
159   public function getRouteCollectionForRequest(Request $request) {
160     // Cache both the system path as well as route parameters and matching
161     // routes.
162     $cid = $this->getRouteCollectionCacheId($request);
163     if ($cached = $this->cache->get($cid)) {
164       $this->currentPath->setPath($cached->data['path'], $request);
165       $request->query->replace($cached->data['query']);
166       return $cached->data['routes'];
167     }
168     else {
169       // Just trim on the right side.
170       $path = $request->getPathInfo();
171       $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/');
172       $path = $this->pathProcessor->processInbound($path, $request);
173       $this->currentPath->setPath($path, $request);
174       // Incoming path processors may also set query parameters.
175       $query_parameters = $request->query->all();
176       $routes = $this->getRoutesByPath(rtrim($path, '/'));
177       $cache_value = [
178         'path' => $path,
179         'query' => $query_parameters,
180         'routes' => $routes,
181       ];
182       $this->cache->set($cid, $cache_value, CacheBackendInterface::CACHE_PERMANENT, ['route_match']);
183       return $routes;
184     }
185   }
186
187   /**
188    * Find the route using the provided route name (and parameters).
189    *
190    * @param string $name
191    *   The route name to fetch
192    *
193    * @return \Symfony\Component\Routing\Route
194    *   The found route.
195    *
196    * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
197    *   Thrown if there is no route with that name in this repository.
198    */
199   public function getRouteByName($name) {
200     $routes = $this->getRoutesByNames([$name]);
201     if (empty($routes)) {
202       throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name));
203     }
204
205     return reset($routes);
206   }
207
208   /**
209    * {@inheritdoc}
210    */
211   public function preLoadRoutes($names) {
212     if (empty($names)) {
213       throw new \InvalidArgumentException('You must specify the route names to load');
214     }
215
216     $routes_to_load = array_diff($names, array_keys($this->routes), array_keys($this->serializedRoutes));
217     if ($routes_to_load) {
218
219       $cid = static::ROUTE_LOAD_CID_PREFIX . hash('sha512', serialize($routes_to_load));
220       if ($cache = $this->cache->get($cid)) {
221         $routes = $cache->data;
222       }
223       else {
224         try {
225           $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN ( :names[] )', [':names[]' => $routes_to_load]);
226           $routes = $result->fetchAllKeyed();
227
228           $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']);
229         }
230         catch (\Exception $e) {
231           $routes = [];
232         }
233       }
234
235       $this->serializedRoutes += $routes;
236     }
237   }
238
239   /**
240    * {@inheritdoc}
241    */
242   public function getRoutesByNames($names) {
243     $this->preLoadRoutes($names);
244
245     foreach ($names as $name) {
246       // The specified route name might not exist or might be serialized.
247       if (!isset($this->routes[$name]) && isset($this->serializedRoutes[$name])) {
248         $this->routes[$name] = unserialize($this->serializedRoutes[$name]);
249         unset($this->serializedRoutes[$name]);
250       }
251     }
252
253     return array_intersect_key($this->routes, array_flip($names));
254   }
255
256   /**
257    * Returns an array of path pattern outlines that could match the path parts.
258    *
259    * @param array $parts
260    *   The parts of the path for which we want candidates.
261    *
262    * @return array
263    *   An array of outlines that could match the specified path parts.
264    */
265   protected function getCandidateOutlines(array $parts) {
266     $number_parts = count($parts);
267     $ancestors = [];
268     $length = $number_parts - 1;
269     $end = (1 << $number_parts) - 1;
270
271     // The highest possible mask is a 1 bit for every part of the path. We will
272     // check every value down from there to generate a possible outline.
273     if ($number_parts == 1) {
274       $masks = [1];
275     }
276     elseif ($number_parts <= 3 && $number_parts > 0) {
277       // Optimization - don't query the state system for short paths. This also
278       // insulates against the state entry for masks going missing for common
279       // user-facing paths since we generate all values without checking state.
280       $masks = range($end, 1);
281     }
282     elseif ($number_parts <= 0) {
283       // No path can match, short-circuit the process.
284       $masks = [];
285     }
286     else {
287       // Get the actual patterns that exist out of state.
288       $masks = (array) $this->state->get('routing.menu_masks.' . $this->tableName, []);
289     }
290
291     // Only examine patterns that actually exist as router items (the masks).
292     foreach ($masks as $i) {
293       if ($i > $end) {
294         // Only look at masks that are not longer than the path of interest.
295         continue;
296       }
297       elseif ($i < (1 << $length)) {
298         // We have exhausted the masks of a given length, so decrease the length.
299         --$length;
300       }
301       $current = '';
302       for ($j = $length; $j >= 0; $j--) {
303         // Check the bit on the $j offset.
304         if ($i & (1 << $j)) {
305           // Bit one means the original value.
306           $current .= $parts[$length - $j];
307         }
308         else {
309           // Bit zero means means wildcard.
310           $current .= '%';
311         }
312         // Unless we are at offset 0, add a slash.
313         if ($j) {
314           $current .= '/';
315         }
316       }
317       $ancestors[] = '/' . $current;
318     }
319     return $ancestors;
320   }
321
322   /**
323    * {@inheritdoc}
324    */
325   public function getRoutesByPattern($pattern) {
326     $path = RouteCompiler::getPatternOutline($pattern);
327
328     return $this->getRoutesByPath($path);
329   }
330
331   /**
332    * Get all routes which match a certain pattern.
333    *
334    * @param string $path
335    *   The route pattern to search for.
336    *
337    * @return \Symfony\Component\Routing\RouteCollection
338    *   Returns a route collection of matching routes. The collection may be
339    *   empty and will be sorted from highest to lowest fit (match of path parts)
340    *   and then in ascending order by route name for routes with the same fit.
341    */
342   protected function getRoutesByPath($path) {
343     // Split the path up on the slashes, ignoring multiple slashes in a row
344     // or leading or trailing slashes. Convert to lower case here so we can
345     // have a case-insensitive match from the incoming path to the lower case
346     // pattern outlines from \Drupal\Core\Routing\RouteCompiler::compile().
347     // @see \Drupal\Core\Routing\CompiledRoute::__construct()
348     $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY);
349
350     $collection = new RouteCollection();
351
352     $ancestors = $this->getCandidateOutlines($parts);
353     if (empty($ancestors)) {
354       return $collection;
355     }
356
357     // The >= check on number_parts allows us to match routes with optional
358     // trailing wildcard parts as long as the pattern matches, since we
359     // dump the route pattern without those optional parts.
360     try {
361       $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", [
362         ':patterns[]' => $ancestors,
363         ':count_parts' => count($parts),
364       ])
365         ->fetchAll(\PDO::FETCH_ASSOC);
366     }
367     catch (\Exception $e) {
368       $routes = [];
369     }
370
371     // We sort by fit and name in PHP to avoid a SQL filesort and avoid any
372     // difference in the sorting behavior of SQL back-ends.
373     usort($routes, [$this, 'routeProviderRouteCompare']);
374
375     foreach ($routes as $row) {
376       $collection->add($row['name'], unserialize($row['route']));
377     }
378
379     return $collection;
380   }
381
382   /**
383    * Comparison function for usort on routes.
384    */
385   protected function routeProviderRouteCompare(array $a, array $b) {
386     if ($a['fit'] == $b['fit']) {
387       return strcmp($a['name'], $b['name']);
388     }
389     // Reverse sort from highest to lowest fit. PHP should cast to int, but
390     // the explicit cast makes this sort more robust against unexpected input.
391     return (int) $a['fit'] < (int) $b['fit'] ? 1 : -1;
392   }
393
394   /**
395    * {@inheritdoc}
396    */
397   public function getAllRoutes() {
398     return new PagedRouteCollection($this);
399   }
400
401   /**
402    * {@inheritdoc}
403    */
404   public function reset() {
405     $this->routes  = [];
406     $this->serializedRoutes = [];
407     $this->cacheTagInvalidator->invalidateTags(['routes']);
408   }
409
410   /**
411    * {@inheritdoc}
412    */
413   public static function getSubscribedEvents() {
414     $events[RoutingEvents::FINISHED][] = ['reset'];
415     return $events;
416   }
417
418   /**
419    * {@inheritdoc}
420    */
421   public function getRoutesPaged($offset, $length = NULL) {
422     $select = $this->connection->select($this->tableName, 'router')
423       ->fields('router', ['name', 'route']);
424
425     if (isset($length)) {
426       $select->range($offset, $length);
427     }
428
429     $routes = $select->execute()->fetchAllKeyed();
430
431     $result = [];
432     foreach ($routes as $name => $route) {
433       $result[$name] = unserialize($route);
434     }
435
436     return $result;
437   }
438
439   /**
440    * {@inheritdoc}
441    */
442   public function getRoutesCount() {
443     return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField();
444   }
445
446   /**
447    * Returns the cache ID for the route collection cache.
448    *
449    * @param \Symfony\Component\HttpFoundation\Request $request
450    *   The request object.
451    *
452    * @return string
453    *   The cache ID.
454    */
455   protected function getRouteCollectionCacheId(Request $request) {
456     // Include the current language code in the cache identifier as
457     // the language information can be elsewhere than in the path, for example
458     // based on the domain.
459     $language_part = $this->getCurrentLanguageCacheIdPart();
460     return 'route:' . $language_part . ':' . $request->getPathInfo() . ':' . $request->getQueryString();
461   }
462
463   /**
464    * Returns the language identifier for the route collection cache.
465    *
466    * @return string
467    *   The language identifier.
468    */
469   protected function getCurrentLanguageCacheIdPart() {
470     // This must be in sync with the language logic in
471     // \Drupal\Core\PathProcessor\PathProcessorAlias::processInbound() and
472     // \Drupal\Core\Path\AliasManager::getPathByAlias().
473     // @todo Update this if necessary in https://www.drupal.org/node/1125428.
474     return $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
475   }
476
477 }