script/dom/html/links/
relations.rs1use 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::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::navigation::navigate;
25
26bitflags::bitflags! {
27 #[derive(Clone, Copy, Debug, PartialEq)]
32 pub(crate) struct LinkRelations: u32 {
33 const ALTERNATE = 1;
35
36 const AUTHOR = 1 << 1;
38
39 const BOOKMARK = 1 << 2;
41
42 const CANONICAL = 1 << 3;
44
45 const DNS_PREFETCH = 1 << 4;
47
48 const EXPECT = 1 << 5;
50
51 const EXTERNAL = 1 << 6;
53
54 const HELP = 1 << 7;
56
57 const ICON = 1 << 8;
59
60 const LICENSE = 1 << 9;
62
63 const NEXT = 1 << 10;
65
66 const MANIFEST = 1 << 11;
68
69 const MODULE_PRELOAD = 1 << 12;
71
72 const NO_FOLLOW = 1 << 13;
74
75 const NO_OPENER = 1 << 14;
77
78 const NO_REFERRER = 1 << 15;
80
81 const OPENER = 1 << 16;
83
84 const PING_BACK = 1 << 17;
86
87 const PRECONNECT = 1 << 18;
89
90 const PREFETCH = 1 << 19;
92
93 const PRELOAD = 1 << 20;
95
96 const PREV = 1 << 21;
98
99 const PRIVACY_POLICY = 1 << 22;
101
102 const SEARCH = 1 << 23;
104
105 const STYLESHEET = 1 << 24;
107
108 const TAG = 1 << 25;
110
111 const TERMS_OF_SERVICE = 1 << 26;
113 }
114}
115
116impl LinkRelations {
117 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 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 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 pub(crate) fn for_element(element: &Element) -> Self {
185 let rel = element.get_attribute_string_value(&local_name!("rel"));
186
187 let mut relations = rel
188 .map(|attribute| {
189 attribute
190 .split(HTML_SPACE_CHARACTERS)
191 .map(Self::from_single_keyword)
192 .collect()
193 })
194 .unwrap_or(Self::empty());
195
196 let has_legacy_author_relation = element
198 .get_attribute_string_value(&local_name!("rev"))
199 .is_some_and(|rev| rev == "made");
200 if has_legacy_author_relation {
201 relations |= Self::AUTHOR;
202 }
203
204 let allowed_relations = if element.is::<HTMLLinkElement>() {
205 Self::ALLOWED_LINK_RELATIONS
206 } else if element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>() {
207 Self::ALLOWED_ANCHOR_OR_AREA_RELATIONS
208 } else if element.is::<HTMLFormElement>() {
209 Self::ALLOWED_FORM_RELATIONS
210 } else {
211 Self::empty()
212 };
213
214 relations & allowed_relations
215 }
216
217 fn from_single_keyword(keyword: &str) -> Self {
221 if keyword.eq_ignore_ascii_case("alternate") {
222 Self::ALTERNATE
223 } else if keyword.eq_ignore_ascii_case("canonical") {
224 Self::CANONICAL
225 } else if keyword.eq_ignore_ascii_case("author") {
226 Self::AUTHOR
227 } else if keyword.eq_ignore_ascii_case("bookmark") {
228 Self::BOOKMARK
229 } else if keyword.eq_ignore_ascii_case("dns-prefetch") {
230 Self::DNS_PREFETCH
231 } else if keyword.eq_ignore_ascii_case("expect") {
232 Self::EXPECT
233 } else if keyword.eq_ignore_ascii_case("external") {
234 Self::EXTERNAL
235 } else if keyword.eq_ignore_ascii_case("help") {
236 Self::HELP
237 } else if keyword.eq_ignore_ascii_case("icon") ||
238 keyword.eq_ignore_ascii_case("shortcut icon") ||
239 keyword.eq_ignore_ascii_case("apple-touch-icon")
240 {
241 Self::ICON
245 } else if keyword.eq_ignore_ascii_case("manifest") {
246 Self::MANIFEST
247 } else if keyword.eq_ignore_ascii_case("modulepreload") {
248 Self::MODULE_PRELOAD
249 } else if keyword.eq_ignore_ascii_case("license") ||
250 keyword.eq_ignore_ascii_case("copyright")
251 {
252 Self::LICENSE
253 } else if keyword.eq_ignore_ascii_case("next") {
254 Self::NEXT
255 } else if keyword.eq_ignore_ascii_case("nofollow") {
256 Self::NO_FOLLOW
257 } else if keyword.eq_ignore_ascii_case("noopener") {
258 Self::NO_OPENER
259 } else if keyword.eq_ignore_ascii_case("noreferrer") {
260 Self::NO_REFERRER
261 } else if keyword.eq_ignore_ascii_case("opener") {
262 Self::OPENER
263 } else if keyword.eq_ignore_ascii_case("pingback") {
264 Self::PING_BACK
265 } else if keyword.eq_ignore_ascii_case("preconnect") {
266 Self::PRECONNECT
267 } else if keyword.eq_ignore_ascii_case("prefetch") {
268 Self::PREFETCH
269 } else if keyword.eq_ignore_ascii_case("preload") {
270 Self::PRELOAD
271 } else if keyword.eq_ignore_ascii_case("prev") || keyword.eq_ignore_ascii_case("previous") {
272 Self::PREV
273 } else if keyword.eq_ignore_ascii_case("privacy-policy") {
274 Self::PRIVACY_POLICY
275 } else if keyword.eq_ignore_ascii_case("search") {
276 Self::SEARCH
277 } else if keyword.eq_ignore_ascii_case("stylesheet") {
278 Self::STYLESHEET
279 } else if keyword.eq_ignore_ascii_case("tag") {
280 Self::TAG
281 } else if keyword.eq_ignore_ascii_case("terms-of-service") {
282 Self::TERMS_OF_SERVICE
283 } else {
284 Self::empty()
285 }
286 }
287
288 pub(crate) fn get_element_noopener(&self, target_attribute_value: Option<&DOMString>) -> bool {
290 if self.contains(Self::NO_OPENER) || self.contains(Self::NO_REFERRER) {
292 return true;
293 }
294
295 let target_is_blank =
298 target_attribute_value.is_some_and(|target| target.eq_ignore_ascii_case("_blank"));
299 if !self.contains(Self::OPENER) && target_is_blank {
300 return true;
301 }
302
303 false
305 }
306}
307
308malloc_size_of_is_0!(LinkRelations);
309
310fn valid_navigable_target_name(target: &DOMString) -> bool {
312 if target.is_empty() {
316 return false;
317 }
318 if target.contains_tab_or_newline() && target.contains("\u{003C}") {
319 return false;
320 }
321 if target.starts_with('\u{005F}') {
322 return false;
323 }
324 true
325}
326
327pub(crate) fn valid_navigable_target_name_or_keyword(target: &DOMString) -> bool {
329 if valid_navigable_target_name(target) {
332 return true;
333 }
334 target.eq_ignore_ascii_case("_blank") ||
335 target.eq_ignore_ascii_case("_self") ||
336 target.eq_ignore_ascii_case("_parent") ||
337 target.eq_ignore_ascii_case("_top")
338}
339
340pub(crate) fn get_element_target(
342 subject: &Element,
343 target: Option<DOMString>,
344) -> Option<DOMString> {
345 assert!(
346 subject.is::<HTMLAreaElement>() ||
347 subject.is::<HTMLAnchorElement>() ||
348 subject.is::<HTMLFormElement>()
349 );
350
351 let target = target.or_else(|| {
353 let element_target = subject.get_string_attribute(&local_name!("target"));
358 if valid_navigable_target_name_or_keyword(&element_target) {
359 Some(element_target)
360 } else {
361 subject
364 .owner_document()
365 .target_base_element()
366 .and_then(|base_element| {
367 let element = base_element.upcast::<Element>();
368 if element.has_attribute(&local_name!("target")) {
369 Some(element.get_string_attribute(&local_name!("target")))
370 } else {
371 None
372 }
373 })
374 }
375 });
376 if let Some(ref target) = target &&
378 target.contains_tab_or_newline() &&
379 target.contains("\u{003C}")
380 {
381 return Some(DOMString::from_static("_blank"));
382 }
383 target
385}
386
387pub(crate) fn follow_hyperlink(
389 cx: &mut JSContext,
390 subject: &Element,
391 relations: LinkRelations,
392 hyperlink_suffix: Option<String>,
393) {
394 if subject.cannot_navigate() {
396 return;
397 }
398
399 let document = subject.owner_document();
408 let target_attribute_value =
409 if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
410 if document
411 .event_handler()
412 .alternate_action_keyboard_modifier_active()
413 {
414 Some(DOMString::from_static("_blank"))
415 } else {
416 get_element_target(subject, None)
417 }
418 } else {
419 None
420 };
421
422 let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
430
431 let window = document.window();
435
436 let Some(source) = document.browsing_context() else {
437 return;
438 };
439 let (maybe_chosen, history_handling) = match target_attribute_value {
440 Some(name) => {
441 let (maybe_chosen, new) = source.choose_a_navigable(cx, name, noopener);
442 let history_handling = if new {
443 NavigationHistoryBehavior::Replace
444 } else {
445 NavigationHistoryBehavior::Push
446 };
447 (maybe_chosen, history_handling)
448 },
449 None => (Some(source), NavigationHistoryBehavior::Push),
450 };
451
452 let chosen = match maybe_chosen {
454 Some(proxy) => proxy,
455 None => return,
456 };
457
458 if let Some(target_document) = chosen.document() {
459 let target_window = target_document.window();
460 let mut href = subject
464 .get_attribute_string_value(&local_name!("href"))
465 .unwrap();
466
467 if let Some(suffix) = hyperlink_suffix {
469 href.push_str(&suffix);
470 }
471 let Ok(url) = document.encoding_parse_a_url(&href) else {
472 return;
473 };
474
475 let referrer_policy = referrer_policy_for_element(subject);
477
478 let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
481 Referrer::NoReferrer
482 } else {
483 target_window.as_global_scope().get_referrer()
484 };
485
486 let secure = target_window.as_global_scope().is_secure_context();
490 let load_data = LoadData::new(
491 LoadOrigin::Script(document.origin().snapshot()),
492 url,
493 document.about_base_url(),
494 Some(window.pipeline_id()),
495 referrer,
496 referrer_policy,
497 Some(secure),
498 Some(document.insecure_requests_policy()),
499 document.has_trustworthy_ancestor_origin(),
500 document.creation_sandboxing_flag_set_considering_parent_iframe(),
501 );
502 let target = Trusted::new(target_window);
503 let task = task!(navigate_follow_hyperlink: move |cx| {
504 debug!("following hyperlink to {}", load_data.url);
505 navigate(cx, &target.root(), history_handling, false, load_data);
506 });
507 target_document
508 .owner_global()
509 .task_manager()
510 .dom_manipulation_task_source()
511 .queue(task);
512 };
513}