1use markup5ever::{LocalName, local_name};
2
3use crate::{
4 BaseDocument, ElementData,
5 traversal::{AncestorTraverser, TreeTraverser},
6};
7use blitz_traits::navigation::NavigationOptions;
8use core::str::FromStr;
9use std::fmt::Display;
10
11impl BaseDocument {
12 pub fn reset_form_owner(&mut self, node_id: usize) {
20 let node = &self.nodes[node_id];
21 let Some(element) = node.element_data() else {
22 return;
23 };
24
25 let final_owner_id = element
27 .attr(local_name!("form"))
28 .and_then(|owner| self.nodes_to_id.get(owner))
29 .copied()
30 .filter(|owner_id| {
31 self.get_node(*owner_id)
32 .is_some_and(|node| node.data.is_element_with_tag_name(&local_name!("form")))
33 })
34 .or_else(|| {
35 AncestorTraverser::new(self, node_id).find(|ancestor_id| {
36 self.nodes[*ancestor_id]
37 .data
38 .is_element_with_tag_name(&local_name!("form"))
39 })
40 });
41
42 if let Some(final_owner_id) = final_owner_id {
43 self.controls_to_form.insert(node_id, final_owner_id);
44 }
45 }
46
47 pub fn submit_form(&self, node_id: usize, submitter_id: usize) {
55 let node = &self.nodes[node_id];
56 let Some(element) = node.element_data() else {
57 return;
58 };
59
60 let entry = construct_entry_list(self, node_id, submitter_id);
61
62 let method = get_form_attr(
63 self,
64 element,
65 local_name!("method"),
66 submitter_id,
67 local_name!("formmethod"),
68 )
69 .and_then(|method| method.parse::<FormMethod>().ok())
70 .unwrap_or(FormMethod::Get);
71
72 let action = get_form_attr(
73 self,
74 element,
75 local_name!("action"),
76 submitter_id,
77 local_name!("formaction"),
78 )
79 .unwrap_or_default();
80
81 let mut parsed_action = self.resolve_url(action);
82
83 let scheme = parsed_action.scheme();
84
85 let enctype = get_form_attr(
86 self,
87 element,
88 local_name!("enctype"),
89 submitter_id,
90 local_name!("formenctype"),
91 )
92 .and_then(|enctype| enctype.parse::<RequestContentType>().ok())
93 .unwrap_or(RequestContentType::FormUrlEncoded);
94
95 let mut post_resource = None;
96
97 match (scheme, method) {
98 ("http" | "https" | "data", FormMethod::Get) => {
99 let pairs = entry.convert_to_list_of_name_value_pairs();
100
101 let mut query = String::new();
102 url::form_urlencoded::Serializer::new(&mut query).extend_pairs(pairs);
103
104 parsed_action.set_query(Some(&query));
105 }
106
107 ("http" | "https", FormMethod::Post) => match enctype {
108 RequestContentType::FormUrlEncoded => {
109 let pairs = entry.convert_to_list_of_name_value_pairs();
110 let mut body = String::new();
111 url::form_urlencoded::Serializer::new(&mut body).extend_pairs(pairs);
112 post_resource = Some(body.into());
113 }
114 RequestContentType::MultipartFormData => {
115 #[cfg(feature = "tracing")]
116 tracing::warn!("Multipart Forms are currently not supported");
117 return;
118 }
119 RequestContentType::TextPlain => {
120 let pairs = entry.convert_to_list_of_name_value_pairs();
121 let body = encode_text_plain(&pairs).into();
122 post_resource = Some(body);
123 }
124 },
125 ("mailto", FormMethod::Get) => {
126 let pairs = entry.convert_to_list_of_name_value_pairs();
127
128 parsed_action.query_pairs_mut().extend_pairs(pairs);
129 }
130 ("mailto", FormMethod::Post) => {
131 let pairs = entry.convert_to_list_of_name_value_pairs();
132 let body = match enctype {
133 RequestContentType::TextPlain => {
134 let body = encode_text_plain(&pairs);
135
136 const DEFAULT_ENCODE_SET: percent_encoding::AsciiSet =
138 percent_encoding::CONTROLS
139 .add(b' ')
141 .add(b'"')
142 .add(b'#')
143 .add(b'<')
144 .add(b'>')
145 .add(b'?')
147 .add(b'`')
148 .add(b'{')
149 .add(b'}');
150
151 percent_encoding::utf8_percent_encode(&body, &DEFAULT_ENCODE_SET)
153 .to_string()
154 }
155 _ => {
156 let mut body = String::new();
157 url::form_urlencoded::Serializer::new(&mut body).extend_pairs(pairs);
158 body
159 }
160 };
161 let mut query = if let Some(query) = parsed_action.query() {
162 let mut query = query.to_string();
163 query.push('&');
164 query
165 } else {
166 String::new()
167 };
168 query.push_str("body=");
169 query.push_str(&body);
170 parsed_action.set_query(Some(&query));
171 }
172 _ => {
173 #[cfg(feature = "tracing")]
174 tracing::warn!(
175 "Scheme {} with method {:?} is not implemented",
176 scheme,
177 method
178 );
179 return;
180 }
181 }
182
183 let navigation_options =
184 NavigationOptions::new(parsed_action, enctype.to_string(), self.id())
185 .set_document_resource(post_resource);
186
187 self.navigation_provider.navigate_to(navigation_options)
188 }
189}
190
191fn construct_entry_list(doc: &BaseDocument, form_id: usize, submitter_id: usize) -> EntryList {
203 let mut entry_list = EntryList::new();
204
205 let mut create_entry = |name: &str, value: &str| {
206 entry_list.0.push(Entry::new(name, value));
207 };
208
209 fn datalist_ancestor(doc: &BaseDocument, node_id: usize) -> bool {
210 AncestorTraverser::new(doc, node_id).any(|node_id| {
211 doc.nodes[node_id]
212 .data
213 .is_element_with_tag_name(&local_name!("datalist"))
214 })
215 }
216
217 for control_id in TreeTraverser::new(doc) {
218 let Some(node) = doc.get_node(control_id) else {
219 continue;
220 };
221 let Some(element) = node.element_data() else {
222 continue;
223 };
224
225 if doc
227 .controls_to_form
228 .get(&control_id)
229 .map(|owner_id| *owner_id != form_id)
230 .unwrap_or(true)
231 {
232 continue;
233 }
234
235 let element_type = element.attr(local_name!("type"));
236
237 if datalist_ancestor(doc, node.id)
245 || element.attr(local_name!("disabled")).is_some()
246 || (element.name.local == local_name!("button") && node.id != submitter_id)
247 || element.name.local == local_name!("input")
248 && ((matches!(element_type, Some("checkbox" | "radio"))
249 && !element.checkbox_input_checked().unwrap_or(false))
250 || matches!(element_type, Some("submit" | "button")))
251 {
252 continue;
253 }
254
255 if element_type == Some("image") {
257 if node.id != submitter_id {
259 continue;
260 }
261 continue;
269 }
270
271 let Some(name) = element
278 .attr(local_name!("name"))
279 .filter(|str| !str.is_empty())
280 else {
281 continue;
282 };
283
284 if element.name.local == local_name!("input")
292 && matches!(element_type, Some("checkbox" | "radio"))
293 {
294 let value = element.attr(local_name!("value")).unwrap_or("on");
296 create_entry(name, value);
298 }
299 else if element.name.local == local_name!("input")
306 && element_type == Some("hidden")
307 && name.eq_ignore_ascii_case("_charset_")
308 {
309 let charset = "UTF-8"; create_entry(name, charset);
313 }
314 else if let Some(text) = element.text_input_data() {
316 create_entry(name, &text.editor.text().to_string());
317 } else if let Some(value) = element.attr(local_name!("value")) {
318 create_entry(name, value);
319 }
320 }
321 entry_list
322}
323
324fn normalize_line_endings(input: &str) -> String {
334 let mut result = String::with_capacity(input.len());
339 let mut chars = input.chars().peekable();
340
341 while let Some(current) = chars.next() {
342 match (current, chars.peek()) {
343 ('\r', Some('\n')) => {
344 result.push_str("\r\n");
345 chars.next();
346 }
347 ('\r' | '\n', _) => {
348 result.push_str("\r\n");
349 }
350 _ => result.push(current),
351 }
352 }
353
354 result
355}
356
357fn get_form_attr<'a>(
358 doc: &'a BaseDocument,
359 form: &'a ElementData,
360 form_local: impl PartialEq<LocalName>,
361 submitter_id: usize,
362 submitter_local: impl PartialEq<LocalName>,
363) -> Option<&'a str> {
364 get_submitter_attr(doc, submitter_id, submitter_local).or_else(|| form.attr(form_local))
365}
366
367fn get_submitter_attr(
368 doc: &BaseDocument,
369 submitter_id: usize,
370 local_name: impl PartialEq<LocalName>,
371) -> Option<&str> {
372 doc.get_node(submitter_id)
373 .and_then(|node| node.element_data())
374 .and_then(|element_data| {
375 if element_data.name.local == local_name!("button")
376 && element_data.attr(local_name!("type")) == Some("submit")
377 {
378 element_data.attr(local_name)
379 } else {
380 None
381 }
382 })
383}
384fn encode_text_plain(input: &[(String, String)]) -> String {
394 let mut out = String::new();
395 for (name, value) in input {
396 out.push_str(name);
397 out.push('=');
398 out.push_str(value);
399 out.push_str("\r\n");
400 }
401 out
402}
403
404#[derive(Debug, Copy, Clone, PartialEq, Eq)]
405enum FormMethod {
406 Get,
407 Post,
408 Dialog,
409}
410impl FromStr for FormMethod {
411 type Err = ();
412 fn from_str(s: &str) -> Result<Self, Self::Err> {
413 Ok(match s.to_lowercase().as_str() {
414 "get" => FormMethod::Get,
415 "post" => FormMethod::Post,
416 "dialog" => FormMethod::Dialog,
417 _ => return Err(()),
418 })
419 }
420}
421
422#[derive(Debug, Clone)]
424pub enum RequestContentType {
425 FormUrlEncoded,
427 MultipartFormData,
429 TextPlain,
431}
432
433impl FromStr for RequestContentType {
434 type Err = ();
435 fn from_str(s: &str) -> Result<Self, Self::Err> {
436 Ok(match s {
437 "application/x-www-form-urlencoded" => RequestContentType::FormUrlEncoded,
438 "multipart/form-data" => RequestContentType::MultipartFormData,
439 "text/plain" => RequestContentType::TextPlain,
440 _ => return Err(()),
441 })
442 }
443}
444
445impl Display for RequestContentType {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 match self {
448 RequestContentType::FormUrlEncoded => write!(f, "application/x-www-form-urlencoded"),
449 RequestContentType::MultipartFormData => write!(f, "multipart/form-data"),
450 RequestContentType::TextPlain => write!(f, "text/plain"),
451 }
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Default)]
457pub struct EntryList(pub Vec<Entry>);
458impl EntryList {
459 pub fn new() -> Self {
461 EntryList(Vec::new())
462 }
463
464 pub fn convert_to_list_of_name_value_pairs(&self) -> Vec<(String, String)> {
466 self.0
467 .iter()
468 .map(|entry| {
469 let name = normalize_line_endings(&entry.name);
470 let value = normalize_line_endings(&entry.value);
471 (name, value)
472 })
473 .collect()
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Default)]
479pub struct Entry {
480 pub name: String,
481 pub value: String,
482}
483
484impl Entry {
485 pub fn new(name: &str, value: &str) -> Self {
486 Self {
487 name: name.to_string(),
488 value: value.to_string(),
489 }
490 }
491}