mirror of
https://github.com/servo/servo
synced 2026-04-26 01:25:32 +02:00
This was the last failure in this directory. To fix it, I had to spelunk into a couple of places: 1. We shouldn't use the `base_element()` of the document, but select the first base element, regardless if it has an empty href or not 2. We didn't implement target checking for elements. Only some values are valid and an empty target (which the test also confusingly uses) is not valid. Hence, it should fallback to the base element 3. We weren't sanitizing the value in case it contains an ASCII tab or newline + U+003C. This is true for both the form target as well as for other link elements. All in all, added a lot more specification text to figure out what was going on. Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
510 lines
20 KiB
Rust
510 lines
20 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, PartialEq)]
|
|
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%27s-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/#valid-navigable-target-name>
|
|
fn valid_navigable_target_name(target: &DOMString) -> bool {
|
|
// > A valid navigable target name is any string with at least one character that does not contain both
|
|
// > an ASCII tab or newline and a U+003C (<), and it does not start with a U+005F (_).
|
|
// > (Names starting with a U+005F (_) are reserved for special keywords.)
|
|
if target.is_empty() {
|
|
return false;
|
|
}
|
|
if target.contains_tab_or_newline() && target.contains("\u{003C}") {
|
|
return false;
|
|
}
|
|
if target.starts_with('\u{005F}') {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#valid-navigable-target-name-or-keyword>
|
|
pub(crate) fn valid_navigable_target_name_or_keyword(target: &DOMString) -> bool {
|
|
// > A valid navigable target name or keyword is any string that is either a valid navigable target name
|
|
// > or that is an ASCII case-insensitive match for one of: _blank, _self, _parent, or _top.
|
|
if valid_navigable_target_name(target) {
|
|
return true;
|
|
}
|
|
let target = target.to_ascii_lowercase();
|
|
target == "_blank" || target == "_self" || target == "_parent" || target == "_top"
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#get-an-element%27s-target>
|
|
pub(crate) fn get_element_target(
|
|
subject: &Element,
|
|
target: Option<DOMString>,
|
|
) -> Option<DOMString> {
|
|
assert!(
|
|
subject.is::<HTMLAreaElement>() ||
|
|
subject.is::<HTMLAnchorElement>() ||
|
|
subject.is::<HTMLFormElement>()
|
|
);
|
|
|
|
// Step 1. If target is null, then:
|
|
let target = target.or_else(|| {
|
|
// Step 1.1. If element has a target attribute, then set target to that attribute's value.
|
|
//
|
|
// Note that for a target attribute to be valid, it must be a valid navigable target name
|
|
// or keyword
|
|
let element_target = subject.get_string_attribute(&local_name!("target"));
|
|
if valid_navigable_target_name_or_keyword(&element_target) {
|
|
Some(element_target)
|
|
} else {
|
|
// Step 1.2. Otherwise, if element's node document contains a base element with a target attribute,
|
|
// set target to the value of the target attribute of the first such base element.
|
|
subject
|
|
.owner_document()
|
|
.target_base_element()
|
|
.and_then(|base_element| {
|
|
let element = base_element.upcast::<Element>();
|
|
if element.has_attribute(&local_name!("target")) {
|
|
Some(element.get_string_attribute(&local_name!("target")))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
});
|
|
// Step 2. If target is not null, and contains an ASCII tab or newline and a U+003C (<), then set target to "_blank".
|
|
if let Some(ref target) = target {
|
|
if target.contains_tab_or_newline() && target.contains("\u{003C}") {
|
|
return Some("_blank".into());
|
|
}
|
|
}
|
|
// Step 3. Return target.
|
|
target
|
|
}
|
|
|
|
/// <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, None)
|
|
}
|
|
} 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,
|
|
document.about_base_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);
|
|
};
|
|
}
|