diff --git a/components/script/dom/htmlanchorelement.rs b/components/script/dom/htmlanchorelement.rs index d571a2f997f..c0f648603af 100644 --- a/components/script/dom/htmlanchorelement.rs +++ b/components/script/dom/htmlanchorelement.rs @@ -2,48 +2,46 @@ * 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 std::cell::Cell; use std::default::Default; use dom_struct::dom_struct; use html5ever::{local_name, namespace_url, ns, LocalName, Prefix}; use js::rust::HandleObject; -use net_traits::request::Referrer; use num_traits::ToPrimitive; -use script_traits::{HistoryEntryReplacement, LoadData, LoadOrigin}; use servo_atoms::Atom; use servo_url::ServoUrl; use style::attr::AttrValue; use crate::dom::activation::Activatable; +use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; -use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use crate::dom::bindings::codegen::Bindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods; use crate::dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::inheritance::Castable; -use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; -use crate::dom::element::{referrer_policy_for_element, Element}; +use crate::dom::element::{AttributeMutation, Element}; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; -use crate::dom::globalscope::GlobalScope; -use crate::dom::htmlareaelement::HTMLAreaElement; use crate::dom::htmlelement::HTMLElement; -use crate::dom::htmlformelement::HTMLFormElement; use crate::dom::htmlimageelement::HTMLImageElement; use crate::dom::mouseevent::MouseEvent; -use crate::dom::node::{document_from_node, Node}; +use crate::dom::node::{document_from_node, BindContext, Node}; use crate::dom::urlhelper::UrlHelper; use crate::dom::virtualmethods::VirtualMethods; -use crate::task_source::TaskSource; +use crate::links::{follow_hyperlink, LinkRelations}; #[dom_struct] pub struct HTMLAnchorElement { htmlelement: HTMLElement, rel_list: MutNullableDom, + + #[no_trace] + relations: Cell, #[no_trace] url: DomRefCell>, } @@ -57,6 +55,7 @@ impl HTMLAnchorElement { HTMLAnchorElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), rel_list: Default::default(), + relations: Cell::new(LinkRelations::empty()), url: DomRefCell::new(None), } } @@ -121,6 +120,27 @@ impl VirtualMethods for HTMLAnchorElement { .parse_plain_attribute(name, value), } } + + fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { + self.super_type().unwrap().attribute_mutated(attr, mutation); + + match *attr.local_name() { + local_name!("rel") | local_name!("rev") => { + self.relations + .set(LinkRelations::for_element(self.upcast())); + }, + _ => {}, + } + } + + fn bind_to_tree(&self, context: &BindContext) { + if let Some(s) = self.super_type() { + s.bind_to_tree(context); + } + + self.relations + .set(LinkRelations::for_element(self.upcast())); + } } impl HTMLAnchorElementMethods for HTMLAnchorElement { @@ -580,153 +600,6 @@ impl Activatable for HTMLAnchorElement { // Step 2. //TODO: Download the link is `download` attribute is set. - follow_hyperlink(element, ismap_suffix); + follow_hyperlink(element, self.relations.get(), ismap_suffix); } } - -/// -pub fn get_element_target(subject: &Element) -> Option { - if !(subject.is::() || - subject.is::() || - subject.is::()) - { - return None; - } - if subject.has_attribute(&local_name!("target")) { - return Some(subject.get_string_attribute(&local_name!("target"))); - } - - let doc = document_from_node(subject).base_element(); - match doc { - Some(doc) => { - let element = doc.upcast::(); - if element.has_attribute(&local_name!("target")) { - Some(element.get_string_attribute(&local_name!("target"))) - } else { - None - } - }, - None => None, - } -} - -/// -pub fn get_element_noopener(subject: &Element, target_attribute_value: Option) -> bool { - if !(subject.is::() || - subject.is::() || - subject.is::()) - { - return false; - } - let target_is_blank = target_attribute_value - .as_ref() - .is_some_and(|target| target.to_lowercase() == "_blank"); - let link_types = match subject.get_attribute(&ns!(), &local_name!("rel")) { - Some(rel) => rel.Value(), - None => return target_is_blank, - }; - link_types.contains("noreferrer") || - link_types.contains("noopener") || - (!link_types.contains("opener") && target_is_blank) -} - -/// -pub fn follow_hyperlink(subject: &Element, hyperlink_suffix: Option) { - // Step 1. - if subject.cannot_navigate() { - return; - } - // Step 2, done in Step 7. - - let document = document_from_node(subject); - let window = document.window(); - - // Step 3: source browsing context. - let source = document.browsing_context().unwrap(); - - // Step 4-5: target attribute. - let target_attribute_value = - if subject.is::() || subject.is::() { - get_element_target(subject) - } else { - None - }; - // Step 6. - let noopener = get_element_noopener(subject, target_attribute_value.clone()); - - // Step 7. - let (maybe_chosen, replace) = match target_attribute_value { - Some(name) => { - let (maybe_chosen, new) = source.choose_browsing_context(name, noopener); - let replace = if new { - HistoryEntryReplacement::Enabled - } else { - HistoryEntryReplacement::Disabled - }; - (maybe_chosen, replace) - }, - None => ( - Some(window.window_proxy()), - HistoryEntryReplacement::Disabled, - ), - }; - - // Step 8. - 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, dis-owning target's opener, if necessary - // will have been done as part of Step 7 above - // in choose_browsing_context/create_auxiliary_browsing_context. - - // Step 10, 11. TODO: if parsing the URL failed, navigate to error page. - let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap(); - let mut href = attribute.Value(); - // Step 11: append a hyperlink suffix. - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28925 - if let Some(suffix) = hyperlink_suffix { - href.push_str(&suffix); - } - let url = match document.base_url().join(&href) { - Ok(url) => url, - Err(_) => return, - }; - - // Step 12. - let referrer_policy = referrer_policy_for_element(subject); - - // Step 13 - let referrer = match subject.get_attribute(&ns!(), &local_name!("rel")) { - Some(ref link_types) if link_types.Value().contains("noreferrer") => { - Referrer::NoReferrer - }, - _ => target_window.upcast::().get_referrer(), - }; - - // Step 14 - let pipeline_id = target_window.upcast::().pipeline_id(); - let secure = target_window.upcast::().is_secure_context(); - let load_data = LoadData::new( - LoadOrigin::Script(document.origin().immutable().clone()), - url, - Some(pipeline_id), - referrer, - referrer_policy, - Some(secure), - ); - let target = Trusted::new(target_window); - let task = task!(navigate_follow_hyperlink: move || { - debug!("following hyperlink to {}", load_data.url); - target.root().load_url(replace, false, load_data); - }); - target_window - .task_manager() - .dom_manipulation_task_source() - .queue(task, target_window.upcast()) - .unwrap(); - }; -} diff --git a/components/script/dom/htmlareaelement.rs b/components/script/dom/htmlareaelement.rs index 59a55b22f04..19224834240 100644 --- a/components/script/dom/htmlareaelement.rs +++ b/components/script/dom/htmlareaelement.rs @@ -2,6 +2,7 @@ * 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 std::cell::Cell; use std::default::Default; use std::{f32, str}; @@ -14,19 +15,20 @@ use servo_atoms::Atom; use style::attr::AttrValue; use crate::dom::activation::Activatable; +use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::HTMLAreaElementBinding::HTMLAreaElementMethods; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; -use crate::dom::element::Element; +use crate::dom::element::{AttributeMutation, Element}; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; -use crate::dom::htmlanchorelement::follow_hyperlink; use crate::dom::htmlelement::HTMLElement; -use crate::dom::node::Node; +use crate::dom::node::{BindContext, Node}; use crate::dom::virtualmethods::VirtualMethods; +use crate::links::{follow_hyperlink, LinkRelations}; #[derive(Debug, PartialEq)] pub enum Area { @@ -75,7 +77,7 @@ impl Area { index += 1; } - //This vector will hold all parsed coordinates + // This vector will hold all parsed coordinates let mut number_list = Vec::new(); let mut array = Vec::new(); @@ -237,6 +239,9 @@ impl Area { pub struct HTMLAreaElement { htmlelement: HTMLElement, rel_list: MutNullableDom, + + #[no_trace] + relations: Cell, } impl HTMLAreaElement { @@ -248,6 +253,7 @@ impl HTMLAreaElement { HTMLAreaElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), rel_list: Default::default(), + relations: Cell::new(LinkRelations::empty()), } } @@ -300,6 +306,27 @@ impl VirtualMethods for HTMLAreaElement { .parse_plain_attribute(name, value), } } + + fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { + self.super_type().unwrap().attribute_mutated(attr, mutation); + + match *attr.local_name() { + local_name!("rel") | local_name!("rev") => { + self.relations + .set(LinkRelations::for_element(self.upcast())); + }, + _ => {}, + } + } + + fn bind_to_tree(&self, context: &BindContext) { + if let Some(s) = self.super_type() { + s.bind_to_tree(context); + } + + self.relations + .set(LinkRelations::for_element(self.upcast())); + } } impl HTMLAreaElementMethods for HTMLAreaElement { @@ -345,6 +372,6 @@ impl Activatable for HTMLAreaElement { } fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { - follow_hyperlink(self.as_element(), None); + follow_hyperlink(self.as_element(), self.relations.get(), None); } } diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index 875b66a22ba..1f32d793d2f 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -24,6 +24,7 @@ use style_dom::ElementState; use super::bindings::trace::{HashMapTracedValues, NoTrace}; use crate::body::Extractable; +use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::AttrBinding::Attr_Binding::AttrMethods; use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods; @@ -56,7 +57,6 @@ use crate::dom::file::File; use crate::dom::formdata::FormData; use crate::dom::formdataevent::FormDataEvent; use crate::dom::globalscope::GlobalScope; -use crate::dom::htmlanchorelement::{get_element_noopener, get_element_target}; use crate::dom::htmlbuttonelement::HTMLButtonElement; use crate::dom::htmlcollection::CollectionFilter; use crate::dom::htmldatalistelement::HTMLDataListElement; @@ -72,7 +72,7 @@ use crate::dom::htmloutputelement::HTMLOutputElement; use crate::dom::htmlselectelement::HTMLSelectElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::node::{ - document_from_node, window_from_node, Node, NodeFlags, UnbindContext, + document_from_node, window_from_node, BindContext, Node, NodeFlags, UnbindContext, VecPreOrderInsertionHelper, }; use crate::dom::nodelist::{NodeList, RadioListMode}; @@ -80,6 +80,7 @@ use crate::dom::radionodelist::RadioNodeList; use crate::dom::submitevent::SubmitEvent; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; +use crate::links::{get_element_target, LinkRelations}; use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; @@ -98,6 +99,9 @@ pub struct HTMLFormElement { past_names_map: DomRefCell, NoTrace)>>, firing_submission_events: Cell, rel_list: MutNullableDom, + + #[no_trace] + relations: Cell, } impl HTMLFormElement { @@ -121,6 +125,7 @@ impl HTMLFormElement { past_names_map: DomRefCell::new(HashMapTracedValues::new()), firing_submission_events: Cell::new(false), rel_list: Default::default(), + relations: Cell::new(LinkRelations::empty()), } } @@ -798,8 +803,10 @@ impl HTMLFormElement { }; // Step 18 - let noopener = - get_element_noopener(self.upcast::(), target_attribute_value.clone()); + let noopener = self + .relations + .get() + .get_element_noopener(target_attribute_value.as_ref()); // Step 19 let source = doc.browsing_context().unwrap(); @@ -1685,6 +1692,27 @@ impl VirtualMethods for HTMLFormElement { .parse_plain_attribute(name, value), } } + + fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { + self.super_type().unwrap().attribute_mutated(attr, mutation); + + match *attr.local_name() { + local_name!("rel") | local_name!("rev") => { + self.relations + .set(LinkRelations::for_element(self.upcast())); + }, + _ => {}, + } + } + + fn bind_to_tree(&self, context: &BindContext) { + if let Some(s) = self.super_type() { + s.bind_to_tree(context); + } + + self.relations + .set(LinkRelations::for_element(self.upcast())); + } } pub trait FormControlElementHelpers { diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index df970d498c4..566d7896f02 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -55,7 +55,7 @@ use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::stylesheet::StyleSheet as DOMStyleSheet; use crate::dom::virtualmethods::VirtualMethods; use crate::fetch::create_a_potential_cors_request; -use crate::link_relations::LinkRelations; +use crate::links::LinkRelations; use crate::network_listener::{submit_timing, NetworkListener, PreInvoke, ResourceTimingListener}; use crate::stylesheet_loader::{StylesheetContextSource, StylesheetLoader, StylesheetOwner}; diff --git a/components/script/lib.rs b/components/script/lib.rs index 1db1a94730b..ca70f062e64 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -94,7 +94,7 @@ mod webdriver_handlers; #[warn(deprecated)] mod window_named_properties; -mod link_relations; +mod links; pub use init::init; pub use script_runtime::JSEngineSetup; diff --git a/components/script/link_relations.rs b/components/script/link_relations.rs deleted file mode 100644 index d32ea9e4e92..00000000000 --- a/components/script/link_relations.rs +++ /dev/null @@ -1,131 +0,0 @@ -/* 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 html5ever::{local_name, namespace_url, ns}; -use malloc_size_of::malloc_size_of_is_0; -use style::str::HTML_SPACE_CHARACTERS; - -use crate::dom::types::Element; - -bitflags::bitflags! { - #[derive(Clone, Copy, Debug)] - pub struct LinkRelations: u32 { - const ALTERNATE = 1; - const AUTHOR = 1 << 1; - const BOOKMARK = 1 << 2; - const CANONICAL = 1 << 3; - const DNS_PREFETCH = 1 << 4; - const EXPECT = 1 << 5; - const EXTERNAL = 1 << 6; - const HELP = 1 << 7; - const ICON = 1 << 8; - const LICENSE = 1 << 9; - const NEXT = 1 << 10; - const MANIFEST = 1 << 11; - const MODULE_PRELOAD = 1 << 12; - const NO_FOLLOW = 1 << 13; - const NO_OPENER = 1 << 14; - const NO_REFERRER = 1 << 15; - const OPENER = 1 << 16; - const PING_BACK = 1 << 17; - const PRECONNECT = 1 << 18; - const PREFETCH = 1 << 19; - const PRELOAD = 1 << 20; - const PREV = 1 << 21; - const PRIVACY_POLICY = 1 << 22; - const SEARCH = 1 << 23; - const STYLESHEET = 1 << 24; - const TAG = 1 << 25; - const TermsOfService = 1 << 26; - } -} - -impl LinkRelations { - pub fn for_element(element: &Element) -> Self { - let rel = element.get_attribute(&ns!(), &local_name!("rel")).map(|e| { - let value = e.value(); - (**value).to_owned() - }); - - // FIXME: for a, area and form elements we need to allow a different - // set of attributes - let mut relations = rel - .map(|attribute| { - attribute - .split(HTML_SPACE_CHARACTERS) - .map(Self::from_single_keyword_for_link_element) - .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; - } - - relations - } - - /// Parse one of the relations allowed for the `` element - /// - /// If the keyword is invalid then `Self::empty` is returned. - fn from_single_keyword_for_link_element(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("dns-prefetch") { - Self::DNS_PREFETCH - } else if keyword.eq_ignore_ascii_case("expect") { - Self::EXPECT - } 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("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("terms-of-service") { - Self::TermsOfService - } else { - Self::empty() - } - } -} - -malloc_size_of_is_0!(LinkRelations); diff --git a/components/script/links.rs b/components/script/links.rs new file mode 100644 index 00000000000..863de553303 --- /dev/null +++ b/components/script/links.rs @@ -0,0 +1,443 @@ +/* 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 ``, ``, `` and `
` elements. + +use html5ever::{local_name, namespace_url, ns}; +use malloc_size_of::malloc_size_of_is_0; +use net_traits::request::Referrer; +use script_traits::{HistoryEntryReplacement, LoadData, LoadOrigin}; +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::htmlanchorelement::HTMLAnchorElement; +use crate::dom::htmlareaelement::HTMLAreaElement; +use crate::dom::htmlformelement::HTMLFormElement; +use crate::dom::htmllinkelement::HTMLLinkElement; +use crate::dom::node::document_from_node; +use crate::dom::types::{Element, GlobalScope}; +use crate::task_source::TaskSource; + +bitflags::bitflags! { + /// Describes the different relations that can be specified on elements using the `rel` + /// attribute. + /// + /// Refer to for more information. + #[derive(Clone, Copy, Debug)] + pub struct LinkRelations: u32 { + /// + const ALTERNATE = 1; + + /// + const AUTHOR = 1 << 1; + + /// + const BOOKMARK = 1 << 2; + + /// + const CANONICAL = 1 << 3; + + /// + const DNS_PREFETCH = 1 << 4; + + /// + const EXPECT = 1 << 5; + + /// + const EXTERNAL = 1 << 6; + + /// + const HELP = 1 << 7; + + /// + const ICON = 1 << 8; + + /// + const LICENSE = 1 << 9; + + /// + const NEXT = 1 << 10; + + /// + const MANIFEST = 1 << 11; + + /// + const MODULE_PRELOAD = 1 << 12; + + /// + const NO_FOLLOW = 1 << 13; + + /// + const NO_OPENER = 1 << 14; + + /// + const NO_REFERRER = 1 << 15; + + /// + const OPENER = 1 << 16; + + /// + const PING_BACK = 1 << 17; + + /// + const PRECONNECT = 1 << 18; + + /// + const PREFETCH = 1 << 19; + + /// + const PRELOAD = 1 << 20; + + /// + const PREV = 1 << 21; + + /// + const PRIVACY_POLICY = 1 << 22; + + /// + const SEARCH = 1 << 23; + + /// + const STYLESHEET = 1 << 24; + + /// + const TAG = 1 << 25; + + /// + const TERMS_OF_SERVICE = 1 << 26; + } +} + +impl LinkRelations { + /// The set of allowed relations for [``] elements + /// + /// [``]: https://html.spec.whatwg.org/multipage/#htmllinkelement + pub 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 [``] and [``] elements + /// + /// [``]: https://html.spec.whatwg.org/multipage/#the-a-element + /// [``]: https://html.spec.whatwg.org/multipage/#the-area-element + pub 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 [``] elements + /// + /// [``]: https://html.spec.whatwg.org/multipage/#the-form-element + pub 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 [``], [``], [``] and [``] elements. + /// + /// [``]: https://html.spec.whatwg.org/multipage/#htmllinkelement + /// [``]: https://html.spec.whatwg.org/multipage/#the-a-element + /// [``]: https://html.spec.whatwg.org/multipage/#the-area-element + /// [``]: https://html.spec.whatwg.org/multipage/#the-form-element + pub 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::() { + Self::ALLOWED_LINK_RELATIONS + } else if element.is::() || element.is::() { + Self::ALLOWED_ANCHOR_OR_AREA_RELATIONS + } else if element.is::() { + 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() + } + } + + /// + pub 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); + +/// +pub fn get_element_target(subject: &Element) -> Option { + if !(subject.is::() || + subject.is::() || + subject.is::()) + { + return None; + } + if subject.has_attribute(&local_name!("target")) { + return Some(subject.get_string_attribute(&local_name!("target"))); + } + + let doc = document_from_node(subject).base_element(); + match doc { + Some(doc) => { + let element = doc.upcast::(); + if element.has_attribute(&local_name!("target")) { + Some(element.get_string_attribute(&local_name!("target"))) + } else { + None + } + }, + None => None, + } +} + +/// +pub fn follow_hyperlink( + subject: &Element, + relations: LinkRelations, + hyperlink_suffix: Option, +) { + // Step 1. If subject cannot navigate, then return. + if subject.cannot_navigate() { + return; + } + // Step 2, done in Step 7. + + let document = document_from_node(subject); + let window = document.window(); + + // Step 3: source browsing context. + let source = document.browsing_context().unwrap(); + + // Step 4-5: target attribute. + let target_attribute_value = + if subject.is::() || subject.is::() { + get_element_target(subject) + } else { + None + }; + + // Step 6. + let noopener = relations.get_element_noopener(target_attribute_value.as_ref()); + + // Step 7. + let (maybe_chosen, replace) = match target_attribute_value { + Some(name) => { + let (maybe_chosen, new) = source.choose_browsing_context(name, noopener); + let replace = if new { + HistoryEntryReplacement::Enabled + } else { + HistoryEntryReplacement::Disabled + }; + (maybe_chosen, replace) + }, + None => ( + Some(window.window_proxy()), + HistoryEntryReplacement::Disabled, + ), + }; + + // Step 8. + 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, dis-owning target's opener, if necessary + // will have been done as part of Step 7 above + // in choose_browsing_context/create_auxiliary_browsing_context. + + // Step 10, 11. TODO: if parsing the URL failed, navigate to error page. + + let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap(); + let mut href = attribute.Value(); + + // Step 11: append a hyperlink suffix. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28925 + if let Some(suffix) = hyperlink_suffix { + href.push_str(&suffix); + } + let Ok(url) = document.base_url().join(&href) else { + return; + }; + + // Step 12. + let referrer_policy = referrer_policy_for_element(subject); + + // Step 13 + let referrer = if relations.contains(LinkRelations::NO_REFERRER) { + Referrer::NoReferrer + } else { + target_window.upcast::().get_referrer() + }; + + // Step 14 + let pipeline_id = target_window.upcast::().pipeline_id(); + let secure = target_window.upcast::().is_secure_context(); + let load_data = LoadData::new( + LoadOrigin::Script(document.origin().immutable().clone()), + url, + Some(pipeline_id), + referrer, + referrer_policy, + Some(secure), + ); + let target = Trusted::new(target_window); + let task = task!(navigate_follow_hyperlink: move || { + debug!("following hyperlink to {}", load_data.url); + target.root().load_url(replace, false, load_data); + }); + target_window + .task_manager() + .dom_manipulation_task_source() + .queue(task, target_window.upcast()) + .unwrap(); + }; +}