script/
links.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Defines shared hyperlink behaviour for `<link>`, `<a>`, `<area>` and `<form>` elements.
6
7use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
8use html5ever::{local_name, ns};
9use malloc_size_of::malloc_size_of_is_0;
10use net_traits::request::Referrer;
11use style::str::HTML_SPACE_CHARACTERS;
12
13use crate::dom::bindings::codegen::Bindings::AttrBinding::Attr_Binding::AttrMethods;
14use crate::dom::bindings::inheritance::Castable;
15use crate::dom::bindings::refcounted::Trusted;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::element::referrer_policy_for_element;
18use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
19use crate::dom::html::htmlareaelement::HTMLAreaElement;
20use crate::dom::html::htmlformelement::HTMLFormElement;
21use crate::dom::html::htmllinkelement::HTMLLinkElement;
22use crate::dom::node::NodeTraits;
23use crate::dom::types::Element;
24use crate::script_runtime::CanGc;
25
26bitflags::bitflags! {
27    /// Describes the different relations that can be specified on elements using the `rel`
28    /// attribute.
29    ///
30    /// Refer to <https://html.spec.whatwg.org/multipage/#linkTypes> for more information.
31    #[derive(Clone, Copy, Debug, PartialEq)]
32    pub(crate) struct LinkRelations: u32 {
33        /// <https://html.spec.whatwg.org/multipage/#rel-alternate>
34        const ALTERNATE = 1;
35
36        /// <https://html.spec.whatwg.org/multipage/#link-type-author>
37        const AUTHOR = 1 << 1;
38
39        /// <https://html.spec.whatwg.org/multipage/#link-type-bookmark>
40        const BOOKMARK = 1 << 2;
41
42        /// <https://html.spec.whatwg.org/multipage/#link-type-canonical>
43        const CANONICAL = 1 << 3;
44
45        /// <https://html.spec.whatwg.org/multipage/#link-type-dns-prefetch>
46        const DNS_PREFETCH = 1 << 4;
47
48        /// <https://html.spec.whatwg.org/multipage/#link-type-expect>
49        const EXPECT = 1 << 5;
50
51        /// <https://html.spec.whatwg.org/multipage/#link-type-external>
52        const EXTERNAL = 1 << 6;
53
54        /// <https://html.spec.whatwg.org/multipage/#link-type-help>
55        const HELP = 1 << 7;
56
57        /// <https://html.spec.whatwg.org/multipage/#rel-icon>
58        const ICON = 1 << 8;
59
60        /// <https://html.spec.whatwg.org/multipage/#link-type-license>
61        const LICENSE = 1 << 9;
62
63        /// <https://html.spec.whatwg.org/multipage/#link-type-next>
64        const NEXT = 1 << 10;
65
66        /// <https://html.spec.whatwg.org/multipage/#link-type-manifest>
67        const MANIFEST = 1 << 11;
68
69        /// <https://html.spec.whatwg.org/multipage/#link-type-modulepreload>
70        const MODULE_PRELOAD = 1 << 12;
71
72        /// <https://html.spec.whatwg.org/multipage/#link-type-nofollow>
73        const NO_FOLLOW = 1 << 13;
74
75        /// <https://html.spec.whatwg.org/multipage/#link-type-noopener>
76        const NO_OPENER = 1 << 14;
77
78        /// <https://html.spec.whatwg.org/multipage/#link-type-noreferrer>
79        const NO_REFERRER = 1 << 15;
80
81        /// <https://html.spec.whatwg.org/multipage/#link-type-opener>
82        const OPENER = 1 << 16;
83
84        /// <https://html.spec.whatwg.org/multipage/#link-type-pingback>
85        const PING_BACK = 1 << 17;
86
87        /// <https://html.spec.whatwg.org/multipage/#link-type-preconnect>
88        const PRECONNECT = 1 << 18;
89
90        /// <https://html.spec.whatwg.org/multipage/#link-type-prefetch>
91        const PREFETCH = 1 << 19;
92
93        /// <https://html.spec.whatwg.org/multipage/#link-type-preload>
94        const PRELOAD = 1 << 20;
95
96        /// <https://html.spec.whatwg.org/multipage/#link-type-prev>
97        const PREV = 1 << 21;
98
99        /// <https://html.spec.whatwg.org/multipage/#link-type-privacy-policy>
100        const PRIVACY_POLICY = 1 << 22;
101
102        /// <https://html.spec.whatwg.org/multipage/#link-type-search>
103        const SEARCH = 1 << 23;
104
105        /// <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>
106        const STYLESHEET = 1 << 24;
107
108        /// <https://html.spec.whatwg.org/multipage/#link-type-tag>
109        const TAG = 1 << 25;
110
111        /// <https://html.spec.whatwg.org/multipage/#link-type-terms-of-service>
112        const TERMS_OF_SERVICE = 1 << 26;
113    }
114}
115
116impl LinkRelations {
117    /// The set of allowed relations for [`<link>`] elements
118    ///
119    /// [`<link>`]: https://html.spec.whatwg.org/multipage/#htmllinkelement
120    pub(crate) const ALLOWED_LINK_RELATIONS: Self = Self::ALTERNATE
121        .union(Self::CANONICAL)
122        .union(Self::AUTHOR)
123        .union(Self::DNS_PREFETCH)
124        .union(Self::EXPECT)
125        .union(Self::HELP)
126        .union(Self::ICON)
127        .union(Self::MANIFEST)
128        .union(Self::MODULE_PRELOAD)
129        .union(Self::LICENSE)
130        .union(Self::NEXT)
131        .union(Self::PING_BACK)
132        .union(Self::PRECONNECT)
133        .union(Self::PREFETCH)
134        .union(Self::PRELOAD)
135        .union(Self::PREV)
136        .union(Self::PRIVACY_POLICY)
137        .union(Self::SEARCH)
138        .union(Self::STYLESHEET)
139        .union(Self::TERMS_OF_SERVICE);
140
141    /// The set of allowed relations for [`<a>`] and [`<area>`] elements
142    ///
143    /// [`<a>`]: https://html.spec.whatwg.org/multipage/#the-a-element
144    /// [`<area>`]: https://html.spec.whatwg.org/multipage/#the-area-element
145    pub(crate) const ALLOWED_ANCHOR_OR_AREA_RELATIONS: Self = Self::ALTERNATE
146        .union(Self::AUTHOR)
147        .union(Self::BOOKMARK)
148        .union(Self::EXTERNAL)
149        .union(Self::HELP)
150        .union(Self::LICENSE)
151        .union(Self::NEXT)
152        .union(Self::NO_FOLLOW)
153        .union(Self::NO_OPENER)
154        .union(Self::NO_REFERRER)
155        .union(Self::OPENER)
156        .union(Self::PREV)
157        .union(Self::PRIVACY_POLICY)
158        .union(Self::SEARCH)
159        .union(Self::TAG)
160        .union(Self::TERMS_OF_SERVICE);
161
162    /// The set of allowed relations for [`<form>`] elements
163    ///
164    /// [`<form>`]: https://html.spec.whatwg.org/multipage/#the-form-element
165    pub(crate) const ALLOWED_FORM_RELATIONS: Self = Self::EXTERNAL
166        .union(Self::HELP)
167        .union(Self::LICENSE)
168        .union(Self::NEXT)
169        .union(Self::NO_FOLLOW)
170        .union(Self::NO_OPENER)
171        .union(Self::NO_REFERRER)
172        .union(Self::OPENER)
173        .union(Self::PREV)
174        .union(Self::SEARCH);
175
176    /// Compute the set of relations for an element given its `"rel"` attribute
177    ///
178    /// This function should only be used with [`<link>`], [`<a>`], [`<area>`] and [`<form>`] elements.
179    ///
180    /// [`<link>`]: https://html.spec.whatwg.org/multipage/#htmllinkelement
181    /// [`<a>`]: https://html.spec.whatwg.org/multipage/#the-a-element
182    /// [`<area>`]: https://html.spec.whatwg.org/multipage/#the-area-element
183    /// [`<form>`]: https://html.spec.whatwg.org/multipage/#the-form-element
184    pub(crate) fn for_element(element: &Element) -> Self {
185        let rel = element.get_attribute(&ns!(), &local_name!("rel")).map(|e| {
186            let value = e.value();
187            (**value).to_owned()
188        });
189
190        let mut relations = rel
191            .map(|attribute| {
192                attribute
193                    .split(HTML_SPACE_CHARACTERS)
194                    .map(Self::from_single_keyword)
195                    .collect()
196            })
197            .unwrap_or(Self::empty());
198
199        // For historical reasons, "rev=made" is treated as if the "author" relation was specified
200        let has_legacy_author_relation = element
201            .get_attribute(&ns!(), &local_name!("rev"))
202            .is_some_and(|rev| &**rev.value() == "made");
203        if has_legacy_author_relation {
204            relations |= Self::AUTHOR;
205        }
206
207        let allowed_relations = if element.is::<HTMLLinkElement>() {
208            Self::ALLOWED_LINK_RELATIONS
209        } else if element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>() {
210            Self::ALLOWED_ANCHOR_OR_AREA_RELATIONS
211        } else if element.is::<HTMLFormElement>() {
212            Self::ALLOWED_FORM_RELATIONS
213        } else {
214            Self::empty()
215        };
216
217        relations & allowed_relations
218    }
219
220    /// Parse one single link relation keyword
221    ///
222    /// If the keyword is invalid then `Self::empty()` is returned.
223    fn from_single_keyword(keyword: &str) -> Self {
224        if keyword.eq_ignore_ascii_case("alternate") {
225            Self::ALTERNATE
226        } else if keyword.eq_ignore_ascii_case("canonical") {
227            Self::CANONICAL
228        } else if keyword.eq_ignore_ascii_case("author") {
229            Self::AUTHOR
230        } else if keyword.eq_ignore_ascii_case("bookmark") {
231            Self::BOOKMARK
232        } else if keyword.eq_ignore_ascii_case("dns-prefetch") {
233            Self::DNS_PREFETCH
234        } else if keyword.eq_ignore_ascii_case("expect") {
235            Self::EXPECT
236        } else if keyword.eq_ignore_ascii_case("external") {
237            Self::EXTERNAL
238        } else if keyword.eq_ignore_ascii_case("help") {
239            Self::HELP
240        } else if keyword.eq_ignore_ascii_case("icon") ||
241            keyword.eq_ignore_ascii_case("shortcut icon") ||
242            keyword.eq_ignore_ascii_case("apple-touch-icon")
243        {
244            // TODO: "apple-touch-icon" is not in the spec. Where did it come from? Do we need it?
245            //       There is also "apple-touch-icon-precomposed" listed in
246            //       https://github.com/servo/servo/blob/e43e4778421be8ea30db9d5c553780c042161522/components/script/dom/htmllinkelement.rs#L452-L467
247            Self::ICON
248        } else if keyword.eq_ignore_ascii_case("manifest") {
249            Self::MANIFEST
250        } else if keyword.eq_ignore_ascii_case("modulepreload") {
251            Self::MODULE_PRELOAD
252        } else if keyword.eq_ignore_ascii_case("license") ||
253            keyword.eq_ignore_ascii_case("copyright")
254        {
255            Self::LICENSE
256        } else if keyword.eq_ignore_ascii_case("next") {
257            Self::NEXT
258        } else if keyword.eq_ignore_ascii_case("nofollow") {
259            Self::NO_FOLLOW
260        } else if keyword.eq_ignore_ascii_case("noopener") {
261            Self::NO_OPENER
262        } else if keyword.eq_ignore_ascii_case("noreferrer") {
263            Self::NO_REFERRER
264        } else if keyword.eq_ignore_ascii_case("opener") {
265            Self::OPENER
266        } else if keyword.eq_ignore_ascii_case("pingback") {
267            Self::PING_BACK
268        } else if keyword.eq_ignore_ascii_case("preconnect") {
269            Self::PRECONNECT
270        } else if keyword.eq_ignore_ascii_case("prefetch") {
271            Self::PREFETCH
272        } else if keyword.eq_ignore_ascii_case("preload") {
273            Self::PRELOAD
274        } else if keyword.eq_ignore_ascii_case("prev") || keyword.eq_ignore_ascii_case("previous") {
275            Self::PREV
276        } else if keyword.eq_ignore_ascii_case("privacy-policy") {
277            Self::PRIVACY_POLICY
278        } else if keyword.eq_ignore_ascii_case("search") {
279            Self::SEARCH
280        } else if keyword.eq_ignore_ascii_case("stylesheet") {
281            Self::STYLESHEET
282        } else if keyword.eq_ignore_ascii_case("tag") {
283            Self::TAG
284        } else if keyword.eq_ignore_ascii_case("terms-of-service") {
285            Self::TERMS_OF_SERVICE
286        } else {
287            Self::empty()
288        }
289    }
290
291    /// <https://html.spec.whatwg.org/multipage/#get-an-element%27s-noopener>
292    pub(crate) fn get_element_noopener(&self, target_attribute_value: Option<&DOMString>) -> bool {
293        // Step 1. If element's link types include the noopener or noreferrer keyword, then return true.
294        if self.contains(Self::NO_OPENER) || self.contains(Self::NO_REFERRER) {
295            return true;
296        }
297
298        // Step 2. If element's link types do not include the opener keyword and
299        //         target is an ASCII case-insensitive match for "_blank", then return true.
300        let target_is_blank =
301            target_attribute_value.is_some_and(|target| target.to_ascii_lowercase() == "_blank");
302        if !self.contains(Self::OPENER) && target_is_blank {
303            return true;
304        }
305
306        // Step 3. Return false.
307        false
308    }
309}
310
311malloc_size_of_is_0!(LinkRelations);
312
313/// <https://html.spec.whatwg.org/multipage/#valid-navigable-target-name>
314fn valid_navigable_target_name(target: &DOMString) -> bool {
315    // > A valid navigable target name is any string with at least one character that does not contain both
316    // > an ASCII tab or newline and a U+003C (<), and it does not start with a U+005F (_).
317    // > (Names starting with a U+005F (_) are reserved for special keywords.)
318    if target.is_empty() {
319        return false;
320    }
321    if target.contains_tab_or_newline() && target.contains("\u{003C}") {
322        return false;
323    }
324    if target.starts_with('\u{005F}') {
325        return false;
326    }
327    true
328}
329
330/// <https://html.spec.whatwg.org/multipage/#valid-navigable-target-name-or-keyword>
331pub(crate) fn valid_navigable_target_name_or_keyword(target: &DOMString) -> bool {
332    // > A valid navigable target name or keyword is any string that is either a valid navigable target name
333    // > or that is an ASCII case-insensitive match for one of: _blank, _self, _parent, or _top.
334    if valid_navigable_target_name(target) {
335        return true;
336    }
337    let target = target.to_ascii_lowercase();
338    target == "_blank" || target == "_self" || target == "_parent" || target == "_top"
339}
340
341/// <https://html.spec.whatwg.org/multipage/#get-an-element%27s-target>
342pub(crate) fn get_element_target(
343    subject: &Element,
344    target: Option<DOMString>,
345) -> Option<DOMString> {
346    assert!(
347        subject.is::<HTMLAreaElement>() ||
348            subject.is::<HTMLAnchorElement>() ||
349            subject.is::<HTMLFormElement>()
350    );
351
352    // Step 1. If target is null, then:
353    let target = target.or_else(|| {
354        // Step 1.1. If element has a target attribute, then set target to that attribute's value.
355        //
356        // Note that for a target attribute to be valid, it must be a valid navigable target name
357        // or keyword
358        let element_target = subject.get_string_attribute(&local_name!("target"));
359        if valid_navigable_target_name_or_keyword(&element_target) {
360            Some(element_target)
361        } else {
362            // Step 1.2. Otherwise, if element's node document contains a base element with a target attribute,
363            // set target to the value of the target attribute of the first such base element.
364            subject
365                .owner_document()
366                .target_base_element()
367                .and_then(|base_element| {
368                    let element = base_element.upcast::<Element>();
369                    if element.has_attribute(&local_name!("target")) {
370                        Some(element.get_string_attribute(&local_name!("target")))
371                    } else {
372                        None
373                    }
374                })
375        }
376    });
377    // Step 2. If target is not null, and contains an ASCII tab or newline and a U+003C (<), then set target to "_blank".
378    if let Some(ref target) = target {
379        if target.contains_tab_or_newline() && target.contains("\u{003C}") {
380            return Some("_blank".into());
381        }
382    }
383    // Step 3. Return target.
384    target
385}
386
387/// <https://html.spec.whatwg.org/multipage/#following-hyperlinks-2>
388pub(crate) fn follow_hyperlink(
389    subject: &Element,
390    relations: LinkRelations,
391    hyperlink_suffix: Option<String>,
392) {
393    // Step 1: If subject cannot navigate, then return.
394    if subject.cannot_navigate() {
395        return;
396    }
397
398    // Step 2: Let targetAttributeValue be the empty string.
399    // This is done below.
400
401    // Step 3: If subject is an a or area element, then set targetAttributeValue to the
402    //         result of getting an element's target given subject.
403    //
404    // Also allow the user to open links in a new WebView by pressing either the meta or
405    // control key (depending on the platform).
406    let document = subject.owner_document();
407    let target_attribute_value =
408        if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
409            if document
410                .event_handler()
411                .alternate_action_keyboard_modifier_active()
412            {
413                Some("_blank".into())
414            } else {
415                get_element_target(subject, None)
416            }
417        } else {
418            None
419        };
420
421    // Step 4: Let urlRecord be the result of encoding-parsing a URL given subject's href
422    //         attribute value, relative to subject's node document.
423    // Step 5: If urlRecord is failure, then return.
424    // TODO: Implement this.
425
426    // Step 6: Let noopener be the result of getting an element's noopener with subject,
427    //         urlRecord, and targetAttributeValue.
428    let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
429
430    // Step 7: Let targetNavigable be the first return value of applying the rules for
431    //         choosing a navigable given targetAttributeValue, subject's node navigable, and
432    //         noopener.
433    let window = document.window();
434    let source = document.browsing_context().unwrap();
435    let (maybe_chosen, history_handling) = match target_attribute_value {
436        Some(name) => {
437            let (maybe_chosen, new) = source.choose_browsing_context(name, noopener);
438            let history_handling = if new {
439                NavigationHistoryBehavior::Replace
440            } else {
441                NavigationHistoryBehavior::Push
442            };
443            (maybe_chosen, history_handling)
444        },
445        None => (Some(window.window_proxy()), NavigationHistoryBehavior::Push),
446    };
447
448    // Step 8: If targetNavigable is null, then return.
449    let chosen = match maybe_chosen {
450        Some(proxy) => proxy,
451        None => return,
452    };
453
454    if let Some(target_document) = chosen.document() {
455        let target_window = target_document.window();
456        // Step 9: Let urlString be the result of applying the URL serializer to urlRecord.
457        // TODO: Implement this.
458
459        let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap();
460        let mut href = attribute.Value();
461
462        // Step 10: If hyperlinkSuffix is non-null, then append it to urlString.
463        if let Some(suffix) = hyperlink_suffix {
464            href.push_str(&suffix);
465        }
466        let Ok(url) = document.base_url().join(&href.str()) else {
467            return;
468        };
469
470        // Step 11: Let referrerPolicy be the current state of subject's referrerpolicy content attribute.
471        let referrer_policy = referrer_policy_for_element(subject);
472
473        // Step 12: If subject's link types includes the noreferrer keyword, then set
474        //          referrerPolicy to "no-referrer".
475        let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
476            Referrer::NoReferrer
477        } else {
478            target_window.as_global_scope().get_referrer()
479        };
480
481        // Step 13: Navigate targetNavigable to urlString using subject's node document,
482        //          with referrerPolicy set to referrerPolicy, userInvolvement set to
483        //          userInvolvement, and sourceElement set to subject.
484        let pipeline_id = target_window.as_global_scope().pipeline_id();
485        let secure = target_window.as_global_scope().is_secure_context();
486        let load_data = LoadData::new(
487            LoadOrigin::Script(document.origin().snapshot()),
488            url,
489            document.about_base_url(),
490            Some(pipeline_id),
491            referrer,
492            referrer_policy,
493            Some(secure),
494            Some(document.insecure_requests_policy()),
495            document.has_trustworthy_ancestor_origin(),
496            document.creation_sandboxing_flag_set_considering_parent_iframe(),
497        );
498        let target = Trusted::new(target_window);
499        let task = task!(navigate_follow_hyperlink: move |cx| {
500            debug!("following hyperlink to {}", load_data.url);
501            target.root().load_url(history_handling, false, load_data, CanGc::from_cx(cx));
502        });
503        target_document
504            .owner_global()
505            .task_manager()
506            .dom_manipulation_task_source()
507            .queue(task);
508    };
509}