Skip to main content

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