3 namespace Drupal\Core\Utility;
5 use Drupal\Component\Utility\UrlHelper;
6 use Drupal\Core\GeneratedUrl;
7 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
8 use Symfony\Component\HttpFoundation\RequestStack;
11 * Provides a way to build external or non Drupal local domain URLs.
13 * It takes into account configured safe HTTP protocols.
15 class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {
18 * A request stack object.
20 * @var \Symfony\Component\HttpFoundation\RequestStack
22 protected $requestStack;
25 * The outbound path processor.
27 * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
29 protected $pathProcessor;
32 * Constructs a new unroutedUrlAssembler object.
34 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
35 * A request stack object.
36 * @param \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $path_processor
37 * The output path processor.
38 * @param string[] $filter_protocols
39 * (optional) An array of protocols allowed for URL generation.
41 public function __construct(RequestStack $request_stack, OutboundPathProcessorInterface $path_processor, array $filter_protocols = ['http', 'https']) {
42 UrlHelper::setAllowedProtocols($filter_protocols);
43 $this->requestStack = $request_stack;
44 $this->pathProcessor = $path_processor;
50 * This is a helper function that calls buildExternalUrl() or buildLocalUrl()
51 * based on a check of whether the path is a valid external URL.
53 public function assemble($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
54 // Note that UrlHelper::isExternal will return FALSE if the $uri has a
55 // disallowed protocol. This is later made safe since we always add at
56 // least a leading slash.
57 if (parse_url($uri, PHP_URL_SCHEME) === 'base') {
58 return $this->buildLocalUrl($uri, $options, $collect_bubbleable_metadata);
60 elseif (UrlHelper::isExternal($uri)) {
61 // UrlHelper::isExternal() only returns true for safe protocols.
62 return $this->buildExternalUrl($uri, $options, $collect_bubbleable_metadata);
64 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.");
70 protected function buildExternalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
71 $this->addOptionDefaults($options);
72 // Split off the fragment.
73 if (strpos($uri, '#') !== FALSE) {
74 list($uri, $old_fragment) = explode('#', $uri, 2);
75 // If $options contains no fragment, take it over from the path.
76 if (isset($old_fragment) && !$options['fragment']) {
77 $options['fragment'] = '#' . $old_fragment;
81 if (isset($options['https'])) {
82 if ($options['https'] === TRUE) {
83 $uri = str_replace('http://', 'https://', $uri);
85 elseif ($options['https'] === FALSE) {
86 $uri = str_replace('https://', 'http://', $uri);
90 if ($options['query']) {
91 $uri .= (strpos($uri, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']);
94 $url = $uri . $options['fragment'];
95 return $collect_bubbleable_metadata ? (new GeneratedUrl())->setGeneratedUrl($url) : $url;
101 protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
102 $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
104 $this->addOptionDefaults($options);
105 $request = $this->requestStack->getCurrentRequest();
107 // Remove the base: scheme.
108 // @todo Consider using a class constant for this in
109 // https://www.drupal.org/node/2417459
110 $uri = substr($uri, 5);
112 // Allow (outbound) path processing, if needed. A valid use case is the path
113 // alias overview form:
114 // @see \Drupal\path\Controller\PathController::adminOverview().
115 if (!empty($options['path_processing'])) {
116 // Do not pass the request, since this is a special case and we do not
117 // want to include e.g. the request language in the processing.
118 $uri = $this->pathProcessor->processOutbound($uri, $options, NULL, $generated_url);
120 // Strip leading slashes from internal paths to prevent them becoming
121 // external URLs without protocol. /example.com should not be turned into
123 $uri = ltrim($uri, '/');
125 // Add any subdirectory where Drupal is installed.
126 $current_base_path = $request->getBasePath() . '/';
128 if ($options['absolute']) {
129 $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path;
130 if (isset($options['https'])) {
131 if (!empty($options['https'])) {
132 $base = str_replace('http://', 'https://', $current_base_url);
133 $options['absolute'] = TRUE;
136 $base = str_replace('https://', 'http://', $current_base_url);
137 $options['absolute'] = TRUE;
141 $base = $current_base_url;
143 if ($collect_bubbleable_metadata) {
144 $generated_url->addCacheContexts(['url.site']);
148 $base = $current_base_path;
151 $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
153 $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
154 $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
155 $url = $base . $options['script'] . $uri . $query . $options['fragment'];
156 return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
160 * Merges in default defaults
162 * @param array $options
163 * The options to merge in the defaults.
165 protected function addOptionDefaults(array &$options) {
166 $request = $this->requestStack->getCurrentRequest();
167 $current_base_path = $request->getBasePath() . '/';
168 $current_script_path = '';
169 $base_path_with_script = $request->getBaseUrl();
171 // If the current request was made with the script name (eg, index.php) in
172 // it, then extract it, making sure the leading / is gone, and a trailing /
173 // is added, to allow simple string concatenation with other parts.
174 if (!empty($base_path_with_script)) {
175 $script_name = $request->getScriptName();
176 if (strpos($base_path_with_script, $script_name) !== FALSE) {
177 $current_script_path = ltrim(substr($script_name, strlen($current_base_path)), '/') . '/';
181 // Merge in defaults.
187 'script' => $current_script_path,
190 if (isset($options['fragment']) && $options['fragment'] !== '') {
191 $options['fragment'] = '#' . $options['fragment'];