Skip to main content

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