Files
servo/components/script/links.rs
Josh Matthews a97a345d6e script: Check same-origin-domain when evaluating javscript: URLs. (#41969)
These changes introduce a new OriginSnapshot type, which is an immutable
version of MutableOrigin (ie. an origin that includes an optional domain
modifier). This is now propagated as part of LoadData's origin, allowing
us to perform the same-origin-domain check for javascript: URLs as
needed.

Testing: Newly-passing tests.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
2026-01-18 02:32:51 +00:00

461 lines
18 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/. */
//! Defines shared hyperlink behaviour for `<link>`, `<a>`, `<area>` and `<form>` elements.
use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
use html5ever::{local_name, ns};
use malloc_size_of::malloc_size_of_is_0;
use net_traits::request::Referrer;
use style::str::HTML_SPACE_CHARACTERS;
use crate::dom::bindings::codegen::Bindings::AttrBinding::Attr_Binding::AttrMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::str::DOMString;
use crate::dom::element::referrer_policy_for_element;
use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
use crate::dom::html::htmlareaelement::HTMLAreaElement;
use crate::dom::html::htmlformelement::HTMLFormElement;
use crate::dom::html::htmllinkelement::HTMLLinkElement;
use crate::dom::node::NodeTraits;
use crate::dom::types::Element;
use crate::script_runtime::CanGc;
bitflags::bitflags! {
/// Describes the different relations that can be specified on elements using the `rel`
/// attribute.
///
/// Refer to <https://html.spec.whatwg.org/multipage/#linkTypes> for more information.
#[derive(Clone, Copy, Debug)]
pub(crate) struct LinkRelations: u32 {
/// <https://html.spec.whatwg.org/multipage/#rel-alternate>
const ALTERNATE = 1;
/// <https://html.spec.whatwg.org/multipage/#link-type-author>
const AUTHOR = 1 << 1;
/// <https://html.spec.whatwg.org/multipage/#link-type-bookmark>
const BOOKMARK = 1 << 2;
/// <https://html.spec.whatwg.org/multipage/#link-type-canonical>
const CANONICAL = 1 << 3;
/// <https://html.spec.whatwg.org/multipage/#link-type-dns-prefetch>
const DNS_PREFETCH = 1 << 4;
/// <https://html.spec.whatwg.org/multipage/#link-type-expect>
const EXPECT = 1 << 5;
/// <https://html.spec.whatwg.org/multipage/#link-type-external>
const EXTERNAL = 1 << 6;
/// <https://html.spec.whatwg.org/multipage/#link-type-help>
const HELP = 1 << 7;
/// <https://html.spec.whatwg.org/multipage/#rel-icon>
const ICON = 1 << 8;
/// <https://html.spec.whatwg.org/multipage/#link-type-license>
const LICENSE = 1 << 9;
/// <https://html.spec.whatwg.org/multipage/#link-type-next>
const NEXT = 1 << 10;
/// <https://html.spec.whatwg.org/multipage/#link-type-manifest>
const MANIFEST = 1 << 11;
/// <https://html.spec.whatwg.org/multipage/#link-type-modulepreload>
const MODULE_PRELOAD = 1 << 12;
/// <https://html.spec.whatwg.org/multipage/#link-type-nofollow>
const NO_FOLLOW = 1 << 13;
/// <https://html.spec.whatwg.org/multipage/#link-type-noopener>
const NO_OPENER = 1 << 14;
/// <https://html.spec.whatwg.org/multipage/#link-type-noreferrer>
const NO_REFERRER = 1 << 15;
/// <https://html.spec.whatwg.org/multipage/#link-type-opener>
const OPENER = 1 << 16;
/// <https://html.spec.whatwg.org/multipage/#link-type-pingback>
const PING_BACK = 1 << 17;
/// <https://html.spec.whatwg.org/multipage/#link-type-preconnect>
const PRECONNECT = 1 << 18;
/// <https://html.spec.whatwg.org/multipage/#link-type-prefetch>
const PREFETCH = 1 << 19;
/// <https://html.spec.whatwg.org/multipage/#link-type-preload>
const PRELOAD = 1 << 20;
/// <https://html.spec.whatwg.org/multipage/#link-type-prev>
const PREV = 1 << 21;
/// <https://html.spec.whatwg.org/multipage/#link-type-privacy-policy>
const PRIVACY_POLICY = 1 << 22;
/// <https://html.spec.whatwg.org/multipage/#link-type-search>
const SEARCH = 1 << 23;
/// <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>
const STYLESHEET = 1 << 24;
/// <https://html.spec.whatwg.org/multipage/#link-type-tag>
const TAG = 1 << 25;
/// <https://html.spec.whatwg.org/multipage/#link-type-terms-of-service>
const TERMS_OF_SERVICE = 1 << 26;
}
}
impl LinkRelations {
/// The set of allowed relations for [`<link>`] elements
///
/// [`<link>`]: https://html.spec.whatwg.org/multipage/#htmllinkelement
pub(crate) const ALLOWED_LINK_RELATIONS: Self = Self::ALTERNATE
.union(Self::CANONICAL)
.union(Self::AUTHOR)
.union(Self::DNS_PREFETCH)
.union(Self::EXPECT)
.union(Self::HELP)
.union(Self::ICON)
.union(Self::MANIFEST)
.union(Self::MODULE_PRELOAD)
.union(Self::LICENSE)
.union(Self::NEXT)
.union(Self::PING_BACK)
.union(Self::PRECONNECT)
.union(Self::PREFETCH)
.union(Self::PRELOAD)
.union(Self::PREV)
.union(Self::PRIVACY_POLICY)
.union(Self::SEARCH)
.union(Self::STYLESHEET)
.union(Self::TERMS_OF_SERVICE);
/// The set of allowed relations for [`<a>`] and [`<area>`] elements
///
/// [`<a>`]: https://html.spec.whatwg.org/multipage/#the-a-element
/// [`<area>`]: https://html.spec.whatwg.org/multipage/#the-area-element
pub(crate) const ALLOWED_ANCHOR_OR_AREA_RELATIONS: Self = Self::ALTERNATE
.union(Self::AUTHOR)
.union(Self::BOOKMARK)
.union(Self::EXTERNAL)
.union(Self::HELP)
.union(Self::LICENSE)
.union(Self::NEXT)
.union(Self::NO_FOLLOW)
.union(Self::NO_OPENER)
.union(Self::NO_REFERRER)
.union(Self::OPENER)
.union(Self::PREV)
.union(Self::PRIVACY_POLICY)
.union(Self::SEARCH)
.union(Self::TAG)
.union(Self::TERMS_OF_SERVICE);
/// The set of allowed relations for [`<form>`] elements
///
/// [`<form>`]: https://html.spec.whatwg.org/multipage/#the-form-element
pub(crate) const ALLOWED_FORM_RELATIONS: Self = Self::EXTERNAL
.union(Self::HELP)
.union(Self::LICENSE)
.union(Self::NEXT)
.union(Self::NO_FOLLOW)
.union(Self::NO_OPENER)
.union(Self::NO_REFERRER)
.union(Self::OPENER)
.union(Self::PREV)
.union(Self::SEARCH);
/// Compute the set of relations for an element given its `"rel"` attribute
///
/// This function should only be used with [`<link>`], [`<a>`], [`<area>`] and [`<form>`] elements.
///
/// [`<link>`]: https://html.spec.whatwg.org/multipage/#htmllinkelement
/// [`<a>`]: https://html.spec.whatwg.org/multipage/#the-a-element
/// [`<area>`]: https://html.spec.whatwg.org/multipage/#the-area-element
/// [`<form>`]: https://html.spec.whatwg.org/multipage/#the-form-element
pub(crate) fn for_element(element: &Element) -> Self {
let rel = element.get_attribute(&ns!(), &local_name!("rel")).map(|e| {
let value = e.value();
(**value).to_owned()
});
let mut relations = rel
.map(|attribute| {
attribute
.split(HTML_SPACE_CHARACTERS)
.map(Self::from_single_keyword)
.collect()
})
.unwrap_or(Self::empty());
// For historical reasons, "rev=made" is treated as if the "author" relation was specified
let has_legacy_author_relation = element
.get_attribute(&ns!(), &local_name!("rev"))
.is_some_and(|rev| &**rev.value() == "made");
if has_legacy_author_relation {
relations |= Self::AUTHOR;
}
let allowed_relations = if element.is::<HTMLLinkElement>() {
Self::ALLOWED_LINK_RELATIONS
} else if element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>() {
Self::ALLOWED_ANCHOR_OR_AREA_RELATIONS
} else if element.is::<HTMLFormElement>() {
Self::ALLOWED_FORM_RELATIONS
} else {
Self::empty()
};
relations & allowed_relations
}
/// Parse one single link relation keyword
///
/// If the keyword is invalid then `Self::empty()` is returned.
fn from_single_keyword(keyword: &str) -> Self {
if keyword.eq_ignore_ascii_case("alternate") {
Self::ALTERNATE
} else if keyword.eq_ignore_ascii_case("canonical") {
Self::CANONICAL
} else if keyword.eq_ignore_ascii_case("author") {
Self::AUTHOR
} else if keyword.eq_ignore_ascii_case("bookmark") {
Self::BOOKMARK
} else if keyword.eq_ignore_ascii_case("dns-prefetch") {
Self::DNS_PREFETCH
} else if keyword.eq_ignore_ascii_case("expect") {
Self::EXPECT
} else if keyword.eq_ignore_ascii_case("external") {
Self::EXTERNAL
} else if keyword.eq_ignore_ascii_case("help") {
Self::HELP
} else if keyword.eq_ignore_ascii_case("icon") ||
keyword.eq_ignore_ascii_case("shortcut icon") ||
keyword.eq_ignore_ascii_case("apple-touch-icon")
{
// TODO: "apple-touch-icon" is not in the spec. Where did it come from? Do we need it?
// There is also "apple-touch-icon-precomposed" listed in
// https://github.com/servo/servo/blob/e43e4778421be8ea30db9d5c553780c042161522/components/script/dom/htmllinkelement.rs#L452-L467
Self::ICON
} else if keyword.eq_ignore_ascii_case("manifest") {
Self::MANIFEST
} else if keyword.eq_ignore_ascii_case("modulepreload") {
Self::MODULE_PRELOAD
} else if keyword.eq_ignore_ascii_case("license") ||
keyword.eq_ignore_ascii_case("copyright")
{
Self::LICENSE
} else if keyword.eq_ignore_ascii_case("next") {
Self::NEXT
} else if keyword.eq_ignore_ascii_case("nofollow") {
Self::NO_FOLLOW
} else if keyword.eq_ignore_ascii_case("noopener") {
Self::NO_OPENER
} else if keyword.eq_ignore_ascii_case("noreferrer") {
Self::NO_REFERRER
} else if keyword.eq_ignore_ascii_case("opener") {
Self::OPENER
} else if keyword.eq_ignore_ascii_case("pingback") {
Self::PING_BACK
} else if keyword.eq_ignore_ascii_case("preconnect") {
Self::PRECONNECT
} else if keyword.eq_ignore_ascii_case("prefetch") {
Self::PREFETCH
} else if keyword.eq_ignore_ascii_case("preload") {
Self::PRELOAD
} else if keyword.eq_ignore_ascii_case("prev") || keyword.eq_ignore_ascii_case("previous") {
Self::PREV
} else if keyword.eq_ignore_ascii_case("privacy-policy") {
Self::PRIVACY_POLICY
} else if keyword.eq_ignore_ascii_case("search") {
Self::SEARCH
} else if keyword.eq_ignore_ascii_case("stylesheet") {
Self::STYLESHEET
} else if keyword.eq_ignore_ascii_case("tag") {
Self::TAG
} else if keyword.eq_ignore_ascii_case("terms-of-service") {
Self::TERMS_OF_SERVICE
} else {
Self::empty()
}
}
/// <https://html.spec.whatwg.org/multipage/#get-an-element's-noopener>
pub(crate) fn get_element_noopener(&self, target_attribute_value: Option<&DOMString>) -> bool {
// Step 1. If element's link types include the noopener or noreferrer keyword, then return true.
if self.contains(Self::NO_OPENER) || self.contains(Self::NO_REFERRER) {
return true;
}
// Step 2. If element's link types do not include the opener keyword and
// target is an ASCII case-insensitive match for "_blank", then return true.
let target_is_blank =
target_attribute_value.is_some_and(|target| target.to_ascii_lowercase() == "_blank");
if !self.contains(Self::OPENER) && target_is_blank {
return true;
}
// Step 3. Return false.
false
}
}
malloc_size_of_is_0!(LinkRelations);
/// <https://html.spec.whatwg.org/multipage/#get-an-element's-target>
pub(crate) fn get_element_target(subject: &Element) -> Option<DOMString> {
if !(subject.is::<HTMLAreaElement>() ||
subject.is::<HTMLAnchorElement>() ||
subject.is::<HTMLFormElement>())
{
return None;
}
if subject.has_attribute(&local_name!("target")) {
return Some(subject.get_string_attribute(&local_name!("target")));
}
let doc = subject.owner_document().base_element();
match doc {
Some(doc) => {
let element = doc.upcast::<Element>();
if element.has_attribute(&local_name!("target")) {
Some(element.get_string_attribute(&local_name!("target")))
} else {
None
}
},
None => None,
}
}
/// <https://html.spec.whatwg.org/multipage/#following-hyperlinks-2>
pub(crate) fn follow_hyperlink(
subject: &Element,
relations: LinkRelations,
hyperlink_suffix: Option<String>,
) {
// Step 1: If subject cannot navigate, then return.
if subject.cannot_navigate() {
return;
}
// Step 2: Let targetAttributeValue be the empty string.
// This is done below.
// Step 3: If subject is an a or area element, then set targetAttributeValue to the
// result of getting an element's target given subject.
//
// Also allow the user to open links in a new WebView by pressing either the meta or
// control key (depending on the platform).
let document = subject.owner_document();
let target_attribute_value =
if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
if document
.event_handler()
.alternate_action_keyboard_modifier_active()
{
Some("_blank".into())
} else {
get_element_target(subject)
}
} else {
None
};
// Step 4: Let urlRecord be the result of encoding-parsing a URL given subject's href
// attribute value, relative to subject's node document.
// Step 5: If urlRecord is failure, then return.
// TODO: Implement this.
// Step 6: Let noopener be the result of getting an element's noopener with subject,
// urlRecord, and targetAttributeValue.
let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
// Step 7: Let targetNavigable be the first return value of applying the rules for
// choosing a navigable given targetAttributeValue, subject's node navigable, and
// noopener.
let window = document.window();
let source = document.browsing_context().unwrap();
let (maybe_chosen, history_handling) = match target_attribute_value {
Some(name) => {
let (maybe_chosen, new) = source.choose_browsing_context(name, noopener);
let history_handling = if new {
NavigationHistoryBehavior::Replace
} else {
NavigationHistoryBehavior::Push
};
(maybe_chosen, history_handling)
},
None => (Some(window.window_proxy()), NavigationHistoryBehavior::Push),
};
// Step 8: If targetNavigable is null, then return.
let chosen = match maybe_chosen {
Some(proxy) => proxy,
None => return,
};
if let Some(target_document) = chosen.document() {
let target_window = target_document.window();
// Step 9: Let urlString be the result of applying the URL serializer to urlRecord.
// TODO: Implement this.
let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap();
let mut href = attribute.Value();
// Step 10: If hyperlinkSuffix is non-null, then append it to urlString.
if let Some(suffix) = hyperlink_suffix {
href.push_str(&suffix);
}
let Ok(url) = document.base_url().join(&href.str()) else {
return;
};
// Step 11: Let referrerPolicy be the current state of subject's referrerpolicy content attribute.
let referrer_policy = referrer_policy_for_element(subject);
// Step 12: If subject's link types includes the noreferrer keyword, then set
// referrerPolicy to "no-referrer".
let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
Referrer::NoReferrer
} else {
target_window.as_global_scope().get_referrer()
};
// Step 13: Navigate targetNavigable to urlString using subject's node document,
// with referrerPolicy set to referrerPolicy, userInvolvement set to
// userInvolvement, and sourceElement set to subject.
let pipeline_id = target_window.as_global_scope().pipeline_id();
let secure = target_window.as_global_scope().is_secure_context();
let load_data = LoadData::new(
LoadOrigin::Script(document.origin().snapshot()),
url,
Some(pipeline_id),
referrer,
referrer_policy,
Some(secure),
Some(document.insecure_requests_policy()),
document.has_trustworthy_ancestor_origin(),
document.creation_sandboxing_flag_set_considering_parent_iframe(),
);
let target = Trusted::new(target_window);
let task = task!(navigate_follow_hyperlink: move |cx| {
debug!("following hyperlink to {}", load_data.url);
target.root().load_url(history_handling, false, load_data, CanGc::from_cx(cx));
});
target_document
.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task);
};
}