Files
servo/components/script/links.rs
Narfinger 423800eec4 Script: Lazily transform the DOMString into Rust String instead of immediately. (#39509)
This implements LazyDOMString (from now on DOMString) as outlined in
https://github.com/servo/servo/issues/39479.
Constructing from a *mut JSString we keep the in a
RootedTraceableBox<Heap<*mut JSString>> and transform
the string into a rust string if necessary via the `make_rust_string`
method.
Methods used in script are implemented on this string. Currently we
transform the string at all times.
But in the future more efficient implementations are possible.

We implement the safety critical sections in a separate module
DOMStringInner which allows simple constructors, `make_rust_string` and
the `bytes` method.
This method returns the new type `EncodedBytes` which contains the
reference to the underlying string in either format.

Testing: WPT tests still seem to work, so this should test this
functionality.

---------

Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
2025-10-09 18:18:03 +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().immutable().clone()),
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 || {
debug!("following hyperlink to {}", load_data.url);
target.root().load_url(history_handling, false, load_data, CanGc::note());
});
target_document
.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task);
};
}