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