| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior}; |
| use dom_struct::dom_struct; |
| use net_traits::request::Referrer; |
| use servo_url::{MutableOrigin, ServoUrl}; |
| |
| use crate::dom::bindings::codegen::Bindings::LocationBinding::LocationMethods; |
| use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; |
| use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; |
| use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; |
| use crate::dom::bindings::root::{Dom, DomRoot}; |
| use crate::dom::bindings::str::USVString; |
| use crate::dom::document::Document; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::urlhelper::UrlHelper; |
| use crate::dom::window::Window; |
| use crate::script_runtime::CanGc; |
| |
| #[derive(PartialEq)] |
| pub(crate) enum NavigationType { |
| /// The "[`Location`-object navigate][1]" steps. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#location-object-navigate |
| Normal, |
| |
| /// The last step of [`reload()`][1] (`reload_triggered == true`) |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload |
| ReloadByScript, |
| |
| /// User-requested navigation (the unlabeled paragraph after |
| /// [`reload()`][1]). |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload |
| ReloadByConstellation, |
| |
| /// Reload triggered by a [declarative refresh][1]. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps |
| DeclarativeRefresh, |
| } |
| |
| #[dom_struct] |
| pub(crate) struct Location { |
| reflector_: Reflector, |
| window: Dom<Window>, |
| } |
| |
| impl Location { |
| fn new_inherited(window: &Window) -> Location { |
| Location { |
| reflector_: Reflector::new(), |
| window: Dom::from_ref(window), |
| } |
| } |
| |
| pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Location> { |
| reflect_dom_object(Box::new(Location::new_inherited(window)), window, can_gc) |
| } |
| |
| /// Navigate the relevant `Document`'s browsing context. |
| pub(crate) fn navigate( |
| &self, |
| url: ServoUrl, |
| history_handling: NavigationHistoryBehavior, |
| navigation_type: NavigationType, |
| can_gc: CanGc, |
| ) { |
| fn incumbent_window() -> DomRoot<Window> { |
| let incumbent_global = GlobalScope::incumbent().expect("no incumbent global object"); |
| DomRoot::downcast(incumbent_global).expect("global object is not a Window") |
| } |
| |
| // The active document of the source browsing context used for |
| // navigation determines the request's referrer and referrer policy. |
| let source_window = match navigation_type { |
| NavigationType::ReloadByScript | |
| NavigationType::ReloadByConstellation | |
| NavigationType::DeclarativeRefresh => { |
| // > Navigate the browsing context [...] the source browsing context |
| // > set to the browsing context being navigated. |
| DomRoot::from_ref(&*self.window) |
| }, |
| NavigationType::Normal => { |
| // > 2. Let `sourceBrowsingContext` be the incumbent global object's |
| // > browsing context. |
| incumbent_window() |
| }, |
| }; |
| let source_document = source_window.Document(); |
| |
| let referrer = Referrer::ReferrerUrl(source_document.url()); |
| let referrer_policy = source_document.get_referrer_policy(); |
| |
| // <https://html.spec.whatwg.org/multipage/#navigate> |
| // > Let `incumbentNavigationOrigin` be the origin of the incumbent |
| // > settings object, or if no script was involved, the origin of the |
| // > node document of the element that initiated the navigation. |
| let navigation_origin_window = match navigation_type { |
| NavigationType::Normal | NavigationType::ReloadByScript => incumbent_window(), |
| NavigationType::ReloadByConstellation | NavigationType::DeclarativeRefresh => { |
| DomRoot::from_ref(&*self.window) |
| }, |
| }; |
| let (load_origin, creator_pipeline_id) = ( |
| navigation_origin_window.origin().immutable().clone(), |
| Some(navigation_origin_window.pipeline_id()), |
| ); |
| |
| // Is `historyHandling` `reload`? |
| let reload_triggered = match navigation_type { |
| NavigationType::ReloadByScript | NavigationType::ReloadByConstellation => true, |
| NavigationType::Normal | NavigationType::DeclarativeRefresh => false, |
| }; |
| |
| // Initiate navigation |
| // TODO: rethrow exceptions, set exceptions enabled flag. |
| let load_data = LoadData::new( |
| LoadOrigin::Script(load_origin), |
| url, |
| creator_pipeline_id, |
| referrer, |
| referrer_policy, |
| None, // Top navigation doesn't inherit secure context |
| Some(source_document.insecure_requests_policy()), |
| source_document.has_trustworthy_ancestor_origin(), |
| ); |
| self.window |
| .load_url(history_handling, reload_triggered, load_data, can_gc); |
| } |
| |
| /// Get if this `Location`'s [relevant `Document`][1] is non-null. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document |
| fn has_document(&self) -> bool { |
| // <https://html.spec.whatwg.org/multipage/#relevant-document> |
| // |
| // > A `Location` object has an associated relevant `Document`, which is |
| // > this `Location` object's relevant global object's browsing |
| // > context's active document, if this `Location` object's relevant |
| // > global object's browsing context is non-null, and null otherwise. |
| self.window.Document().browsing_context().is_some() |
| } |
| |
| /// Get this `Location` object's [relevant `Document`][1], or |
| /// `Err(Error::Security)` if it's non-null and its origin is not same |
| /// origin-domain with the entry setting object's origin. |
| /// |
| /// In the specification's terms: |
| /// |
| /// 1. If this `Location` object's relevant `Document` is null, then return |
| /// null. |
| /// |
| /// 2. If this `Location` object's relevant `Document`'s origin is not same |
| /// origin-domain with the entry settings object's origin, then throw a |
| /// "`SecurityError`" `DOMException`. |
| /// |
| /// 3. Return this `Location` object's relevant `Document`. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#relevant-document |
| fn document_if_same_origin(&self) -> Fallible<Option<DomRoot<Document>>> { |
| // <https://html.spec.whatwg.org/multipage/#relevant-document> |
| // |
| // > A `Location` object has an associated relevant `Document`, which is |
| // > this `Location` object's relevant global object's browsing |
| // > context's active document, if this `Location` object's relevant |
| // > global object's browsing context is non-null, and null otherwise. |
| if let Some(window_proxy) = self.window.Document().browsing_context() { |
| // `Location`'s many other operations: |
| // |
| // > If this `Location` object's relevant `Document` is non-null and |
| // > its origin is not same origin-domain with the entry settings |
| // > object's origin, then throw a "SecurityError" `DOMException`. |
| // |
| // FIXME: We should still return the active document if it's same |
| // origin but not fully active. `WindowProxy::document` |
| // currently returns `None` in this case. |
| if let Some(document) = window_proxy.document().filter(|document| { |
| self.entry_settings_object() |
| .origin() |
| .same_origin_domain(document.origin()) |
| }) { |
| Ok(Some(document)) |
| } else { |
| Err(Error::Security) |
| } |
| } else { |
| // The browsing context is null |
| Ok(None) |
| } |
| } |
| |
| /// Get this `Location` object's [relevant url][1] or |
| /// `Err(Error::Security)` if the [relevant `Document`][2] if it's non-null |
| /// and its origin is not same origin-domain with the entry setting object's |
| /// origin. |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#concept-location-url |
| /// [2]: https://html.spec.whatwg.org/multipage/#relevant-document |
| fn get_url_if_same_origin(&self) -> Fallible<ServoUrl> { |
| Ok(if let Some(document) = self.document_if_same_origin()? { |
| document.url() |
| } else { |
| ServoUrl::parse("about:blank").unwrap() |
| }) |
| } |
| |
| fn entry_settings_object(&self) -> DomRoot<GlobalScope> { |
| GlobalScope::entry() |
| } |
| |
| /// The common algorithm for `Location`'s setters and `Location::Assign`. |
| #[inline] |
| fn setter_common( |
| &self, |
| f: impl FnOnce(ServoUrl) -> Fallible<Option<ServoUrl>>, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| // Step 1: If this Location object's relevant Document is null, then return. |
| // Step 2: If this Location object's relevant Document's origin is not |
| // same origin-domain with the entry settings object's origin, then |
| // throw a "SecurityError" DOMException. |
| if let Some(document) = self.document_if_same_origin()? { |
| // Step 3: Let copyURL be a copy of this Location object's url. |
| // Step 4: Assign the result of running f(copyURL) to copyURL. |
| if let Some(copy_url) = f(document.url())? { |
| // Step 5: Terminate these steps if copyURL is null. |
| // Step 6: Location-object navigate to copyURL. |
| self.navigate( |
| copy_url, |
| NavigationHistoryBehavior::Push, |
| NavigationType::Normal, |
| can_gc, |
| ); |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Perform a user-requested reload (the unlabeled paragraph after |
| /// [`reload()`][1]). |
| /// |
| /// [1]: https://html.spec.whatwg.org/multipage/#dom-location-reload |
| pub(crate) fn reload_without_origin_check(&self, can_gc: CanGc) { |
| // > When a user requests that the active document of a browsing context |
| // > be reloaded through a user interface element, the user agent should |
| // > navigate the browsing context to the same resource as that |
| // > `Document`, with `historyHandling` set to "reload". |
| let url = self.window.get_url(); |
| self.navigate( |
| url, |
| NavigationHistoryBehavior::Replace, |
| NavigationType::ReloadByConstellation, |
| can_gc, |
| ); |
| } |
| |
| #[allow(dead_code)] |
| pub(crate) fn origin(&self) -> &MutableOrigin { |
| self.window.origin() |
| } |
| } |
| |
| impl LocationMethods<crate::DomTypeHolder> for Location { |
| // https://html.spec.whatwg.org/multipage/#dom-location-assign |
| fn Assign(&self, url: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |_copy_url| { |
| // Step 3: Parse url relative to the entry settings object. If that failed, |
| // throw a "SyntaxError" DOMException. |
| let base_url = self.entry_settings_object().api_base_url(); |
| let url = match base_url.join(&url.0) { |
| Ok(url) => url, |
| Err(_) => return Err(Error::Syntax(None)), |
| }; |
| |
| Ok(Some(url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-reload |
| fn Reload(&self, can_gc: CanGc) -> ErrorResult { |
| let url = self.get_url_if_same_origin()?; |
| self.navigate( |
| url, |
| NavigationHistoryBehavior::Replace, |
| NavigationType::ReloadByScript, |
| can_gc, |
| ); |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-replace |
| fn Replace(&self, url: USVString, can_gc: CanGc) -> ErrorResult { |
| // Step 1: If this Location object's relevant Document is null, then return. |
| if self.has_document() { |
| // Step 2: Parse url relative to the entry settings object. If that failed, |
| // throw a "SyntaxError" DOMException. |
| let base_url = self.entry_settings_object().api_base_url(); |
| let url = match base_url.join(&url.0) { |
| Ok(url) => url, |
| Err(_) => return Err(Error::Syntax(None)), |
| }; |
| // Step 3: Location-object navigate to the resulting URL record with |
| // the replacement flag set. |
| self.navigate( |
| url, |
| NavigationHistoryBehavior::Replace, |
| NavigationType::Normal, |
| can_gc, |
| ); |
| } |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-hash |
| fn GetHash(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Hash(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-hash |
| fn SetHash(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: Let input be the given value with a single leading "#" removed, if any. |
| // Step 5: Set copyURL's fragment to the empty string. |
| // Step 6: Basic URL parse input, with copyURL as url and fragment state as |
| // state override. |
| let new_fragment = if value.0.starts_with('#') { |
| Some(&value.0[1..]) |
| } else { |
| Some(value.0.as_str()) |
| }; |
| // Step 7: If copyURL's fragment is this's url's fragment, then return. |
| if copy_url.fragment() == new_fragment { |
| Ok(None) |
| } else { |
| copy_url.as_mut_url().set_fragment(new_fragment); |
| |
| Ok(Some(copy_url)) |
| } |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-host |
| fn GetHost(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Host(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-host |
| fn SetHost(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps. |
| if copy_url.cannot_be_a_base() { |
| return Ok(None); |
| } |
| |
| // Step 5: Basic URL parse the given value, with copyURL as url and host state |
| // as state override. |
| let _ = copy_url.as_mut_url().set_host(Some(&value.0)); |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-origin |
| fn GetOrigin(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Origin(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-hostname |
| fn GetHostname(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Hostname(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-hostname |
| fn SetHostname(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps. |
| if copy_url.cannot_be_a_base() { |
| return Ok(None); |
| } |
| |
| // Step 5: Basic URL parse the given value, with copyURL as url and hostname |
| // state as state override. |
| let _ = copy_url.as_mut_url().set_host(Some(&value.0)); |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-href |
| fn GetHref(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Href(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-href |
| fn SetHref(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| // Step 1: If this Location object's relevant Document is null, then return. |
| if self.has_document() { |
| // Note: no call to self.check_same_origin_domain() |
| // Step 2: Let url be the result of encoding-parsing a URL given the given value, relative to the entry settings object. |
| // Step 3: If url is failure, then throw a "SyntaxError" DOMException. |
| let base_url = self.entry_settings_object().api_base_url(); |
| let url = match base_url.join(&value.0) { |
| Ok(url) => url, |
| Err(e) => return Err(Error::Syntax(Some(format!("Couldn't parse URL: {}", e)))), |
| }; |
| // Step 4: Location-object navigate this to url. |
| self.navigate( |
| url, |
| NavigationHistoryBehavior::Push, |
| NavigationType::Normal, |
| can_gc, |
| ); |
| } |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-pathname |
| fn GetPathname(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Pathname(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-pathname |
| fn SetPathname(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: If copyURL's cannot-be-a-base-URL flag is set, terminate these steps. |
| if copy_url.cannot_be_a_base() { |
| return Ok(None); |
| } |
| |
| // Step 5: Set copyURL's path to the empty list. |
| // Step 6: Basic URL parse the given value, with copyURL as url and path |
| // start state as state override. |
| copy_url.as_mut_url().set_path(&value.0); |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-port |
| fn GetPort(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Port(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-port |
| fn SetPort(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: If copyURL cannot have a username/password/port, then return. |
| // https://url.spec.whatwg.org/#cannot-have-a-username-password-port |
| if copy_url.host().is_none() || |
| copy_url.cannot_be_a_base() || |
| copy_url.scheme() == "file" |
| { |
| return Ok(None); |
| } |
| |
| // Step 5: If the given value is the empty string, then set copyURL's |
| // port to null. |
| // Step 6: Otherwise, basic URL parse the given value, with copyURL as url |
| // and port state as state override. |
| let _ = url::quirks::set_port(copy_url.as_mut_url(), &value.0); |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-protocol |
| fn GetProtocol(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Protocol(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-protocol |
| fn SetProtocol(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: Let possibleFailure be the result of basic URL parsing the given |
| // value, followed by ":", with copyURL as url and scheme start state as |
| // state override. |
| let scheme = match value.0.find(':') { |
| Some(position) => &value.0[..position], |
| None => &value.0, |
| }; |
| |
| if copy_url.as_mut_url().set_scheme(scheme).is_err() { |
| // Step 5: If possibleFailure is failure, then throw a "SyntaxError" DOMException. |
| return Err(Error::Syntax(None)); |
| } |
| |
| // Step 6: If copyURL's scheme is not an HTTP(S) scheme, then terminate these steps. |
| if !copy_url.scheme().eq_ignore_ascii_case("http") && |
| !copy_url.scheme().eq_ignore_ascii_case("https") |
| { |
| return Ok(None); |
| } |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-search |
| fn GetSearch(&self) -> Fallible<USVString> { |
| Ok(UrlHelper::Search(&self.get_url_if_same_origin()?)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location-search |
| fn SetSearch(&self, value: USVString, can_gc: CanGc) -> ErrorResult { |
| self.setter_common( |
| |mut copy_url| { |
| // Step 4: If the given value is the empty string, set copyURL's query to null. |
| // Step 5: Otherwise, run these substeps: |
| // 1. Let input be the given value with a single leading "?" removed, if any. |
| // 2. Set copyURL's query to the empty string. |
| // 3. Basic URL parse input, with copyURL as url and query state as state |
| // override, and the relevant Document's document's character encoding as |
| // encoding override. |
| copy_url.as_mut_url().set_query(match value.0.as_str() { |
| "" => None, |
| _ if value.0.starts_with('?') => Some(&value.0[1..]), |
| _ => Some(&value.0), |
| }); |
| |
| Ok(Some(copy_url)) |
| }, |
| can_gc, |
| ) |
| } |
| } |