Url.php 29 KB

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