1use markup5ever::{LocalName, local_name};
2
3use crate::{
4 BaseDocument, ElementData,
5 traversal::{AncestorTraverser, TreeTraverser},
6};
7use bliss_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 Some(mut parsed_action) = self.resolve_url(action) else {
99 return;
100 };
101
102 let scheme = parsed_action.scheme();
103
104 let enctype = get_form_attr(
105 self,
106 element,
107 local_name!("enctype"),
108 submitter_id,
109 local_name!("formenctype"),
110 )
111 .and_then(|enctype| enctype.parse::<RequestContentType>().ok())
112 .unwrap_or(RequestContentType::FormUrlEncoded);
113
114 let mut post_resource = Body::Empty;
115
116 match (scheme, method) {
117 ("http" | "https" | "data", FormMethod::Get) => {
118 let pairs = convert_to_list_of_name_value_pairs(entry);
119 let mut query = String::new();
120 url::form_urlencoded::Serializer::new(&mut query).extend_pairs(pairs);
121 parsed_action.set_query(Some(&query));
122 }
123 ("http" | "https", FormMethod::Post) => post_resource = Body::Form(entry),
124 ("mailto", FormMethod::Get) => {
125 let pairs = convert_to_list_of_name_value_pairs(entry);
126 parsed_action.query_pairs_mut().extend_pairs(pairs);
127 }
128 ("mailto", FormMethod::Post) => {
129 let pairs = convert_to_list_of_name_value_pairs(entry);
130 let body = match enctype {
131 RequestContentType::TextPlain => {
132 let body = encode_text_plain(&pairs);
133 percent_encoding::utf8_percent_encode(&body, &DEFAULT_ENCODE_SET)
134 .to_string()
135 }
136 _ => {
137 let mut body = String::new();
138 url::form_urlencoded::Serializer::new(&mut body).extend_pairs(pairs);
139 body
140 }
141 };
142 let mut query = if let Some(query) = parsed_action.query() {
143 let mut query = query.to_string();
144 query.push('&');
145 query
146 } else {
147 String::new()
148 };
149 query.push_str("body=");
150 query.push_str(&body);
151 parsed_action.set_query(Some(&query));
152 }
153 _ => {
154 #[cfg(feature = "tracing")]
155 tracing::warn!(
156 "Scheme {} with method {:?} is not implemented",
157 scheme,
158 method
159 );
160 return;
161 }
162 }
163
164 let method = method.try_into().unwrap_or_default();
165
166 let navigation_options =
167 NavigationOptions::new(parsed_action, enctype.to_string(), self.id())
168 .set_document_resource(post_resource)
169 .set_method(method);
170
171 self.navigation_provider.navigate_to(navigation_options)
172 }
173}
174
175fn construct_entry_list(doc: &BaseDocument, form_id: usize, submitter_id: usize) -> FormData {
187 let mut entry_list = FormData::new();
188
189 let mut create_entry = |name: &str, value: EntryValue| {
190 entry_list.0.push(Entry {
191 name: name.to_string(),
192 value,
193 });
194 };
195
196 fn datalist_ancestor(doc: &BaseDocument, node_id: usize) -> bool {
197 AncestorTraverser::new(doc, node_id).any(|node_id| {
198 doc.nodes[node_id]
199 .data
200 .is_element_with_tag_name(&local_name!("datalist"))
201 })
202 }
203
204 for control_id in TreeTraverser::new(doc) {
206 let Some(node) = doc.get_node(control_id) else {
207 continue;
208 };
209 let Some(element) = node.element_data() else {
210 continue;
211 };
212
213 if doc
215 .controls_to_form
216 .get(&control_id)
217 .map(|owner_id| *owner_id != form_id)
218 .unwrap_or(true)
219 {
220 continue;
221 }
222
223 let element_type = element.attr(local_name!("type"));
224
225 if datalist_ancestor(doc, node.id)
233 || element.attr(local_name!("disabled")).is_some()
234 || (element.name.local == local_name!("button") && node.id != submitter_id)
235 || element.name.local == local_name!("input")
236 && ((matches!(element_type, Some("checkbox" | "radio"))
237 && !element.checkbox_input_checked().unwrap_or(false))
238 || matches!(element_type, Some("submit" | "button")))
239 {
240 continue;
241 }
242
243 if element_type == Some("image") {
245 if node.id != submitter_id {
247 continue;
248 }
249 continue;
257 }
258
259 let Some(name) = element
266 .attr(local_name!("name"))
267 .filter(|str| !str.is_empty())
268 else {
269 continue;
270 };
271
272 if element.name.local == local_name!("select") {
274 let is_multiple = element.attr(local_name!("multiple")).is_some();
275
276 let mut enabled_options: Vec<(usize, bool)> = Vec::new();
279 for option_id in TreeTraverser::new_with_root(doc, control_id)
280 .skip(1) .filter(|&child_id| {
282 doc.nodes[child_id]
283 .data
284 .is_element_with_tag_name(&local_name!("option"))
285 })
286 {
287 let opt_node = &doc.nodes[option_id];
288 let Some(opt_element) = opt_node.element_data() else {
289 continue;
290 };
291 if opt_element.attr(local_name!("disabled")).is_some() {
293 continue;
294 }
295 let is_selected = opt_element.attr(local_name!("selected")).is_some();
296 enabled_options.push((option_id, is_selected));
297 }
298
299 if is_multiple {
301 for (opt_id, is_selected) in &enabled_options {
303 if *is_selected {
304 let opt_node = &doc.nodes[*opt_id];
305 let value = opt_node
306 .element_data()
307 .and_then(|el| el.attr(local_name!("value")))
308 .map(|v| v.to_string())
309 .unwrap_or_else(|| opt_node.text_content());
310 create_entry(name, value.as_str().into());
311 }
312 }
313 } else {
314 let submit_id = enabled_options
316 .iter()
317 .find(|(_, is_selected)| *is_selected)
318 .map(|(id, _)| *id)
319 .or_else(|| enabled_options.first().map(|(id, _)| *id));
320
321 if let Some(opt_id) = submit_id {
322 let opt_node = &doc.nodes[opt_id];
323 let value = opt_node
324 .element_data()
325 .and_then(|el| el.attr(local_name!("value")))
326 .map(|v| v.to_string())
327 .unwrap_or_else(|| opt_node.text_content());
328 create_entry(name, value.as_str().into());
329 }
330 }
331
332 continue;
333 }
334
335 if element.name.local == local_name!("input")
337 && matches!(element_type, Some("checkbox" | "radio"))
338 {
339 let value = element.attr(local_name!("value")).unwrap_or("on");
341 create_entry(name, value.into());
343 continue;
344 }
345 #[cfg(feature = "file_input")]
347 if element.name.local == local_name!("input") && matches!(element_type, Some("file")) {
348 let Some(files) = element.file_data() else {
350 create_entry(name, EntryValue::EmptyFile);
351 continue;
352 };
353 if files.is_empty() {
354 create_entry(name, EntryValue::EmptyFile);
355 }
356 else {
358 for path_buf in files.iter() {
359 create_entry(name, path_buf.clone().into());
360 }
361 }
362 continue;
363 }
364 if element.name.local == local_name!("input")
366 && element_type == Some("hidden")
367 && name.eq_ignore_ascii_case("_charset_")
368 {
369 let charset = "UTF-8";
373 create_entry(name, charset.into());
375 }
376 else if let Some(text) = element.text_input_data() {
378 create_entry(name, text.editor.text().to_string().as_str().into());
379 } else if let Some(value) = element.attr(local_name!("value")) {
380 create_entry(name, value.into());
381 }
382 }
383 entry_list
384}
385
386fn get_form_attr<'a>(
387 doc: &'a BaseDocument,
388 form: &'a ElementData,
389 form_local: impl PartialEq<LocalName>,
390 submitter_id: usize,
391 submitter_local: impl PartialEq<LocalName>,
392) -> Option<&'a str> {
393 get_submitter_attr(doc, submitter_id, submitter_local).or_else(|| form.attr(form_local))
394}
395
396fn get_submitter_attr(
397 doc: &BaseDocument,
398 submitter_id: usize,
399 local_name: impl PartialEq<LocalName>,
400) -> Option<&str> {
401 doc.get_node(submitter_id)
402 .and_then(|node| node.element_data())
403 .and_then(|element_data| {
404 if element_data.name.local == local_name!("button")
405 && element_data.attr(local_name!("type")) == Some("submit")
406 {
407 element_data.attr(local_name)
408 } else {
409 None
410 }
411 })
412}
413
414#[derive(Debug, Copy, Clone, PartialEq, Eq)]
415enum FormMethod {
416 Get,
417 Post,
418 Dialog,
419}
420impl FromStr for FormMethod {
421 type Err = ();
422 fn from_str(s: &str) -> Result<Self, Self::Err> {
423 Ok(match s.to_lowercase().as_str() {
424 "get" => FormMethod::Get,
425 "post" => FormMethod::Post,
426 "dialog" => FormMethod::Dialog,
427 _ => return Err(()),
428 })
429 }
430}
431impl TryFrom<FormMethod> for Method {
432 type Error = &'static str;
433 fn try_from(method: FormMethod) -> Result<Self, Self::Error> {
434 Ok(match method {
435 FormMethod::Get => Method::GET,
436 FormMethod::Post => Method::POST,
437 FormMethod::Dialog => return Err("Dialog is not an HTTP method"),
438 })
439 }
440}
441#[derive(Debug, Clone)]
443pub enum RequestContentType {
444 FormUrlEncoded,
446 MultipartFormData,
448 TextPlain,
450}
451
452impl FromStr for RequestContentType {
453 type Err = ();
454 fn from_str(s: &str) -> Result<Self, Self::Err> {
455 Ok(match s {
456 "application/x-www-form-urlencoded" => RequestContentType::FormUrlEncoded,
457 "multipart/form-data" => RequestContentType::MultipartFormData,
458 "text/plain" => RequestContentType::TextPlain,
459 _ => return Err(()),
460 })
461 }
462}
463
464impl Display for RequestContentType {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 match self {
467 RequestContentType::FormUrlEncoded => write!(f, "application/x-www-form-urlencoded"),
468 RequestContentType::MultipartFormData => write!(f, "multipart/form-data"),
469 RequestContentType::TextPlain => write!(f, "text/plain"),
470 }
471 }
472}
473
474fn convert_to_list_of_name_value_pairs(form_data: FormData) -> Vec<(String, String)> {
477 form_data
478 .iter()
479 .map(|Entry { name, value }| {
480 let name = normalize_line_endings(name.as_ref());
481 let value = normalize_line_endings(value.as_ref());
482 (name, value)
483 })
484 .collect()
485}
486
487fn normalize_line_endings(input: &str) -> String {
490 let mut result = String::with_capacity(input.len());
495 let mut chars = input.chars().peekable();
496
497 while let Some(current) = chars.next() {
498 match (current, chars.peek()) {
499 ('\r', Some('\n')) => {
500 result.push_str("\r\n");
501 chars.next();
502 }
503 ('\r' | '\n', _) => {
504 result.push_str("\r\n");
505 }
506 _ => result.push(current),
507 }
508 }
509
510 result
511}
512
513fn encode_text_plain<T: AsRef<str>, U: AsRef<str>>(input: &[(T, U)]) -> String {
516 let mut out = String::new();
517 for (name, value) in input {
518 out.push_str(name.as_ref());
519 out.push('=');
520 out.push_str(value.as_ref());
521 out.push_str("\r\n");
522 }
523 out
524}