script/dom/location.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
6use dom_struct::dom_struct;
7use net_traits::request::Referrer;
8use servo_url::{MutableOrigin, ServoUrl};
9
10use crate::dom::bindings::codegen::Bindings::LocationBinding::LocationMethods;
11use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
12use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
13use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
14use crate::dom::bindings::root::{Dom, DomRoot};
15use crate::dom::bindings::str::USVString;
16use crate::dom::document::Document;
17use crate::dom::globalscope::GlobalScope;
18use crate::dom::urlhelper::UrlHelper;
19use crate::dom::window::Window;
20use crate::script_runtime::CanGc;
21
22#[derive(PartialEq)]
23pub(crate) enum NavigationType {
24 /// The "[`Location`-object navigate][1]" steps.
25 ///
26 /// [1]: https://html.spec.whatwg.org/multipage/#location-object-navigate
27 Normal,
28
29 /// The last step of [`reload()`][1] (`reload_triggered == true`)
30 ///
31 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
32 ReloadByScript,
33
34 /// User-requested navigation (the unlabeled paragraph after
35 /// [`reload()`][1]).
36 ///
37 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
38 ReloadByConstellation,
39
40 /// Reload triggered by a [declarative refresh][1].
41 ///
42 /// [1]: https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps
43 DeclarativeRefresh,
44}
45
46#[dom_struct]
47pub(crate) struct Location {
48 reflector_: Reflector,
49 window: Dom<Window>,
50}
51
52impl Location {
53 fn new_inherited(window: &Window) -> Location {
54 Location {
55 reflector_: Reflector::new(),
56 window: Dom::from_ref(window),
57 }
58 }
59
60 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Location> {
61 reflect_dom_object(Box::new(Location::new_inherited(window)), window, can_gc)
62 }
63
64 /// Navigate the relevant `Document`'s browsing context.
65 pub(crate) fn navigate(
66 &self,
67 url: ServoUrl,
68 history_handling: NavigationHistoryBehavior,
69 navigation_type: NavigationType,
70 can_gc: CanGc,
71 ) {
72 fn incumbent_window() -> DomRoot<Window> {
73 let incumbent_global = GlobalScope::incumbent().expect("no incumbent global object");
74 DomRoot::downcast(incumbent_global).expect("global object is not a Window")
75 }
76
77 // The active document of the source browsing context used for
78 // navigation determines the request's referrer and referrer policy.
79 let source_window = match navigation_type {
80 NavigationType::ReloadByScript |
81 NavigationType::ReloadByConstellation |
82 NavigationType::DeclarativeRefresh => {
83 // > Navigate the browsing context [...] the source browsing context
84 // > set to the browsing context being navigated.
85 DomRoot::from_ref(&*self.window)
86 },
87 NavigationType::Normal => {
88 // > 2. Let `sourceBrowsingContext` be the incumbent global object's
89 // > browsing context.
90 incumbent_window()
91 },
92 };
93 let source_document = source_window.Document();
94
95 let referrer = Referrer::ReferrerUrl(source_document.url());
96 let referrer_policy = source_document.get_referrer_policy();
97
98 // <https://html.spec.whatwg.org/multipage/#navigate>
99 // > Let `incumbentNavigationOrigin` be the origin of the incumbent
100 // > settings object, or if no script was involved, the origin of the
101 // > node document of the element that initiated the navigation.
102 let navigation_origin_window = match navigation_type {
103 NavigationType::Normal | NavigationType::ReloadByScript => incumbent_window(),
104 NavigationType::ReloadByConstellation | NavigationType::DeclarativeRefresh => {
105 DomRoot::from_ref(&*self.window)
106 },
107 };
108 let (load_origin, creator_pipeline_id) = (
109 navigation_origin_window.origin().immutable().clone(),
110 Some(navigation_origin_window.pipeline_id()),
111 );
112
113 // Is `historyHandling` `reload`?
114 let reload_triggered = match navigation_type {
115 NavigationType::ReloadByScript | NavigationType::ReloadByConstellation => true,
116 NavigationType::Normal | NavigationType::DeclarativeRefresh => false,
117 };
118
119 // Initiate navigation
120 // TODO: rethrow exceptions, set exceptions enabled flag.
121 let load_data = LoadData::new(
122 LoadOrigin::Script(load_origin),
123 url,
124 creator_pipeline_id,
125 referrer,
126 referrer_policy,
127 None, // Top navigation doesn't inherit secure context
128 Some(source_document.insecure_requests_policy()),
129 source_document.has_trustworthy_ancestor_origin(),
130 );
131 self.window
132 .load_url(history_handling, reload_triggered, load_data, can_gc);
133 }
134
135 /// Get if this `Location`'s [relevant `Document`][1] is non-null.
136 ///
137 /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document
138 fn has_document(&self) -> bool {
139 // <https://html.spec.whatwg.org/multipage/#relevant-document>
140 //
141 // > A `Location` object has an associated relevant `Document`, which is
142 // > this `Location` object's relevant global object's browsing
143 // > context's active document, if this `Location` object's relevant
144 // > global object's browsing context is non-null, and null otherwise.
145 self.window.Document().browsing_context().is_some()
146 }
147
148 /// Get this `Location` object's [relevant `Document`][1], or
149 /// `Err(Error::Security)` if it's non-null and its origin is not same
150 /// origin-domain with the entry setting object's origin.
151 ///
152 /// In the specification's terms:
153 ///
154 /// 1. If this `Location` object's relevant `Document` is null, then return
155 /// null.
156 ///
157 /// 2. If this `Location` object's relevant `Document`'s origin is not same
158 /// origin-domain with the entry settings object's origin, then throw a
159 /// "`SecurityError`" `DOMException`.
160 ///
161 /// 3. Return this `Location` object's relevant `Document`.
162 ///
163 /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document
164 fn document_if_same_origin(&self) -> Fallible<Option<DomRoot<Document>>> {
165 // <https://html.spec.whatwg.org/multipage/#relevant-document>
166 //
167 // > A `Location` object has an associated relevant `Document`, which is
168 // > this `Location` object's relevant global object's browsing
169 // > context's active document, if this `Location` object's relevant
170 // > global object's browsing context is non-null, and null otherwise.
171 if let Some(window_proxy) = self.window.Document().browsing_context() {
172 // `Location`'s many other operations:
173 //
174 // > If this `Location` object's relevant `Document` is non-null and
175 // > its origin is not same origin-domain with the entry settings
176 // > object's origin, then throw a "SecurityError" `DOMException`.
177 //
178 // FIXME: We should still return the active document if it's same
179 // origin but not fully active. `WindowProxy::document`
180 // currently returns `None` in this case.
181 if let Some(document) = window_proxy.document().filter(|document| {
182 self.entry_settings_object()
183 .origin()
184 .same_origin_domain(document.origin())
185 }) {
186 Ok(Some(document))
187 } else {
188 Err(Error::Security)
189 }
190 } else {
191 // The browsing context is null
192 Ok(None)
193 }
194 }
195
196 /// Get this `Location` object's [relevant url][1] or
197 /// `Err(Error::Security)` if the [relevant `Document`][2] if it's non-null
198 /// and its origin is not same origin-domain with the entry setting object's
199 /// origin.
200 ///
201 /// [1]: https://html.spec.whatwg.org/multipage/#concept-location-url
202 /// [2]: https://html.spec.whatwg.org/multipage/#relevant-document
203 fn get_url_if_same_origin(&self) -> Fallible<ServoUrl> {
204 Ok(if let Some(document) = self.document_if_same_origin()? {
205 document.url()
206 } else {
207 ServoUrl::parse("about:blank").unwrap()
208 })
209 }
210
211 fn entry_settings_object(&self) -> DomRoot<GlobalScope> {
212 GlobalScope::entry()
213 }
214
215 /// The common algorithm for `Location`'s setters and `Location::Assign`.
216 #[inline]
217 fn setter_common(
218 &self,
219 f: impl FnOnce(ServoUrl) -> Fallible<Option<ServoUrl>>,
220 can_gc: CanGc,
221 ) -> ErrorResult {
222 // Step 1: If this Location object's relevant Document is null, then return.
223 // Step 2: If this Location object's relevant Document's origin is not
224 // same origin-domain with the entry settings object's origin, then
225 // throw a "SecurityError" DOMException.
226 if let Some(document) = self.document_if_same_origin()? {
227 // Step 3: Let copyURL be a copy of this Location object's url.
228 // Step 4: Assign the result of running f(copyURL) to copyURL.
229 if let Some(copy_url) = f(document.url())? {
230 // Step 5: Terminate these steps if copyURL is null.
231 // Step 6: Location-object navigate to copyURL.
232 self.navigate(
233 copy_url,
234 NavigationHistoryBehavior::Push,
235 NavigationType::Normal,
236 can_gc,
237 );
238 }
239 }
240 Ok(())
241 }
242
243 /// Perform a user-requested reload (the unlabeled paragraph after
244 /// [`reload()`][1]).
245 ///
246 /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload
247 pub(crate) fn reload_without_origin_check(&self, can_gc: CanGc) {
248 // > When a user requests that the active document of a browsing context
249 // > be reloaded through a user interface element, the user agent should
250 // > navigate the browsing context to the same resource as that
251 // > `Document`, with `historyHandling` set to "reload".
252 let url = self.window.get_url();
253 self.navigate(
254 url,
255 NavigationHistoryBehavior::Replace,
256 NavigationType::ReloadByConstellation,
257 can_gc,
258 );
259 }
260
261 #[allow(dead_code)]
262 pub(crate) fn origin(&self) -> &MutableOrigin {
263 self.window.origin()
264 }
265}
266
267impl LocationMethods<crate::DomTypeHolder> for Location {
268 // https://html.spec.whatwg.org/multipage/#dom-location-assign
269 fn Assign(&self, url: USVString, can_gc: CanGc) -> ErrorResult {
270 self.setter_common(
271 |_copy_url| {
272 // Step 3: Parse url relative to the entry settings object. If that failed,
273 // throw a "SyntaxError" DOMException.
274 let base_url = self.entry_settings_object().api_base_url();
275 let url = match base_url.join(&url.0) {
276 Ok(url) => url,
277 Err(_) => return Err(Error::Syntax(None)),
278 };
279
280 Ok(Some(url))
281 },
282 can_gc,
283 )
284 }
285
286 // https://html.spec.whatwg.org/multipage/#dom-location-reload
287 fn Reload(&self, can_gc: CanGc) -> ErrorResult {
288 let url = self.get_url_if_same_origin()?;
289 self.navigate(
290 url,
291 NavigationHistoryBehavior::Replace,
292 NavigationType::ReloadByScript,
293 can_gc,
294 );
295 Ok(())
296 }
297
298 // https://html.spec.whatwg.org/multipage/#dom-location-replace
299 fn Replace(&self, url: USVString, can_gc: CanGc) -> ErrorResult {
300 // Step 1: If this Location object's relevant Document is null, then return.
301 if self.has_document() {
302 // Step 2: Parse url relative to the entry settings object. If that failed,
303 // throw a "SyntaxError" DOMException.
304 let base_url = self.entry_settings_object().api_base_url();
305 let url = match base_url.join(&url.0) {
306 Ok(url) => url,
307 Err(_) => return Err(Error::Syntax(None)),
308 };
309 // Step 3: Location-object navigate to the resulting URL record with
310 // the replacement flag set.
311 self.navigate(
312 url,
313 NavigationHistoryBehavior::Replace,
314 NavigationType::Normal,
315 can_gc,
316 );
317 }
318 Ok(())
319 }
320
321 // https://html.spec.whatwg.org/multipage/#dom-location-hash
322 fn GetHash(&self) -> Fallible<USVString> {
323 Ok(UrlHelper::Hash(&self.get_url_if_same_origin()?))
324 }
325
326 // https://html.spec.whatwg.org/multipage/#dom-location-hash
327 fn SetHash(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
328 self.setter_common(
329 |mut copy_url| {
330 // Step 4: Let input be the given value with a single leading "#" removed, if any.
331 // Step 5: Set copyURL's fragment to the empty string.
332 // Step 6: Basic URL parse input, with copyURL as url and fragment state as
333 // state override.
334 let new_fragment = if value.0.starts_with('#') {
335 Some(&value.0[1..])
336 } else {
337 Some(value.0.as_str())
338 };
339 // Step 7: If copyURL's fragment is this's url's fragment, then return.
340 if copy_url.fragment() == new_fragment {
341 Ok(None)
342 } else {
343 copy_url.as_mut_url().set_fragment(new_fragment);
344
345 Ok(Some(copy_url))
346 }
347 },
348 can_gc,
349 )
350 }
351
352 // https://html.spec.whatwg.org/multipage/#dom-location-host
353 fn GetHost(&self) -> Fallible<USVString> {
354 Ok(UrlHelper::Host(&self.get_url_if_same_origin()?))
355 }
356
357 // https://html.spec.whatwg.org/multipage/#dom-location-host
358 fn SetHost(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
359 self.setter_common(
360 |mut copy_url| {
361 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
362 if copy_url.cannot_be_a_base() {
363 return Ok(None);
364 }
365
366 // Step 5: Basic URL parse the given value, with copyURL as url and host state
367 // as state override.
368 let _ = copy_url.as_mut_url().set_host(Some(&value.0));
369
370 Ok(Some(copy_url))
371 },
372 can_gc,
373 )
374 }
375
376 // https://html.spec.whatwg.org/multipage/#dom-location-origin
377 fn GetOrigin(&self) -> Fallible<USVString> {
378 Ok(UrlHelper::Origin(&self.get_url_if_same_origin()?))
379 }
380
381 // https://html.spec.whatwg.org/multipage/#dom-location-hostname
382 fn GetHostname(&self) -> Fallible<USVString> {
383 Ok(UrlHelper::Hostname(&self.get_url_if_same_origin()?))
384 }
385
386 // https://html.spec.whatwg.org/multipage/#dom-location-hostname
387 fn SetHostname(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
388 self.setter_common(
389 |mut copy_url| {
390 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
391 if copy_url.cannot_be_a_base() {
392 return Ok(None);
393 }
394
395 // Step 5: Basic URL parse the given value, with copyURL as url and hostname
396 // state as state override.
397 let _ = copy_url.as_mut_url().set_host(Some(&value.0));
398
399 Ok(Some(copy_url))
400 },
401 can_gc,
402 )
403 }
404
405 // https://html.spec.whatwg.org/multipage/#dom-location-href
406 fn GetHref(&self) -> Fallible<USVString> {
407 Ok(UrlHelper::Href(&self.get_url_if_same_origin()?))
408 }
409
410 // https://html.spec.whatwg.org/multipage/#dom-location-href
411 fn SetHref(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
412 // Step 1: If this Location object's relevant Document is null, then return.
413 if self.has_document() {
414 // Note: no call to self.check_same_origin_domain()
415 // Step 2: Let url be the result of encoding-parsing a URL given the given value, relative to the entry settings object.
416 // Step 3: If url is failure, then throw a "SyntaxError" DOMException.
417 let base_url = self.entry_settings_object().api_base_url();
418 let url = match base_url.join(&value.0) {
419 Ok(url) => url,
420 Err(e) => return Err(Error::Syntax(Some(format!("Couldn't parse URL: {}", e)))),
421 };
422 // Step 4: Location-object navigate this to url.
423 self.navigate(
424 url,
425 NavigationHistoryBehavior::Push,
426 NavigationType::Normal,
427 can_gc,
428 );
429 }
430 Ok(())
431 }
432
433 // https://html.spec.whatwg.org/multipage/#dom-location-pathname
434 fn GetPathname(&self) -> Fallible<USVString> {
435 Ok(UrlHelper::Pathname(&self.get_url_if_same_origin()?))
436 }
437
438 // https://html.spec.whatwg.org/multipage/#dom-location-pathname
439 fn SetPathname(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
440 self.setter_common(
441 |mut copy_url| {
442 // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps.
443 if copy_url.cannot_be_a_base() {
444 return Ok(None);
445 }
446
447 // Step 5: Set copyURL's path to the empty list.
448 // Step 6: Basic URL parse the given value, with copyURL as url and path
449 // start state as state override.
450 copy_url.as_mut_url().set_path(&value.0);
451
452 Ok(Some(copy_url))
453 },
454 can_gc,
455 )
456 }
457
458 // https://html.spec.whatwg.org/multipage/#dom-location-port
459 fn GetPort(&self) -> Fallible<USVString> {
460 Ok(UrlHelper::Port(&self.get_url_if_same_origin()?))
461 }
462
463 // https://html.spec.whatwg.org/multipage/#dom-location-port
464 fn SetPort(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
465 self.setter_common(
466 |mut copy_url| {
467 // Step 4: If copyURL cannot have a username/password/port, then return.
468 // https://url.spec.whatwg.org/#cannot-have-a-username-password-port
469 if copy_url.host().is_none() ||
470 copy_url.cannot_be_a_base() ||
471 copy_url.scheme() == "file"
472 {
473 return Ok(None);
474 }
475
476 // Step 5: If the given value is the empty string, then set copyURL's
477 // port to null.
478 // Step 6: Otherwise, basic URL parse the given value, with copyURL as url
479 // and port state as state override.
480 let _ = url::quirks::set_port(copy_url.as_mut_url(), &value.0);
481
482 Ok(Some(copy_url))
483 },
484 can_gc,
485 )
486 }
487
488 // https://html.spec.whatwg.org/multipage/#dom-location-protocol
489 fn GetProtocol(&self) -> Fallible<USVString> {
490 Ok(UrlHelper::Protocol(&self.get_url_if_same_origin()?))
491 }
492
493 // https://html.spec.whatwg.org/multipage/#dom-location-protocol
494 fn SetProtocol(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
495 self.setter_common(
496 |mut copy_url| {
497 // Step 4: Let possibleFailure be the result of basic URL parsing the given
498 // value, followed by ":", with copyURL as url and scheme start state as
499 // state override.
500 let scheme = match value.0.find(':') {
501 Some(position) => &value.0[..position],
502 None => &value.0,
503 };
504
505 if copy_url.as_mut_url().set_scheme(scheme).is_err() {
506 // Step 5: If possibleFailure is failure, then throw a "SyntaxError" DOMException.
507 return Err(Error::Syntax(None));
508 }
509
510 // Step 6: If copyURL's scheme is not an HTTP(S) scheme, then terminate these steps.
511 if !copy_url.scheme().eq_ignore_ascii_case("http") &&
512 !copy_url.scheme().eq_ignore_ascii_case("https")
513 {
514 return Ok(None);
515 }
516
517 Ok(Some(copy_url))
518 },
519 can_gc,
520 )
521 }
522
523 // https://html.spec.whatwg.org/multipage/#dom-location-search
524 fn GetSearch(&self) -> Fallible<USVString> {
525 Ok(UrlHelper::Search(&self.get_url_if_same_origin()?))
526 }
527
528 // https://html.spec.whatwg.org/multipage/#dom-location-search
529 fn SetSearch(&self, value: USVString, can_gc: CanGc) -> ErrorResult {
530 self.setter_common(
531 |mut copy_url| {
532 // Step 4: If the given value is the empty string, set copyURL's query to null.
533 // Step 5: Otherwise, run these substeps:
534 // 1. Let input be the given value with a single leading "?" removed, if any.
535 // 2. Set copyURL's query to the empty string.
536 // 3. Basic URL parse input, with copyURL as url and query state as state
537 // override, and the relevant Document's document's character encoding as
538 // encoding override.
539 copy_url.as_mut_url().set_query(match value.0.as_str() {
540 "" => None,
541 _ if value.0.starts_with('?') => Some(&value.0[1..]),
542 _ => Some(&value.0),
543 });
544
545 Ok(Some(copy_url))
546 },
547 can_gc,
548 )
549 }
550}