1use blitz_traits::node_id::NodeId;
2use markup5ever::{LocalName, local_name};
3
4use crate::{
5 BaseDocument, ElementData,
6 traversal::{AncestorTraverser, TreeTraverser},
7};
8use blitz_traits::{
9 navigation::NavigationOptions,
10 net::{Body, Entry, EntryValue, FormData, Method},
11};
12use core::str::FromStr;
13use std::fmt::Display;
14
15const DEFAULT_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS
17 .add(b' ')
19 .add(b'"')
20 .add(b'#')
21 .add(b'<')
22 .add(b'>')
23 .add(b'?')
25 .add(b'`')
26 .add(b'{')
27 .add(b'}');
28
29impl BaseDocument {
30 pub fn reset_form_owner(&mut self, node_id: NodeId) {
38 let node = &self.nodes[node_id];
39 let Some(element) = node.element_data() else {
40 return;
41 };
42
43 let final_owner_id = element
45 .attr(local_name!("form"))
46 .and_then(|owner| self.get_element_by_id(owner))
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: NodeId, submitter_id: NodeId) {
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, Some(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: NodeId, submitter_id: NodeId) -> 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: NodeId) -> 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!("select") {
279 let options = doc.select_options(control_id);
280 for (index, option_id) in options.iter().enumerate() {
281 let is_selected = element
282 .select_data()
283 .map(|data| data.is_selected(index))
284 .unwrap_or_else(|| doc.option_is_selected(*option_id));
287 if is_selected && !doc.option_is_disabled(*option_id) {
288 create_entry(name, doc.option_value(*option_id).as_str().into());
289 }
290 }
291 continue;
292 }
293
294 if element.name.local == local_name!("input")
296 && matches!(element_type, Some("checkbox" | "radio"))
297 {
298 let value = element.attr(local_name!("value")).unwrap_or("on");
300 create_entry(name, value.into());
302 continue;
303 }
304 #[cfg(feature = "file-input")]
306 if element.name.local == local_name!("input") && matches!(element_type, Some("file")) {
307 let Some(files) = element.file_data() else {
309 create_entry(name, EntryValue::EmptyFile);
310 continue;
311 };
312 if files.is_empty() {
313 create_entry(name, EntryValue::EmptyFile);
314 }
315 else {
317 for path_buf in files.iter() {
318 create_entry(name, path_buf.clone().into());
319 }
320 }
321 continue;
322 }
323 if element.name.local == local_name!("input")
325 && element_type == Some("hidden")
326 && name.eq_ignore_ascii_case("_charset_")
327 {
328 let charset = "UTF-8"; create_entry(name, charset.into());
332 }
333 else if let Some(text) = element.text_input_data() {
335 create_entry(name, text.editor.text().to_string().as_str().into());
336 } else if let Some(value) = element.attr(local_name!("value")) {
337 create_entry(name, value.into());
338 }
339 }
340 entry_list
341}
342
343fn get_form_attr<'a>(
344 doc: &'a BaseDocument,
345 form: &'a ElementData,
346 form_local: impl PartialEq<LocalName>,
347 submitter_id: NodeId,
348 submitter_local: impl PartialEq<LocalName>,
349) -> Option<&'a str> {
350 get_submitter_attr(doc, submitter_id, submitter_local).or_else(|| form.attr(form_local))
351}
352
353fn get_submitter_attr(
354 doc: &BaseDocument,
355 submitter_id: NodeId,
356 local_name: impl PartialEq<LocalName>,
357) -> Option<&str> {
358 doc.get_node(submitter_id)
359 .and_then(|node| node.element_data())
360 .and_then(|element_data| {
361 if element_data.name.local == local_name!("button")
362 && element_data.attr(local_name!("type")) == Some("submit")
363 {
364 element_data.attr(local_name)
365 } else {
366 None
367 }
368 })
369}
370
371#[derive(Debug, Copy, Clone, PartialEq, Eq)]
372enum FormMethod {
373 Get,
374 Post,
375 Dialog,
376}
377impl FromStr for FormMethod {
378 type Err = ();
379 fn from_str(s: &str) -> Result<Self, Self::Err> {
380 Ok(match s.to_lowercase().as_str() {
381 "get" => FormMethod::Get,
382 "post" => FormMethod::Post,
383 "dialog" => FormMethod::Dialog,
384 _ => return Err(()),
385 })
386 }
387}
388impl TryFrom<FormMethod> for Method {
389 type Error = &'static str;
390 fn try_from(method: FormMethod) -> Result<Self, Self::Error> {
391 Ok(match method {
392 FormMethod::Get => Method::GET,
393 FormMethod::Post => Method::POST,
394 FormMethod::Dialog => return Err("Dialog is not an HTTP method"),
395 })
396 }
397}
398#[derive(Debug, Clone)]
400pub enum RequestContentType {
401 FormUrlEncoded,
403 MultipartFormData,
405 TextPlain,
407}
408
409impl FromStr for RequestContentType {
410 type Err = ();
411 fn from_str(s: &str) -> Result<Self, Self::Err> {
412 Ok(match s {
413 "application/x-www-form-urlencoded" => RequestContentType::FormUrlEncoded,
414 "multipart/form-data" => RequestContentType::MultipartFormData,
415 "text/plain" => RequestContentType::TextPlain,
416 _ => return Err(()),
417 })
418 }
419}
420
421impl Display for RequestContentType {
422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423 match self {
424 RequestContentType::FormUrlEncoded => write!(f, "application/x-www-form-urlencoded"),
425 RequestContentType::MultipartFormData => write!(f, "multipart/form-data"),
426 RequestContentType::TextPlain => write!(f, "text/plain"),
427 }
428 }
429}
430
431fn convert_to_list_of_name_value_pairs(form_data: FormData) -> Vec<(String, String)> {
434 form_data
435 .iter()
436 .map(|Entry { name, value }| {
437 let name = normalize_line_endings(name.as_ref());
438 let value = normalize_line_endings(value.as_ref());
439 (name, value)
440 })
441 .collect()
442}
443
444fn normalize_line_endings(input: &str) -> String {
447 let mut result = String::with_capacity(input.len());
452 let mut chars = input.chars().peekable();
453
454 while let Some(current) = chars.next() {
455 match (current, chars.peek()) {
456 ('\r', Some('\n')) => {
457 result.push_str("\r\n");
458 chars.next();
459 }
460 ('\r' | '\n', _) => {
461 result.push_str("\r\n");
462 }
463 _ => result.push(current),
464 }
465 }
466
467 result
468}
469
470fn encode_text_plain<T: AsRef<str>, U: AsRef<str>>(input: &[(T, U)]) -> String {
473 let mut out = String::new();
474 for (name, value) in input {
475 out.push_str(name.as_ref());
476 out.push('=');
477 out.push_str(value.as_ref());
478 out.push_str("\r\n");
479 }
480 out
481}