1<?php
2
3namespace Drupal\Core;
4
5use Drupal\Component\Utility\NestedArray;
6use Drupal\Component\Utility\UrlHelper;
7use Drupal\Core\Access\AccessResult;
8use Drupal\Core\DependencyInjection\DependencySerializationTrait;
9use Drupal\Core\Security\TrustedCallbackInterface;
10use Drupal\Core\Routing\RouteMatchInterface;
11use Drupal\Core\Routing\UrlGeneratorInterface;
12use Drupal\Core\Session\AccountInterface;
13use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
14use Drupal\Core\Routing\RouteObjectInterface;
15use Symfony\Component\HttpFoundation\Request;
16
17/**
18 * Defines an object that holds information about a URL.
19 *
20 * In most cases, these should be created with the following methods:
21 * - \Drupal\Core\Url::fromRoute()
22 * - \Drupal\Core\Url::fromRouteMatch()
23 * - \Drupal\Core\Url::fromUri()
24 * - \Drupal\Core\Url::fromUserInput()
25 *
26 * @see \Drupal\Core\Entity\EntityBase::toUrl()
27 */
28class Url implements TrustedCallbackInterface {
29  use DependencySerializationTrait;
30
31  /**
32   * The URL generator.
33   *
34   * @var \Drupal\Core\Routing\UrlGeneratorInterface
35   */
36  protected $urlGenerator;
37
38  /**
39   * The unrouted URL assembler.
40   *
41   * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
42   */
43  protected $urlAssembler;
44
45  /**
46   * The access manager.
47   *
48   * @var \Drupal\Core\Access\AccessManagerInterface
49   */
50  protected $accessManager;
51
52  /**
53   * The route name.
54   *
55   * @var string
56   */
57  protected $routeName;
58
59  /**
60   * The route parameters.
61   *
62   * @var array
63   */
64  protected $routeParameters = [];
65
66  /**
67   * The URL options.
68   *
69   * See \Drupal\Core\Url::fromUri() for details on the options.
70   *
71   * @var array
72   */
73  protected $options = [];
74
75  /**
76   * Indicates whether this object contains an external URL.
77   *
78   * @var bool
79   */
80  protected $external = FALSE;
81
82  /**
83   * Indicates whether this URL is for a URI without a Drupal route.
84   *
85   * @var bool
86   */
87  protected $unrouted = FALSE;
88
89  /**
90   * The non-route URI.
91   *
92   * Only used if self::$unrouted is TRUE.
93   *
94   * @var string
95   */
96  protected $uri;
97
98  /**
99   * Stores the internal path, if already requested by getInternalPath().
100   *
101   * @var string
102   */
103  protected $internalPath;
104
105  /**
106   * Constructs a new Url object.
107   *
108   * In most cases, use Url::fromRoute() or Url::fromUri() rather than
109   * constructing Url objects directly in order to avoid ambiguity and make your
110   * code more self-documenting.
111   *
112   * @param string $route_name
113   *   The name of the route
114   * @param array $route_parameters
115   *   (optional) An associative array of parameter names and values.
116   * @param array $options
117   *   See \Drupal\Core\Url::fromUri() for details.
118   *
119   * @see static::fromRoute()
120   * @see static::fromUri()
121   *
122   * @todo Update this documentation for non-routed URIs in
123   *   https://www.drupal.org/node/2346787
124   */
125  public function __construct($route_name, $route_parameters = [], $options = []) {
126    $this->routeName = $route_name;
127    $this->routeParameters = $route_parameters;
128    $this->options = $options;
129  }
130
131  /**
132   * Creates a new Url object for a URL that has a Drupal route.
133   *
134   * This method is for URLs that have Drupal routes (that is, most pages
135   * generated by Drupal). For non-routed local URIs relative to the base
136   * path (like robots.txt) use Url::fromUri() with the base: scheme.
137   *
138   * @param string $route_name
139   *   The name of the route
140   * @param array $route_parameters
141   *   (optional) An associative array of route parameter names and values.
142   * @param array $options
143   *   See \Drupal\Core\Url::fromUri() for details.
144   *
145   * @return static
146   *   A new Url object for a routed (internal to Drupal) URL.
147   *
148   * @see \Drupal\Core\Url::fromUserInput()
149   * @see \Drupal\Core\Url::fromUri()
150   */
151  public static function fromRoute($route_name, $route_parameters = [], $options = []) {
152    return new static($route_name, $route_parameters, $options);
153  }
154
155  /**
156   * Creates a new URL object from a route match.
157   *
158   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
159   *   The route match.
160   *
161   * @return static
162   */
163  public static function fromRouteMatch(RouteMatchInterface $route_match) {
164    if ($route_match->getRouteObject()) {
165      return new static($route_match->getRouteName(), $route_match->getRawParameters()->all());
166    }
167    else {
168      throw new \InvalidArgumentException('Route required');
169    }
170  }
171
172  /**
173   * Creates a Url object for a relative URI reference submitted by user input.
174   *
175   * Use this method to create a URL for user-entered paths that may or may not
176   * correspond to a valid Drupal route.
177   *
178   * @param string $user_input
179   *   User input for a link or path. The first character must be one of the
180   *   following characters:
181   *   - '/': A path within the current site. This path might be to a Drupal
182   *     route (e.g., '/admin'), to a file (e.g., '/README.txt'), or to
183   *     something processed by a non-Drupal script (e.g.,
184   *     '/not/a/drupal/page'). If the path matches a Drupal route, then the
185   *     URL generation will include Drupal's path processors (e.g.,
186   *     language-prefixing and aliasing). Otherwise, the URL generation will
187   *     just append the passed-in path to Drupal's base path.
188   *   - '?': A query string for the current page or resource.
189   *   - '#': A fragment (jump-link) on the current page or resource.
190   *   This helps reduce ambiguity for user-entered links and paths, and
191   *   supports user interfaces where users may normally use auto-completion
192   *   to search for existing resources, but also may type one of these
193   *   characters to link to (e.g.) a specific path on the site.
194   *   (With regard to the URI specification, the user input is treated as a
195   *   @link https://tools.ietf.org/html/rfc3986#section-4.2 relative URI reference @endlink
196   *   where the relative part is of type
197   *   @link https://tools.ietf.org/html/rfc3986#section-3.3 path-abempty @endlink.)
198   * @param array $options
199   *   (optional) An array of options. See Url::fromUri() for details.
200   *
201   * @return static
202   *   A new Url object based on user input.
203   *
204   * @throws \InvalidArgumentException
205   *   Thrown when the user input does not begin with one of the following
206   *   characters: '/', '?', or '#'.
207   */
208  public static function fromUserInput($user_input, $options = []) {
209    // Ensuring one of these initial characters also enforces that what is
210    // passed is a relative URI reference rather than an absolute URI,
211    // because these are URI reserved characters that a scheme name may not
212    // start with.
213    if ((strpos($user_input, '/') !== 0) && (strpos($user_input, '#') !== 0) && (strpos($user_input, '?') !== 0)) {
214      throw new \InvalidArgumentException("The user-entered string '$user_input' must begin with a '/', '?', or '#'.");
215    }
216
217    // fromUri() requires an absolute URI, so prepend the appropriate scheme
218    // name.
219    return static::fromUri('internal:' . $user_input, $options);
220  }
221
222  /**
223   * Creates a new Url object from a URI.
224   *
225   * This method is for generating URLs for URIs that:
226   * - do not have Drupal routes: both external URLs and unrouted local URIs
227   *   like base:robots.txt
228   * - do have a Drupal route but have a custom scheme to simplify linking.
229   *   Currently, there is only the entity: scheme (This allows URIs of the
230   *   form entity:{entity_type}/{entity_id}. For example: entity:node/1
231   *   resolves to the entity.node.canonical route with a node parameter of 1.)
232   *
233   * For URLs that have Drupal routes (that is, most pages generated by Drupal),
234   * use Url::fromRoute().
235   *
236   * @param string $uri
237   *   The URI of the resource including the scheme. For user input that may
238   *   correspond to a Drupal route, use internal: for the scheme. For paths
239   *   that are known not to be handled by the Drupal routing system (such as
240   *   static files), use base: for the scheme to get a link relative to the
241   *   Drupal base path (like the <base> HTML element). For a link to an entity
242   *   you may use entity:{entity_type}/{entity_id} URIs. The internal: scheme
243   *   should be avoided except when processing actual user input that may or
244   *   may not correspond to a Drupal route. Normally use Url::fromRoute() for
245   *   code linking to any any Drupal page.
246   * @param array $options
247   *   (optional) An associative array of additional URL options, with the
248   *   following elements:
249   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
250   *     to append to the URL.
251   *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
252   *     Do not include the leading '#' character.
253   *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
254   *     absolute link (beginning with http:). Useful for links that will be
255   *     displayed outside the site, such as in an RSS feed.
256   *   - 'attributes': An associative array of HTML attributes that will be
257   *     added to the anchor tag if you use the \Drupal\Core\Link class to make
258   *     the link.
259   *   - 'language': An optional language object used to look up the alias
260   *     for the URL. If $options['language'] is omitted, it defaults to the
261   *     current language for the language type LanguageInterface::TYPE_URL.
262   *   - 'https': Whether this URL should point to a secure location. If not
263   *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
264   *     respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
265   *
266   * @return static
267   *   A new Url object with properties depending on the URI scheme. Call the
268   *   access() method on this to do access checking.
269   *
270   * @throws \InvalidArgumentException
271   *   Thrown when the passed in path has no scheme.
272   *
273   * @see \Drupal\Core\Url::fromRoute()
274   * @see \Drupal\Core\Url::fromUserInput()
275   */
276  public static function fromUri($uri, $options = []) {
277    // parse_url() incorrectly parses base:number/... as hostname:port/...
278    // and not the scheme. Prevent that by prefixing the path with a slash.
279    if (preg_match('/^base:\d/', $uri)) {
280      $uri = str_replace('base:', 'base:/', $uri);
281    }
282    $uri_parts = parse_url($uri);
283    if ($uri_parts === FALSE) {
284      throw new \InvalidArgumentException("The URI '$uri' is malformed.");
285    }
286    // We support protocol-relative URLs.
287    if (strpos($uri, '//') === 0) {
288      $uri_parts['scheme'] = '';
289    }
290    elseif (empty($uri_parts['scheme'])) {
291      throw new \InvalidArgumentException("The URI '$uri' is invalid. You must use a valid URI scheme.");
292    }
293    $uri_parts += ['path' => ''];
294    // Discard empty fragment in $options for consistency with parse_url().
295    if (isset($options['fragment']) && strlen($options['fragment']) == 0) {
296      unset($options['fragment']);
297    }
298    // Extract query parameters and fragment and merge them into $uri_options,
299    // but preserve the original $options for the fallback case.
300    $uri_options = $options;
301    if (isset($uri_parts['fragment']) && $uri_parts['fragment'] !== '') {
302      $uri_options += ['fragment' => $uri_parts['fragment']];
303      unset($uri_parts['fragment']);
304    }
305
306    if (!empty($uri_parts['query'])) {
307      $uri_query = [];
308      parse_str($uri_parts['query'], $uri_query);
309      $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query;
310      unset($uri_parts['query']);
311    }
312
313    if ($uri_parts['scheme'] === 'entity') {
314      $url = static::fromEntityUri($uri_parts, $uri_options, $uri);
315    }
316    elseif ($uri_parts['scheme'] === 'internal') {
317      $url = static::fromInternalUri($uri_parts, $uri_options);
318    }
319    elseif ($uri_parts['scheme'] === 'route') {
320      $url = static::fromRouteUri($uri_parts, $uri_options, $uri);
321    }
322    else {
323      $url = new static($uri, [], $options);
324      if ($uri_parts['scheme'] !== 'base') {
325        $url->external = TRUE;
326        $url->setOption('external', TRUE);
327      }
328      $url->setUnrouted();
329    }
330
331    return $url;
332  }
333
334  /**
335   * Create a new Url object for entity URIs.
336   *
337   * @param array $uri_parts
338   *   Parts from a URI of the form entity:{entity_type}/{entity_id} as from
339   *   parse_url().
340   * @param array $options
341   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
342   * @param string $uri
343   *   The original entered URI.
344   *
345   * @return static
346   *   A new Url object for an entity's canonical route.
347   *
348   * @throws \InvalidArgumentException
349   *   Thrown if the entity URI is invalid.
350   */
351  protected static function fromEntityUri(array $uri_parts, array $options, $uri) {
352    list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2);
353    if ($uri_parts['scheme'] != 'entity' || $entity_id === '') {
354      throw new \InvalidArgumentException("The entity URI '$uri' is invalid. You must specify the entity id in the URL. e.g., entity:node/1 for loading the canonical path to node entity with id 1.");
355    }
356
357    return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options);
358  }
359
360  /**
361   * Creates a new Url object for 'internal:' URIs.
362   *
363   * Important note: the URI minus the scheme can NOT simply be validated by a
364   * \Drupal\Core\Path\PathValidatorInterface implementation. The semantics of
365   * the 'internal:' URI scheme are different:
366   * - PathValidatorInterface accepts paths without a leading slash (e.g.
367   *   'node/add') as well as 2 special paths: '<front>' and '<none>', which are
368   *   mapped to the correspondingly named routes.
369   * - 'internal:' URIs store paths with a leading slash that represents the
370   *   root — i.e. the front page — (e.g. 'internal:/node/add'), and doesn't
371   *   have any exceptions.
372   *
373   * To clarify, a few examples of path plus corresponding 'internal:' URI:
374   * - 'node/add' -> 'internal:/node/add'
375   * - 'node/add?foo=bar' -> 'internal:/node/add?foo=bar'
376   * - 'node/add#kitten' -> 'internal:/node/add#kitten'
377   * - '<front>' -> 'internal:/'
378   * - '<front>foo=bar' -> 'internal:/?foo=bar'
379   * - '<front>#kitten' -> 'internal:/#kitten'
380   * - '<none>' -> 'internal:'
381   * - '<none>foo=bar' -> 'internal:?foo=bar'
382   * - '<none>#kitten' -> 'internal:#kitten'
383   *
384   * Therefore, when using a PathValidatorInterface to validate 'internal:'
385   * URIs, we must map:
386   * - 'internal:' (path component is '')  to the special '<none>' path
387   * - 'internal:/' (path component is '/') to the special '<front>' path
388   * - 'internal:/some-path' (path component is '/some-path') to 'some-path'
389   *
390   * @param array $uri_parts
391   *   Parts from a URI of the form internal:{path} as from parse_url().
392   * @param array $options
393   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
394   *
395   * @return static
396   *   A new Url object for an 'internal:' URI.
397   *
398   * @throws \InvalidArgumentException
399   *   Thrown when the URI's path component doesn't have a leading slash.
400   */
401  protected static function fromInternalUri(array $uri_parts, array $options) {
402    // Both PathValidator::getUrlIfValidWithoutAccessCheck() and 'base:' URIs
403    // only accept/contain paths without a leading slash, unlike 'internal:'
404    // URIs, for which the leading slash means "relative to Drupal root" and
405    // "relative to Symfony app root" (just like in Symfony/Drupal 8 routes).
406    if (empty($uri_parts['path'])) {
407      $uri_parts['path'] = '<none>';
408    }
409    elseif ($uri_parts['path'] === '/') {
410      $uri_parts['path'] = '<front>';
411    }
412    else {
413      if ($uri_parts['path'][0] !== '/') {
414        throw new \InvalidArgumentException("The internal path component '{$uri_parts['path']}' is invalid. Its path component must have a leading slash, e.g. internal:/foo.");
415      }
416      // Remove the leading slash.
417      $uri_parts['path'] = substr($uri_parts['path'], 1);
418
419      if (UrlHelper::isExternal($uri_parts['path'])) {
420        throw new \InvalidArgumentException("The internal path component '{$uri_parts['path']}' is external. You are not allowed to specify an external URL together with internal:/.");
421      }
422    }
423
424    $url = \Drupal::pathValidator()
425      ->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options);
426    // Allow specifying additional options.
427    $url->setOptions($options + $url->getOptions());
428
429    return $url;
430  }
431
432  /**
433   * Creates a new Url object for 'route:' URIs.
434   *
435   * @param array $uri_parts
436   *   Parts from a URI of the form route:{route_name};{route_parameters} as
437   *   from parse_url(), where the path is the route name optionally followed by
438   *   a ";" followed by route parameters in key=value format with & separators.
439   * @param array $options
440   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
441   * @param string $uri
442   *   The original passed in URI.
443   *
444   * @return static
445   *   A new Url object for a 'route:' URI.
446   *
447   * @throws \InvalidArgumentException
448   *   Thrown when the route URI does not have a route name.
449   */
450  protected static function fromRouteUri(array $uri_parts, array $options, $uri) {
451    $route_parts = explode(';', $uri_parts['path'], 2);
452    $route_name = $route_parts[0];
453    if ($route_name === '') {
454      throw new \InvalidArgumentException("The route URI '$uri' is invalid. You must have a route name in the URI. e.g., route:system.admin");
455    }
456    $route_parameters = [];
457    if (!empty($route_parts[1])) {
458      parse_str($route_parts[1], $route_parameters);
459    }
460
461    return new static($route_name, $route_parameters, $options);
462  }
463
464  /**
465   * Returns the Url object matching a request.
466   *
467   * SECURITY NOTE: The request path is not checked to be valid and accessible
468   * by the current user to allow storing and reusing Url objects by different
469   * users. The 'path.validator' service getUrlIfValid() method should be used
470   * instead of this one if validation and access check is desired. Otherwise,
471   * 'access_manager' service checkNamedRoute() method should be used on the
472   * router name and parameters stored in the Url object returned by this
473   * method.
474   *
475   * @param \Symfony\Component\HttpFoundation\Request $request
476   *   A request object.
477   *
478   * @return static
479   *   A Url object. Warning: the object is created even if the current user
480   *   would get an access denied running the same request via the normal page
481   *   flow.
482   *
483   * @throws \Drupal\Core\Routing\MatchingRouteNotFoundException
484   *   Thrown when the request cannot be matched.
485   */
486  public static function createFromRequest(Request $request) {
487    // We use the router without access checks because URL objects might be
488    // created and stored for different users.
489    $result = \Drupal::service('router.no_access_checks')->matchRequest($request);
490    $route_name = $result[RouteObjectInterface::ROUTE_NAME];
491    $route_parameters = $result['_raw_variables']->all();
492    return new static($route_name, $route_parameters);
493  }
494
495  /**
496   * Sets this Url to encapsulate an unrouted URI.
497   *
498   * @return $this
499   */
500  protected function setUnrouted() {
501    $this->unrouted = TRUE;
502    // What was passed in as the route name is actually the URI.
503    // @todo Consider fixing this in https://www.drupal.org/node/2346787.
504    $this->uri = $this->routeName;
505    // Set empty route name and parameters.
506    $this->routeName = NULL;
507    $this->routeParameters = [];
508    return $this;
509  }
510
511  /**
512   * Generates a URI string that represents the data in the Url object.
513   *
514   * The URI will typically have the scheme of route: even if the object was
515   * constructed using an entity: or internal: scheme. An internal: URI that
516   * does not match a Drupal route with be returned here with the base: scheme,
517   * and external URLs will be returned in their original form.
518   *
519   * @return string
520   *   A URI representation of the Url object data.
521   */
522  public function toUriString() {
523    if ($this->isRouted()) {
524      $uri = 'route:' . $this->routeName;
525      if ($this->routeParameters) {
526        $uri .= ';' . UrlHelper::buildQuery($this->routeParameters);
527      }
528    }
529    else {
530      $uri = $this->uri;
531    }
532    $query = !empty($this->options['query']) ? ('?' . UrlHelper::buildQuery($this->options['query'])) : '';
533    $fragment = isset($this->options['fragment']) && strlen($this->options['fragment']) ? '#' . $this->options['fragment'] : '';
534    return $uri . $query . $fragment;
535  }
536
537  /**
538   * Indicates if this Url is external.
539   *
540   * @return bool
541   */
542  public function isExternal() {
543    return $this->external;
544  }
545
546  /**
547   * Indicates if this Url has a Drupal route.
548   *
549   * @return bool
550   */
551  public function isRouted() {
552    return !$this->unrouted;
553  }
554
555  /**
556   * Returns the route name.
557   *
558   * @return string
559   *
560   * @throws \UnexpectedValueException.
561   *   If this is a URI with no corresponding route.
562   */
563  public function getRouteName() {
564    if ($this->unrouted) {
565      throw new \UnexpectedValueException('External URLs do not have an internal route name.');
566    }
567
568    return $this->routeName;
569  }
570
571  /**
572   * Returns the route parameters.
573   *
574   * @return array
575   *
576   * @throws \UnexpectedValueException.
577   *   If this is a URI with no corresponding route.
578   */
579  public function getRouteParameters() {
580    if ($this->unrouted) {
581      throw new \UnexpectedValueException('External URLs do not have internal route parameters.');
582    }
583
584    return $this->routeParameters;
585  }
586
587  /**
588   * Sets the route parameters.
589   *
590   * @param array $parameters
591   *   The array of parameters.
592   *
593   * @return $this
594   *
595   * @throws \UnexpectedValueException.
596   *   If this is a URI with no corresponding route.
597   */
598  public function setRouteParameters($parameters) {
599    if ($this->unrouted) {
600      throw new \UnexpectedValueException('External URLs do not have route parameters.');
601    }
602    $this->routeParameters = $parameters;
603    return $this;
604  }
605
606  /**
607   * Sets a specific route parameter.
608   *
609   * @param string $key
610   *   The key of the route parameter.
611   * @param mixed $value
612   *   The route parameter.
613   *
614   * @return $this
615   *
616   * @throws \UnexpectedValueException.
617   *   If this is a URI with no corresponding route.
618   */
619  public function setRouteParameter($key, $value) {
620    if ($this->unrouted) {
621      throw new \UnexpectedValueException('External URLs do not have route parameters.');
622    }
623    $this->routeParameters[$key] = $value;
624    return $this;
625  }
626
627  /**
628   * Returns the URL options.
629   *
630   * @return array
631   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
632   *   it contains.
633   */
634  public function getOptions() {
635    return $this->options;
636  }
637
638  /**
639   * Gets a specific option.
640   *
641   * See \Drupal\Core\Url::fromUri() for details on the options.
642   *
643   * @param string $name
644   *   The name of the option.
645   *
646   * @return mixed
647   *   The value for a specific option, or NULL if it does not exist.
648   */
649  public function getOption($name) {
650    if (!isset($this->options[$name])) {
651      return NULL;
652    }
653
654    return $this->options[$name];
655  }
656
657  /**
658   * Sets the URL options.
659   *
660   * @param array $options
661   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
662   *   it contains.
663   *
664   * @return $this
665   */
666  public function setOptions($options) {
667    $this->options = $options;
668    return $this;
669  }
670
671  /**
672   * Sets a specific option.
673   *
674   * See \Drupal\Core\Url::fromUri() for details on the options.
675   *
676   * @param string $name
677   *   The name of the option.
678   * @param mixed $value
679   *   The option value.
680   *
681   * @return $this
682   */
683  public function setOption($name, $value) {
684    $this->options[$name] = $value;
685    return $this;
686  }
687
688  /**
689   * Merges the URL options with any currently set.
690   *
691   * In the case of conflict with existing options, the new options will replace
692   * the existing options.
693   *
694   * @param array $options
695   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
696   *   it contains.
697   *
698   * @return $this
699   */
700  public function mergeOptions($options) {
701    $this->options = NestedArray::mergeDeep($this->options, $options);
702    return $this;
703  }
704
705  /**
706   * Returns the URI value for this Url object.
707   *
708   * Only to be used if self::$unrouted is TRUE.
709   *
710   * @return string
711   *   A URI not connected to a route. May be an external URL.
712   *
713   * @throws \UnexpectedValueException
714   *   Thrown when the URI was requested for a routed URL.
715   */
716  public function getUri() {
717    if (!$this->unrouted) {
718      throw new \UnexpectedValueException('This URL has a Drupal route, so the canonical form is not a URI.');
719    }
720
721    return $this->uri;
722  }
723
724  /**
725   * Sets the value of the absolute option for this Url.
726   *
727   * @param bool $absolute
728   *   (optional) Whether to make this Url absolute or not. Defaults to TRUE.
729   *
730   * @return $this
731   */
732  public function setAbsolute($absolute = TRUE) {
733    $this->options['absolute'] = $absolute;
734    return $this;
735  }
736
737  /**
738   * Generates the string URL representation for this Url object.
739   *
740   * For an external URL, the string will contain the input plus any query
741   * string or fragment specified by the options array.
742   *
743   * If this Url object was constructed from a Drupal route or from an internal
744   * URI (URIs using the internal:, base:, or entity: schemes), the returned
745   * string will either be a relative URL like /node/1 or an absolute URL like
746   * http://example.com/node/1 depending on the options array, plus any
747   * specified query string or fragment.
748   *
749   * @param bool $collect_bubbleable_metadata
750   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
751   *   associated bubbleable metadata are returned.
752   *
753   * @return string|\Drupal\Core\GeneratedUrl
754   *   A string URL.
755   *   When $collect_bubbleable_metadata is TRUE, a GeneratedUrl object is
756   *   returned, containing the generated URL plus bubbleable metadata.
757   */
758  public function toString($collect_bubbleable_metadata = FALSE) {
759    if ($this->unrouted) {
760      return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions(), $collect_bubbleable_metadata);
761    }
762
763    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions(), $collect_bubbleable_metadata);
764  }
765
766  /**
767   * Returns the route information for a render array.
768   *
769   * @return array
770   *   An associative array suitable for a render array.
771   */
772  public function toRenderArray() {
773    $render_array = [
774      '#url' => $this,
775      '#options' => $this->getOptions(),
776    ];
777    if (!$this->unrouted) {
778      $render_array['#access_callback'] = [get_class(), 'renderAccess'];
779    }
780    return $render_array;
781  }
782
783  /**
784   * Returns the internal path (system path) for this route.
785   *
786   * This path will not include any prefixes, fragments, or query strings.
787   *
788   * @return string
789   *   The internal path for this route.
790   *
791   * @throws \UnexpectedValueException.
792   *   If this is a URI with no corresponding system path.
793   */
794  public function getInternalPath() {
795    if ($this->unrouted) {
796      throw new \UnexpectedValueException('Unrouted URIs do not have internal representations.');
797    }
798
799    if (!isset($this->internalPath)) {
800      $this->internalPath = $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
801    }
802    return $this->internalPath;
803  }
804
805  /**
806   * Checks this Url object against applicable access check services.
807   *
808   * Determines whether the route is accessible or not.
809   *
810   * @param \Drupal\Core\Session\AccountInterface|null $account
811   *   (optional) Run access checks for this account. NULL for the current user.
812   * @param bool $return_as_object
813   *   (optional) Defaults to FALSE.
814   *
815   * @return bool|\Drupal\Core\Access\AccessResultInterface
816   *   The access result. Returns a boolean if $return_as_object is FALSE (this
817   *   is the default) and otherwise an AccessResultInterface object.
818   *   When a boolean is returned, the result of AccessInterface::isAllowed() is
819   *   returned, i.e. TRUE means access is explicitly allowed, FALSE means
820   *   access is either explicitly forbidden or "no opinion".
821   */
822  public function access(AccountInterface $account = NULL, $return_as_object = FALSE) {
823    if ($this->isRouted()) {
824      return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account, $return_as_object);
825    }
826    return $return_as_object ? AccessResult::allowed() : TRUE;
827  }
828
829  /**
830   * Checks a Url render element against applicable access check services.
831   *
832   * @param array $element
833   *   A render element as returned from \Drupal\Core\Url::toRenderArray().
834   *
835   * @return bool
836   *   Returns TRUE if the current user has access to the url, otherwise FALSE.
837   */
838  public static function renderAccess(array $element) {
839    return $element['#url']->access();
840  }
841
842  /**
843   * @return \Drupal\Core\Access\AccessManagerInterface
844   */
845  protected function accessManager() {
846    if (!isset($this->accessManager)) {
847      $this->accessManager = \Drupal::service('access_manager');
848    }
849    return $this->accessManager;
850  }
851
852  /**
853   * Gets the URL generator.
854   *
855   * @return \Drupal\Core\Routing\UrlGeneratorInterface
856   *   The URL generator.
857   */
858  protected function urlGenerator() {
859    if (!$this->urlGenerator) {
860      $this->urlGenerator = \Drupal::urlGenerator();
861    }
862    return $this->urlGenerator;
863  }
864
865  /**
866   * Gets the unrouted URL assembler for non-Drupal URLs.
867   *
868   * @return \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
869   *   The unrouted URL assembler.
870   */
871  protected function unroutedUrlAssembler() {
872    if (!$this->urlAssembler) {
873      $this->urlAssembler = \Drupal::service('unrouted_url_assembler');
874    }
875    return $this->urlAssembler;
876  }
877
878  /**
879   * Sets the URL generator.
880   *
881   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
882   *   (optional) The URL generator, specify NULL to reset it.
883   *
884   * @return $this
885   */
886  public function setUrlGenerator(UrlGeneratorInterface $url_generator = NULL) {
887    $this->urlGenerator = $url_generator;
888    $this->internalPath = NULL;
889    return $this;
890  }
891
892  /**
893   * Sets the unrouted URL assembler.
894   *
895   * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface $url_assembler
896   *   The unrouted URL assembler.
897   *
898   * @return $this
899   */
900  public function setUnroutedUrlAssembler(UnroutedUrlAssemblerInterface $url_assembler) {
901    $this->urlAssembler = $url_assembler;
902    return $this;
903  }
904
905  /**
906   * {@inheritdoc}
907   */
908  public static function trustedCallbacks() {
909    return ['renderAccess'];
910  }
911
912}
913