Upgraded drupal core with security updates
[yaffs-website] / web / core / lib / Drupal / Core / Utility / UnroutedUrlAssembler.php
1 <?php
2
3 namespace Drupal\Core\Utility;
4
5 use Drupal\Component\Utility\UrlHelper;
6 use Drupal\Core\GeneratedUrl;
7 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
8 use Symfony\Component\HttpFoundation\RequestStack;
9
10 /**
11  * Provides a way to build external or non Drupal local domain URLs.
12  *
13  * It takes into account configured safe HTTP protocols.
14  */
15 class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {
16
17   /**
18    * A request stack object.
19    *
20    * @var \Symfony\Component\HttpFoundation\RequestStack
21    */
22   protected $requestStack;
23
24   /**
25    * The outbound path processor.
26    *
27    * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
28    */
29   protected $pathProcessor;
30
31   /**
32    * Constructs a new unroutedUrlAssembler object.
33    *
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.
40    */
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;
45   }
46
47   /**
48    * {@inheritdoc}
49    *
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.
52    */
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);
59     }
60     elseif (UrlHelper::isExternal($uri)) {
61       // UrlHelper::isExternal() only returns true for safe protocols.
62       return $this->buildExternalUrl($uri, $options, $collect_bubbleable_metadata);
63     }
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.");
65   }
66
67   /**
68    * {@inheritdoc}
69    */
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;
78       }
79     }
80
81     if (isset($options['https'])) {
82       if ($options['https'] === TRUE) {
83         $uri = str_replace('http://', 'https://', $uri);
84       }
85       elseif ($options['https'] === FALSE) {
86         $uri = str_replace('https://', 'http://', $uri);
87       }
88     }
89     // Append the query.
90     if ($options['query']) {
91       $uri .= (strpos($uri, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']);
92     }
93     // Reassemble.
94     $url = $uri . $options['fragment'];
95     return $collect_bubbleable_metadata ? (new GeneratedUrl())->setGeneratedUrl($url) : $url;
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
102     $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
103
104     $this->addOptionDefaults($options);
105     $request = $this->requestStack->getCurrentRequest();
106
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);
111
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);
119     }
120     // Strip leading slashes from internal paths to prevent them becoming
121     // external URLs without protocol. /example.com should not be turned into
122     // //example.com.
123     $uri = ltrim($uri, '/');
124
125     // Add any subdirectory where Drupal is installed.
126     $current_base_path = $request->getBasePath() . '/';
127
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;
134         }
135         else {
136           $base = str_replace('https://', 'http://', $current_base_url);
137           $options['absolute'] = TRUE;
138         }
139       }
140       else {
141         $base = $current_base_url;
142       }
143       if ($collect_bubbleable_metadata) {
144         $generated_url->addCacheContexts(['url.site']);
145       }
146     }
147     else {
148       $base = $current_base_path;
149     }
150
151     $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
152
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;
157   }
158
159   /**
160    * Merges in default defaults
161    *
162    * @param array $options
163    *   The options to merge in the defaults.
164    */
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();
170
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)), '/') . '/';
178       }
179     }
180
181     // Merge in defaults.
182     $options += [
183       'fragment' => '',
184       'query' => [],
185       'absolute' => FALSE,
186       'prefix' => '',
187       'script' => $current_script_path,
188     ];
189
190     if (isset($options['fragment']) && $options['fragment'] !== '') {
191       $options['fragment'] = '#' . $options['fragment'];
192     }
193   }
194
195 }