| /* 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) 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(), |
| ); |
| 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); |
| }; |
| } |