Files
servo/components/script/dom/location.rs
Tim van der Lippe c0b55a2c34 script: Add about_base_url (#42104)
We populate the required field for all relevant entrypoints
and set it to `document.base_url` when the url is `about:blank`
or `about:srcdoc`. In all other cases, it uses
`document.about_base_url`.

Testing: WPT
Fixes #41836

---------

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2026-01-27 18:09:01 +00:00

571 lines
24 KiB
Rust

/* 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,
}
#[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)
}
/// <https://html.spec.whatwg.org/multipage/#location-object-navigate>
fn navigate_a_location(
&self,
url: ServoUrl,
history_handling: NavigationHistoryBehavior,
can_gc: CanGc,
) {
// Step 1. Let navigable be location's relevant global object's navigable.
let navigable = &self.window;
let navigable_document = navigable.Document();
// Step 2. Let sourceDocument be the incumbent global object's associated Document.
let incumbent_global = GlobalScope::incumbent().expect("no incumbent global object");
let mut load_data = incumbent_global
.as_window()
.load_data_for_document(url, navigable.pipeline_id());
load_data.about_base_url = navigable_document.about_base_url();
// Step 3. If location's relevant Document is not yet completely loaded,
// and the incumbent global object does not have transient activation, then set historyHandling to "replace".
//
// TODO: check for transient activation
let history_handling = if !navigable_document.completely_loaded() {
NavigationHistoryBehavior::Replace
} else {
history_handling
};
// Step 4. Navigate navigable to url using sourceDocument, with exceptionsEnabled set to true and historyHandling set to historyHandling.
navigable.load_url(history_handling, false, load_data, can_gc);
}
/// Navigate the relevant `Document`'s browsing context.
///
/// This is ostensibly an implementation of
/// <https://html.spec.whatwg.org/multipage/#navigate>, but the specification has
/// greatly deviated from our code.
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 => {
// > 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 => DomRoot::from_ref(&*self.window),
};
let (load_origin, creator_pipeline_id) = (
navigation_origin_window.origin().snapshot(),
Some(navigation_origin_window.pipeline_id()),
);
// Is `historyHandling` `reload`?
let reload_triggered = match navigation_type {
NavigationType::ReloadByScript | NavigationType::ReloadByConstellation => true,
NavigationType::Normal => false,
};
// Initiate navigation
// TODO: rethrow exceptions, set exceptions enabled flag.
let load_data = LoadData::new(
LoadOrigin::Script(load_origin),
url,
source_document.about_base_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(),
source_document.creation_sandboxing_flag_set_considering_parent_iframe(),
);
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(None))` 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(None))
}
} else {
// The browsing context is null
Ok(None)
}
}
/// Get this `Location` object's [relevant url][1] or
/// `Err(Error::Security(None))` 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,
);
}
#[expect(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. Let urlRecord be the result of encoding-parsing a URL given url, relative to the entry settings object.
let base_url = self.entry_settings_object().api_base_url();
let url = match base_url.join(&url.0) {
Ok(url) => url,
// Step 3. If urlRecord is failure, then throw a "SyntaxError" DOMException.
Err(_) => return Err(Error::Syntax(None)),
};
// Step 4. Location-object navigate this to urlRecord given "replace".
self.navigate_a_location(url, NavigationHistoryBehavior::Replace, 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 {
// Step 1. If this's relevant Document is null, then return.
if self.has_document() {
// Step 2. If this's relevant Document's origin is not same origin-domain
// with the entry settings object's origin, then throw a "SecurityError" DOMException.
// Step 3. Let copyURL be a copy of this's url.
let mut copy_url = self.get_url_if_same_origin()?;
// Step 4. Let thisURLFragment be copyURL's fragment if it is non-null; otherwise the empty string.
let this_url_fragment = copy_url.fragment().map(str::to_owned).unwrap_or_default();
// Step 6. Set copyURL's fragment to the empty string.
// Step 7. Basic URL parse input, with copyURL as url and fragment state as state override.
let input = &value.0;
// Note that if the hash is the empty string, we shouldn't then set the fragment to `None`.
// That's because the empty string is a valid hash target and should then scroll to the
// top of the document. Therefore, we don't use `UrlHelpers::SetHash` here, which would
// set it to `None`.
copy_url.set_fragment(match input {
// Step 5. Let input be the given value with a single leading "#" removed, if any.
_ if input.starts_with('#') => Some(&input[1..]),
_ => Some(input),
});
// Step 8. If copyURL's fragment is thisURLFragment, then return.
if copy_url.fragment() != Some(&this_url_fragment) {
// Step 9. Location-object navigate this to copyURL.
self.navigate_a_location(copy_url, NavigationHistoryBehavior::Auto, can_gc);
}
}
Ok(())
}
/// <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'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.
let base_url = self.entry_settings_object().api_base_url();
let url = match base_url.join(&value.0) {
Ok(url) => url,
// Step 3: If url is failure, then throw a "SyntaxError" DOMException.
Err(e) => return Err(Error::Syntax(Some(format!("Couldn't parse URL: {}", e)))),
};
// Step 4: Location-object navigate this to url.
self.navigate_a_location(url, NavigationHistoryBehavior::Auto, 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,
)
}
}