1use markup5ever::{LocalName, local_name};
2
3use crate::{
4 BaseDocument, ElementData,
5 traversal::{AncestorTraverser, TreeTraverser},
6};
7use blitz_traits::{
8 navigation::NavigationOptions,
9 net::{Body, Entry, EntryValue, FormData, Method},
10};
11use core::str::FromStr;
12use std::fmt::Display;
13
14const DEFAULT_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS
16 .add(b' ')
18 .add(b'"')
19 .add(b'#')
20 .add(b'<')
21 .add(b'>')
22 .add(b'?')
24 .add(b'`')
25 .add(b'{')
26 .add(b'}');
27
28impl BaseDocument {
29 pub fn reset_form_owner(&mut self, node_id: usize) {
37 let node = &self.nodes[node_id];
38 let Some(element) = node.element_data() else {
39 return;
40 };
41
42 let final_owner_id = element
44 .attr(local_name!("form"))
45 .and_then(|owner| self.nodes_to_id.get(owner))
46 .copied()
47 .filter(|owner_id| {
48 self.get_node(*owner_id)
49 .is_some_and(|node| node.data.is_element_with_tag_name(&local_name!("form")))
50 })
51 .or_else(|| {
52 AncestorTraverser::new(self, node_id).find(|ancestor_id| {
53 self.nodes[*ancestor_id]
54 .data
55 .is_element_with_tag_name(&local_name!("form"))
56 })
57 });
58
59 if let Some(final_owner_id) = final_owner_id {
60 self.controls_to_form.insert(node_id, final_owner_id);
61 }
62 }
63
64 pub fn submit_form(&self, node_id: usize, submitter_id: usize) {
72 let node = &self.nodes[node_id];
73 let Some(element) = node.element_data() else {
74 return;
75 };
76
77 let entry = construct_entry_list(self, node_id, submitter_id);
78
79 let method = get_form_attr(
80 self,
81 element,
82 local_name!("method"),
83 submitter_id,
84 local_name!("formmethod"),
85 )
86 .and_then(|method| method.parse::<FormMethod>().ok())
87 .unwrap_or(FormMethod::Get);
88
89 let action = get_form_attr(
90 self,
91 element,
92 local_name!("action"),
93 submitter_id,
94 local_name!("formaction"),
95 )
96 .unwrap_or_default();
97
98 let mut parsed_action = self.resolve_url(action);
99
100 let scheme = parsed_action.scheme();
101
102 let enctype = get_form_attr(
103 self,
104 element,
105 local_name!("enctype"),
106 submitter_id,
107 local_name!("formenctype"),
108 )
109 .and_then(|enctype| enctype.parse::<RequestContentType>().ok())
110 .unwrap_or(RequestContentType::FormUrlEncoded);
111
112 let mut post_resource = Body::Empty;
113
114 match (scheme, method) {
115 ("http" | "https" | "data", FormMethod::Get) => {
116 let pairs = convert_to_list_of_name_value_pairs(entry);
117 let mut query = String::new();
118 url::form_urlencoded::Serializer::new(&mut query).extend_pairs(pairs);
119 parsed_action.set_query(Some(&query));
120 }
121 ("http" | "https", FormMethod::Post) => post_resource = Body::Form(entry),
122 ("mailto", FormMethod::Get) => {
123 let pairs = convert_to_list_of_name_value_pairs(entry);
124 parsed_action.query_pairs_mut().extend_pairs(pairs);
125 }
126 ("mailto", FormMethod::Post) => {
127 let pairs = convert_to_list_of_name_value_pairs(entry);
128 let body = match enctype {
129 RequestContentType::TextPlain => {
130 let body = encode_text_plain(&pairs);
131 percent_encoding::utf8_percent_encode(&body, &DEFAULT_ENCODE_SET)
132 .to_string()
133 }
134 _ => {
135 let mut body = String::new();
136 url::form_urlencoded::Serializer::new(&mut body).extend_pairs(pairs);
137 body
138 }
139 };
140 let mut query = if let Some(query) = parsed_action.query() {
141 let mut query = query.to_string();
142 query.push('&');
143 query
144 } else {
145 String::new()
146 };
147 query.push_str("body=");
148 query.push_str(&body);
149 parsed_action.set_query(Some(&query));
150 }
151 _ => {
152 #[cfg(feature = "tracing")]
153 tracing::warn!(
154 "Scheme {} with method {:?} is not implemented",
155 scheme,
156 method
157 );
158 return;
159 }
160 }
161
162 let method = method.try_into().unwrap_or_default();
163
164 let navigation_options =
165 NavigationOptions::new(parsed_action, enctype.to_string(), self.id())
166 .set_document_resource(post_resource)
167 .set_method(method);
168
169 self.navigation_provider.navigate_to(navigation_options)
170 }
171}
172
173fn construct_entry_list(doc: &BaseDocument, form_id: usize, submitter_id: usize) -> FormData {
185 let mut entry_list = FormData::new();
186
187 let mut create_entry = |name: &str, value: EntryValue| {
188 entry_list.0.push(Entry {
189 name: name.to_string(),
190 value,
191 });
192 };
193
194 fn datalist_ancestor(doc: &BaseDocument, node_id: usize) -> bool {
195 AncestorTraverser::new(doc, node_id).any(|node_id| {
196 doc.nodes[node_id]
197 .data
198 .is_element_with_tag_name(&local_name!("datalist"))
199 })
200 }
201
202 for control_id in TreeTraverser::new(doc) {
204 let Some(node) = doc.get_node(control_id) else {
205 continue;
206 };
207 let Some(element) = node.element_data() else {
208 continue;
209 };
210
211 if doc
213 .controls_to_form
214 .get(&control_id)
215 .map(|owner_id| *owner_id != form_id)
216 .unwrap_or(true)
217 {
218 continue;
219 }
220
221 let element_type = element.attr(local_name!("type"));
222
223 if datalist_ancestor(doc, node.id)
231 || element.attr(local_name!("disabled")).is_some()
232 || (element.name.local == local_name!("button") && node.id != submitter_id)
233 || element.name.local == local_name!("input")
234 && ((matches!(element_type, Some("checkbox" | "radio"))
235 && !element.checkbox_input_checked().unwrap_or(false))
236 || matches!(element_type, Some("submit" | "button")))
237 {
238 continue;
239 }
240
241 if element_type == Some("image") {
243 if node.id != submitter_id {
245 continue;
246 }
247 continue;
255 }
256
257 let Some(name) = element
264 .attr(local_name!("name"))
265 .filter(|str| !str.is_empty())
266 else {
267 continue;
268 };
269
270 if element.name.local == local_name!("input")
278 && matches!(element_type, Some("checkbox" | "radio"))
279 {
280 let value = element.attr(local_name!("value")).unwrap_or("on");
282 create_entry(name, value.into());
284 continue;
285 }
286 #[cfg(feature = "file_input")]
288 if element.name.local == local_name!("input") && matches!(element_type, Some("file")) {
289 let Some(files) = element.file_data() else {
291 create_entry(name, EntryValue::EmptyFile);
292 continue;
293 };
294 if files.is_empty() {
295 create_entry(name, EntryValue::EmptyFile);
296 }
297 else {
299 for path_buf in files.iter() {
300 create_entry(name, path_buf.clone().into());
301 }
302 }
303 continue;
304 }
305 if element.name.local == local_name!("input")
307 && element_type == Some("hidden")
308 && name.eq_ignore_ascii_case("_charset_")
309 {
310 let charset = "UTF-8"; create_entry(name, charset.into());
314 }
315 else if let Some(text) = element.text_input_data() {
317 create_entry(name, text.editor.text().to_string().as_str().into());
318 } else if let Some(value) = element.attr(local_name!("value")) {
319 create_entry(name, value.into());
320 }
321 }
322 entry_list
323}
324
325fn get_form_attr<'a>(
326 doc: &'a BaseDocument,
327 form: &'a ElementData,
328 form_local: impl PartialEq<LocalName>,
329 submitter_id: usize,
330 submitter_local: impl PartialEq<LocalName>,
331) -> Option<&'a str> {
332 get_submitter_attr(doc, submitter_id, submitter_local).or_else(|| form.attr(form_local))
333}
334
335fn get_submitter_attr(
336 doc: &BaseDocument,
337 submitter_id: usize,
338 local_name: impl PartialEq<LocalName>,
339) -> Option<&str> {
340 doc.get_node(submitter_id)
341 .and_then(|node| node.element_data())
342 .and_then(|element_data| {
343 if element_data.name.local == local_name!("button")
344 && element_data.attr(local_name!("type")) == Some("submit")
345 {
346 element_data.attr(local_name)
347 } else {
348 None
349 }
350 })
351}
352
353#[derive(Debug, Copy, Clone, PartialEq, Eq)]
354enum FormMethod {
355 Get,
356 Post,
357 Dialog,
358}
359impl FromStr for FormMethod {
360 type Err = ();
361 fn from_str(s: &str) -> Result<Self, Self::Err> {
362 Ok(match s.to_lowercase().as_str() {
363 "get" => FormMethod::Get,
364 "post" => FormMethod::Post,
365 "dialog" => FormMethod::Dialog,
366 _ => return Err(()),
367 })
368 }
369}
370impl TryFrom<FormMethod> for Method {
371 type Error = &'static str;
372 fn try_from(method: FormMethod) -> Result<Self, Self::Error> {
373 Ok(match method {
374 FormMethod::Get => Method::GET,
375 FormMethod::Post => Method::POST,
376 FormMethod::Dialog => return Err("Dialog is not an HTTP method"),
377 })
378 }
379}
380#[derive(Debug, Clone)]
382pub enum RequestContentType {
383 FormUrlEncoded,
385 MultipartFormData,
387 TextPlain,
389}
390
391impl FromStr for RequestContentType {
392 type Err = ();
393 fn from_str(s: &str) -> Result<Self, Self::Err> {
394 Ok(match s {
395 "application/x-www-form-urlencoded" => RequestContentType::FormUrlEncoded,
396 "multipart/form-data" => RequestContentType::MultipartFormData,
397 "text/plain" => RequestContentType::TextPlain,
398 _ => return Err(()),
399 })
400 }
401}
402
403impl Display for RequestContentType {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 match self {
406 RequestContentType::FormUrlEncoded => write!(f, "application/x-www-form-urlencoded"),
407 RequestContentType::MultipartFormData => write!(f, "multipart/form-data"),
408 RequestContentType::TextPlain => write!(f, "text/plain"),
409 }
410 }
411}
412
413fn convert_to_list_of_name_value_pairs(form_data: FormData) -> Vec<(String, String)> {
416 form_data
417 .iter()
418 .map(|Entry { name, value }| {
419 let name = normalize_line_endings(name.as_ref());
420 let value = normalize_line_endings(value.as_ref());
421 (name, value)
422 })
423 .collect()
424}
425
426fn normalize_line_endings(input: &str) -> String {
429 let mut result = String::with_capacity(input.len());
434 let mut chars = input.chars().peekable();
435
436 while let Some(current) = chars.next() {
437 match (current, chars.peek()) {
438 ('\r', Some('\n')) => {
439 result.push_str("\r\n");
440 chars.next();
441 }
442 ('\r' | '\n', _) => {
443 result.push_str("\r\n");
444 }
445 _ => result.push(current),
446 }
447 }
448
449 result
450}
451
452fn encode_text_plain<T: AsRef<str>, U: AsRef<str>>(input: &[(T, U)]) -> String {
455 let mut out = String::new();
456 for (name, value) in input {
457 out.push_str(name.as_ref());
458 out.push('=');
459 out.push_str(value.as_ref());
460 out.push_str("\r\n");
461 }
462 out
463}