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}