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