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