Files
servo/components/script/links.rs
Abbas Olanrewaju Sarafa 782dce68c4 Used encoding-parsing algorithm in follow_hyperlink (#43822)
Used encoding-parsing algorithm in follow_hyperlink

Testing: Ran ```./mach test-wpt
tests/wpt/tests/html/semantics/links/links-created-by-a-and-area-elements```
Result;
```
Running 11 tests in web-platform-tests

Ran 11 tests finished in 74.9 seconds.
  • 11 ran as expected.
```

Second test;
```./mach test-wpt tests/wpt/tests/html/semantics/links/links-created-by-a-and-area-elements/anchor-src-encoding.html```
Result;
```
web-platform-test
~~~~~~~~~~~~~~~~~
Ran 2 checks (1 subtests, 1 tests)
Expected results: 2
Unexpected results: 0
OK
```

Fixes: #43508

---------

Signed-off-by: Sabb <sarafaabbas@gmail.com>
2026-04-02 13:45:08 +00:00

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 html5ever::local_name;
use malloc_size_of::malloc_size_of_is_0;
use net_traits::request::Referrer;
use servo_constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
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::navigation::navigate;
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(&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(&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(&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.encoding_parse_a_url(&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);
navigate(cx, &target.root(), history_handling, false, load_data);
});
target_document
.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task);
};
}