1use 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 #[derive(Clone, Copy, Debug, PartialEq)]
33 pub(crate) struct LinkRelations: u32 {
34 const ALTERNATE = 1;
36
37 const AUTHOR = 1 << 1;
39
40 const BOOKMARK = 1 << 2;
42
43 const CANONICAL = 1 << 3;
45
46 const DNS_PREFETCH = 1 << 4;
48
49 const EXPECT = 1 << 5;
51
52 const EXTERNAL = 1 << 6;
54
55 const HELP = 1 << 7;
57
58 const ICON = 1 << 8;
60
61 const LICENSE = 1 << 9;
63
64 const NEXT = 1 << 10;
66
67 const MANIFEST = 1 << 11;
69
70 const MODULE_PRELOAD = 1 << 12;
72
73 const NO_FOLLOW = 1 << 13;
75
76 const NO_OPENER = 1 << 14;
78
79 const NO_REFERRER = 1 << 15;
81
82 const OPENER = 1 << 16;
84
85 const PING_BACK = 1 << 17;
87
88 const PRECONNECT = 1 << 18;
90
91 const PREFETCH = 1 << 19;
93
94 const PRELOAD = 1 << 20;
96
97 const PREV = 1 << 21;
99
100 const PRIVACY_POLICY = 1 << 22;
102
103 const SEARCH = 1 << 23;
105
106 const STYLESHEET = 1 << 24;
108
109 const TAG = 1 << 25;
111
112 const TERMS_OF_SERVICE = 1 << 26;
114 }
115}
116
117impl LinkRelations {
118 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 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 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 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 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 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 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 pub(crate) fn get_element_noopener(&self, target_attribute_value: Option<&DOMString>) -> bool {
294 if self.contains(Self::NO_OPENER) || self.contains(Self::NO_REFERRER) {
296 return true;
297 }
298
299 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 false
309 }
310}
311
312malloc_size_of_is_0!(LinkRelations);
313
314fn valid_navigable_target_name(target: &DOMString) -> bool {
316 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
331pub(crate) fn valid_navigable_target_name_or_keyword(target: &DOMString) -> bool {
333 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
342pub(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 let target = target.or_else(|| {
355 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 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 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 target
387}
388
389pub(crate) fn follow_hyperlink(
391 cx: &mut JSContext,
392 subject: &Element,
393 relations: LinkRelations,
394 hyperlink_suffix: Option<String>,
395) {
396 if subject.cannot_navigate() {
398 return;
399 }
400
401 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 let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
432
433 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 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 let attribute = subject.get_attribute(&local_name!("href")).unwrap();
463 let mut href = attribute.Value();
464
465 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 let referrer_policy = referrer_policy_for_element(subject);
475
476 let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
479 Referrer::NoReferrer
480 } else {
481 target_window.as_global_scope().get_referrer()
482 };
483
484 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}