1use 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 #[derive(Clone, Copy, Debug)]
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(&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 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 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 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 pub(crate) fn get_element_noopener(&self, target_attribute_value: Option<&DOMString>) -> bool {
293 if self.contains(Self::NO_OPENER) || self.contains(Self::NO_REFERRER) {
295 return true;
296 }
297
298 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 false
308 }
309}
310
311malloc_size_of_is_0!(LinkRelations);
312
313pub(crate) fn get_element_target(subject: &Element) -> Option<DOMString> {
315 if !(subject.is::<HTMLAreaElement>() ||
316 subject.is::<HTMLAnchorElement>() ||
317 subject.is::<HTMLFormElement>())
318 {
319 return None;
320 }
321 if subject.has_attribute(&local_name!("target")) {
322 return Some(subject.get_string_attribute(&local_name!("target")));
323 }
324
325 let doc = subject.owner_document().base_element();
326 match doc {
327 Some(doc) => {
328 let element = doc.upcast::<Element>();
329 if element.has_attribute(&local_name!("target")) {
330 Some(element.get_string_attribute(&local_name!("target")))
331 } else {
332 None
333 }
334 },
335 None => None,
336 }
337}
338
339pub(crate) fn follow_hyperlink(
341 subject: &Element,
342 relations: LinkRelations,
343 hyperlink_suffix: Option<String>,
344) {
345 if subject.cannot_navigate() {
347 return;
348 }
349
350 let document = subject.owner_document();
359 let target_attribute_value =
360 if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
361 if document
362 .event_handler()
363 .alternate_action_keyboard_modifier_active()
364 {
365 Some("_blank".into())
366 } else {
367 get_element_target(subject)
368 }
369 } else {
370 None
371 };
372
373 let noopener = relations.get_element_noopener(target_attribute_value.as_ref());
381
382 let window = document.window();
386 let source = document.browsing_context().unwrap();
387 let (maybe_chosen, history_handling) = match target_attribute_value {
388 Some(name) => {
389 let (maybe_chosen, new) = source.choose_browsing_context(name, noopener);
390 let history_handling = if new {
391 NavigationHistoryBehavior::Replace
392 } else {
393 NavigationHistoryBehavior::Push
394 };
395 (maybe_chosen, history_handling)
396 },
397 None => (Some(window.window_proxy()), NavigationHistoryBehavior::Push),
398 };
399
400 let chosen = match maybe_chosen {
402 Some(proxy) => proxy,
403 None => return,
404 };
405
406 if let Some(target_document) = chosen.document() {
407 let target_window = target_document.window();
408 let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap();
412 let mut href = attribute.Value();
413
414 if let Some(suffix) = hyperlink_suffix {
416 href.push_str(&suffix);
417 }
418 let Ok(url) = document.base_url().join(&href) else {
419 return;
420 };
421
422 let referrer_policy = referrer_policy_for_element(subject);
424
425 let referrer = if relations.contains(LinkRelations::NO_REFERRER) {
428 Referrer::NoReferrer
429 } else {
430 target_window.as_global_scope().get_referrer()
431 };
432
433 let pipeline_id = target_window.as_global_scope().pipeline_id();
437 let secure = target_window.as_global_scope().is_secure_context();
438 let load_data = LoadData::new(
439 LoadOrigin::Script(document.origin().immutable().clone()),
440 url,
441 Some(pipeline_id),
442 referrer,
443 referrer_policy,
444 Some(secure),
445 Some(document.insecure_requests_policy()),
446 document.has_trustworthy_ancestor_origin(),
447 );
448 let target = Trusted::new(target_window);
449 let task = task!(navigate_follow_hyperlink: move || {
450 debug!("following hyperlink to {}", load_data.url);
451 target.root().load_url(history_handling, false, load_data, CanGc::note());
452 });
453 target_document
454 .owner_global()
455 .task_manager()
456 .dom_manipulation_task_source()
457 .queue(task);
458 };
459}