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