UnroutedUrlAssembler.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. namespace Drupal\Core\Utility;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Component\Utility\UrlHelper;
  5. use Drupal\Core\GeneratedUrl;
  6. use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
  7. use Symfony\Component\HttpFoundation\RequestStack;
  8. /**
  9. * Provides a way to build external or non Drupal local domain URLs.
  10. *
  11. * It takes into account configured safe HTTP protocols.
  12. */
  13. class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {
  14. /**
  15. * A request stack object.
  16. *
  17. * @var \Symfony\Component\HttpFoundation\RequestStack
  18. */
  19. protected $requestStack;
  20. /**
  21. * The outbound path processor.
  22. *
  23. * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
  24. */
  25. protected $pathProcessor;
  26. /**
  27. * Constructs a new unroutedUrlAssembler object.
  28. *
  29. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  30. * A request stack object.
  31. * @param \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $path_processor
  32. * The output path processor.
  33. * @param string[] $filter_protocols
  34. * (optional) An array of protocols allowed for URL generation.
  35. */
  36. public function __construct(RequestStack $request_stack, OutboundPathProcessorInterface $path_processor, array $filter_protocols = ['http', 'https']) {
  37. UrlHelper::setAllowedProtocols($filter_protocols);
  38. $this->requestStack = $request_stack;
  39. $this->pathProcessor = $path_processor;
  40. }
  41. /**
  42. * {@inheritdoc}
  43. *
  44. * This is a helper function that calls buildExternalUrl() or buildLocalUrl()
  45. * based on a check of whether the path is a valid external URL.
  46. */
  47. public function assemble($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
  48. // Note that UrlHelper::isExternal will return FALSE if the $uri has a
  49. // disallowed protocol. This is later made safe since we always add at
  50. // least a leading slash.
  51. if (parse_url($uri, PHP_URL_SCHEME) === 'base') {
  52. return $this->buildLocalUrl($uri, $options, $collect_bubbleable_metadata);
  53. }
  54. elseif (UrlHelper::isExternal($uri)) {
  55. // UrlHelper::isExternal() only returns true for safe protocols.
  56. return $this->buildExternalUrl($uri, $options, $collect_bubbleable_metadata);
  57. }
  58. throw new \InvalidArgumentException("The URI '$uri' is invalid. You must use a valid URI scheme. Use base: for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.");
  59. }
  60. /**
  61. * {@inheritdoc}
  62. */
  63. protected function buildExternalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
  64. $this->addOptionDefaults($options);
  65. // Split off the query & fragment.
  66. $parsed = UrlHelper::parse($uri);
  67. $uri = $parsed['path'];
  68. $parsed += ['query' => []];
  69. $options += ['query' => []];
  70. $options['query'] = NestedArray::mergeDeep($parsed['query'], $options['query']);
  71. if ($parsed['fragment'] && !$options['fragment']) {
  72. $options['fragment'] = '#' . $parsed['fragment'];
  73. }
  74. if (isset($options['https'])) {
  75. if ($options['https'] === TRUE) {
  76. $uri = str_replace('http://', 'https://', $uri);
  77. }
  78. elseif ($options['https'] === FALSE) {
  79. $uri = str_replace('https://', 'http://', $uri);
  80. }
  81. }
  82. // Append the query.
  83. if ($options['query']) {
  84. $uri .= '?' . UrlHelper::buildQuery($options['query']);
  85. }
  86. // Reassemble.
  87. $url = $uri . $options['fragment'];
  88. return $collect_bubbleable_metadata ? (new GeneratedUrl())->setGeneratedUrl($url) : $url;
  89. }
  90. /**
  91. * {@inheritdoc}
  92. */
  93. protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
  94. $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
  95. $this->addOptionDefaults($options);
  96. $request = $this->requestStack->getCurrentRequest();
  97. // Remove the base: scheme.
  98. // @todo Consider using a class constant for this in
  99. // https://www.drupal.org/node/2417459
  100. $uri = substr($uri, 5);
  101. // Allow (outbound) path processing, if needed. A valid use case is the path
  102. // alias overview form:
  103. // @see \Drupal\path\Controller\PathController::adminOverview().
  104. if (!empty($options['path_processing'])) {
  105. // Do not pass the request, since this is a special case and we do not
  106. // want to include e.g. the request language in the processing.
  107. $uri = $this->pathProcessor->processOutbound($uri, $options, NULL, $generated_url);
  108. }
  109. // Strip leading slashes from internal paths to prevent them becoming
  110. // external URLs without protocol. /example.com should not be turned into
  111. // //example.com.
  112. $uri = ltrim($uri, '/');
  113. // Add any subdirectory where Drupal is installed.
  114. $current_base_path = $request->getBasePath() . '/';
  115. if ($options['absolute']) {
  116. $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path;
  117. if (isset($options['https'])) {
  118. if (!empty($options['https'])) {
  119. $base = str_replace('http://', 'https://', $current_base_url);
  120. $options['absolute'] = TRUE;
  121. }
  122. else {
  123. $base = str_replace('https://', 'http://', $current_base_url);
  124. $options['absolute'] = TRUE;
  125. }
  126. }
  127. else {
  128. $base = $current_base_url;
  129. }
  130. if ($collect_bubbleable_metadata) {
  131. $generated_url->addCacheContexts(['url.site']);
  132. }
  133. }
  134. else {
  135. $base = $current_base_path;
  136. }
  137. $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
  138. $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
  139. $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
  140. $url = $base . $options['script'] . $uri . $query . $options['fragment'];
  141. return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
  142. }
  143. /**
  144. * Merges in default defaults
  145. *
  146. * @param array $options
  147. * The options to merge in the defaults.
  148. */
  149. protected function addOptionDefaults(array &$options) {
  150. $request = $this->requestStack->getCurrentRequest();
  151. $current_base_path = $request->getBasePath() . '/';
  152. $current_script_path = '';
  153. $base_path_with_script = $request->getBaseUrl();
  154. // If the current request was made with the script name (eg, index.php) in
  155. // it, then extract it, making sure the leading / is gone, and a trailing /
  156. // is added, to allow simple string concatenation with other parts.
  157. if (!empty($base_path_with_script)) {
  158. $script_name = $request->getScriptName();
  159. if (strpos($base_path_with_script, $script_name) !== FALSE) {
  160. $current_script_path = ltrim(substr($script_name, strlen($current_base_path)), '/') . '/';
  161. }
  162. }
  163. // Merge in defaults.
  164. $options += [
  165. 'fragment' => '',
  166. 'query' => [],
  167. 'absolute' => FALSE,
  168. 'prefix' => '',
  169. 'script' => $current_script_path,
  170. ];
  171. if (isset($options['fragment']) && $options['fragment'] !== '') {
  172. $options['fragment'] = '#' . $options['fragment'];
  173. }
  174. }
  175. }