/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::borrow::ToOwned; use std::cell::Cell; use std::time::{Duration, Instant}; use dom_struct::dom_struct; use encoding_rs::{Encoding, UTF_8}; use headers::{ContentType, HeaderMapExt}; use html5ever::{local_name, namespace_url, ns, LocalName, Prefix}; use http::Method; use js::rust::HandleObject; use mime::{self, Mime}; use net_traits::http_percent_encode; use net_traits::request::Referrer; use script_traits::{HistoryEntryReplacement, LoadData, LoadOrigin}; use servo_atoms::Atom; use servo_rand::random; use style::attr::AttrValue; use style::str::split_html_space_chars; use style_traits::dom::ElementState; use super::bindings::trace::{HashMapTracedValues, NoTrace}; use crate::body::Extractable; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::AttrBinding::Attr_Binding::AttrMethods; use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods; use crate::dom::bindings::codegen::Bindings::HTMLButtonElementBinding::HTMLButtonElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLFormControlsCollectionBinding::HTMLFormControlsCollectionMethods; use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::HTMLFormElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::{NodeConstants, NodeMethods}; use crate::dom::bindings::codegen::Bindings::NodeListBinding::NodeListMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; use crate::dom::bindings::codegen::UnionTypes::RadioNodeListOrElement; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{Dom, DomOnceCell, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::blob::Blob; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::file::File; use crate::dom::formdata::FormData; use crate::dom::formdataevent::FormDataEvent; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlanchorelement::{get_element_noopener, get_element_target}; use crate::dom::htmlbuttonelement::HTMLButtonElement; use crate::dom::htmlcollection::CollectionFilter; use crate::dom::htmldatalistelement::HTMLDataListElement; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; use crate::dom::htmlformcontrolscollection::HTMLFormControlsCollection; use crate::dom::htmlimageelement::HTMLImageElement; use crate::dom::htmlinputelement::{HTMLInputElement, InputType}; use crate::dom::htmllabelelement::HTMLLabelElement; use crate::dom::htmllegendelement::HTMLLegendElement; use crate::dom::htmlobjectelement::HTMLObjectElement; use crate::dom::htmloutputelement::HTMLOutputElement; use crate::dom::htmlselectelement::HTMLSelectElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::node::{ document_from_node, window_from_node, Node, NodeFlags, UnbindContext, VecPreOrderInsertionHelper, }; use crate::dom::nodelist::{NodeList, RadioListMode}; use crate::dom::radionodelist::RadioNodeList; use crate::dom::submitevent::SubmitEvent; use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; use crate::task_source::TaskSource; #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] pub struct GenerationId(u32); #[dom_struct] pub struct HTMLFormElement { htmlelement: HTMLElement, marked_for_reset: Cell, /// constructing_entry_list: Cell, elements: DomOnceCell, generation_id: Cell, controls: DomRefCell>>, past_names_map: DomRefCell, NoTrace)>>, firing_submission_events: Cell, rel_list: MutNullableDom, } impl HTMLFormElement { fn new_inherited( local_name: LocalName, prefix: Option, document: &Document, ) -> HTMLFormElement { HTMLFormElement { htmlelement: HTMLElement::new_inherited_with_state( ElementState::VALID, local_name, prefix, document, ), marked_for_reset: Cell::new(false), constructing_entry_list: Cell::new(false), elements: Default::default(), generation_id: Cell::new(GenerationId(0)), controls: DomRefCell::new(Vec::new()), past_names_map: DomRefCell::new(HashMapTracedValues::new()), firing_submission_events: Cell::new(false), rel_list: Default::default(), } } #[allow(crown::unrooted_must_root)] pub fn new( local_name: LocalName, prefix: Option, document: &Document, proto: Option, ) -> DomRoot { Node::reflect_node_with_proto( Box::new(HTMLFormElement::new_inherited(local_name, prefix, document)), document, proto, ) } fn filter_for_radio_list(mode: RadioListMode, child: &Element, name: &Atom) -> bool { if let Some(child) = child.downcast::() { match mode { RadioListMode::ControlsExceptImageInputs => { if child .downcast::() .map_or(false, |c| c.is_listed_element()) && (child.get_id().map_or(false, |i| i == *name) || child.get_name().map_or(false, |n| n == *name)) { if let Some(inp) = child.downcast::() { // input, only return it if it's not image-button state return inp.input_type() != InputType::Image; } else { // control, but not an input return true; } } return false; }, RadioListMode::Images => { return child.is::() && (child.get_id().map_or(false, |i| i == *name) || child.get_name().map_or(false, |n| n == *name)); }, } } false } pub fn nth_for_radio_list( &self, index: u32, mode: RadioListMode, name: &Atom, ) -> Option> { self.controls .borrow() .iter() .filter(|n| HTMLFormElement::filter_for_radio_list(mode, n, name)) .nth(index as usize) .map(|n| DomRoot::from_ref(n.upcast::())) } pub fn count_for_radio_list(&self, mode: RadioListMode, name: &Atom) -> u32 { self.controls .borrow() .iter() .filter(|n| HTMLFormElement::filter_for_radio_list(mode, n, name)) .count() as u32 } } impl HTMLFormElementMethods for HTMLFormElement { // https://html.spec.whatwg.org/multipage/#dom-form-acceptcharset make_getter!(AcceptCharset, "accept-charset"); // https://html.spec.whatwg.org/multipage/#dom-form-acceptcharset make_setter!(SetAcceptCharset, "accept-charset"); // https://html.spec.whatwg.org/multipage/#dom-fs-action make_form_action_getter!(Action, "action"); // https://html.spec.whatwg.org/multipage/#dom-fs-action make_setter!(SetAction, "action"); // https://html.spec.whatwg.org/multipage/#dom-form-autocomplete make_enumerated_getter!(Autocomplete, "autocomplete", "on", "off"); // https://html.spec.whatwg.org/multipage/#dom-form-autocomplete make_setter!(SetAutocomplete, "autocomplete"); // https://html.spec.whatwg.org/multipage/#dom-fs-enctype make_enumerated_getter!( Enctype, "enctype", "application/x-www-form-urlencoded", "text/plain" | "multipart/form-data" ); // https://html.spec.whatwg.org/multipage/#dom-fs-enctype make_setter!(SetEnctype, "enctype"); // https://html.spec.whatwg.org/multipage/#dom-fs-encoding fn Encoding(&self) -> DOMString { self.Enctype() } // https://html.spec.whatwg.org/multipage/#dom-fs-encoding fn SetEncoding(&self, value: DOMString) { self.SetEnctype(value) } // https://html.spec.whatwg.org/multipage/#dom-fs-method make_enumerated_getter!(Method, "method", "get", "post" | "dialog"); // https://html.spec.whatwg.org/multipage/#dom-fs-method make_setter!(SetMethod, "method"); // https://html.spec.whatwg.org/multipage/#dom-form-name make_getter!(Name, "name"); // https://html.spec.whatwg.org/multipage/#dom-form-name make_atomic_setter!(SetName, "name"); // https://html.spec.whatwg.org/multipage/#dom-fs-novalidate make_bool_getter!(NoValidate, "novalidate"); // https://html.spec.whatwg.org/multipage/#dom-fs-novalidate make_bool_setter!(SetNoValidate, "novalidate"); // https://html.spec.whatwg.org/multipage/#dom-fs-target make_getter!(Target, "target"); // https://html.spec.whatwg.org/multipage/#dom-fs-target make_setter!(SetTarget, "target"); // https://html.spec.whatwg.org/multipage/#dom-a-rel make_getter!(Rel, "rel"); // https://html.spec.whatwg.org/multipage/#the-form-element:concept-form-submit fn Submit(&self) { self.submit(SubmittedFrom::FromForm, FormSubmitter::FormElement(self)); } // https://html.spec.whatwg.org/multipage/#dom-form-requestsubmit fn RequestSubmit(&self, submitter: Option<&HTMLElement>) -> Fallible<()> { let submitter: FormSubmitter = match submitter { Some(submitter_element) => { // Step 1.1 let error_not_a_submit_button = Err(Error::Type("submitter must be a submit button".to_string())); let element = match submitter_element.upcast::().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(element)) => element, _ => { return error_not_a_submit_button; }, }; let submit_button = match element { HTMLElementTypeId::HTMLInputElement => FormSubmitter::InputElement( submitter_element .downcast::() .expect("Failed to downcast submitter elem to HTMLInputElement."), ), HTMLElementTypeId::HTMLButtonElement => FormSubmitter::ButtonElement( submitter_element .downcast::() .expect("Failed to downcast submitter elem to HTMLButtonElement."), ), _ => { return error_not_a_submit_button; }, }; if !submit_button.is_submit_button() { return error_not_a_submit_button; } let submitters_owner = submit_button.form_owner(); // Step 1.2 let owner = match submitters_owner { Some(owner) => owner, None => { return Err(Error::NotFound); }, }; if *owner != *self { return Err(Error::NotFound); } submit_button }, None => { // Step 2 FormSubmitter::FormElement(self) }, }; // Step 3 self.submit(SubmittedFrom::NotFromForm, submitter); Ok(()) } // https://html.spec.whatwg.org/multipage/#dom-form-reset fn Reset(&self) { self.reset(ResetFrom::FromForm); } // https://html.spec.whatwg.org/multipage/#dom-form-elements fn Elements(&self) -> DomRoot { #[derive(JSTraceable, MallocSizeOf)] struct ElementsFilter { form: DomRoot, } impl CollectionFilter for ElementsFilter { fn filter<'a>(&self, elem: &'a Element, _root: &'a Node) -> bool { let form_owner = match elem.upcast::().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(t)) => match t { HTMLElementTypeId::HTMLButtonElement => { elem.downcast::().unwrap().form_owner() }, HTMLElementTypeId::HTMLFieldSetElement => { elem.downcast::().unwrap().form_owner() }, HTMLElementTypeId::HTMLInputElement => { let input_elem = elem.downcast::().unwrap(); if input_elem.input_type() == InputType::Image { return false; } input_elem.form_owner() }, HTMLElementTypeId::HTMLObjectElement => { elem.downcast::().unwrap().form_owner() }, HTMLElementTypeId::HTMLOutputElement => { elem.downcast::().unwrap().form_owner() }, HTMLElementTypeId::HTMLSelectElement => { elem.downcast::().unwrap().form_owner() }, HTMLElementTypeId::HTMLTextAreaElement => { elem.downcast::().unwrap().form_owner() }, _ => { debug_assert!(!elem .downcast::() .unwrap() .is_listed_element()); return false; }, }, _ => return false, }; match form_owner { Some(form_owner) => form_owner == self.form, None => false, } } } DomRoot::from_ref(self.elements.init_once(|| { let filter = Box::new(ElementsFilter { form: DomRoot::from_ref(self), }); let window = window_from_node(self); HTMLFormControlsCollection::new(&window, self, filter) })) } // https://html.spec.whatwg.org/multipage/#dom-form-length fn Length(&self) -> u32 { self.Elements().Length() } // https://html.spec.whatwg.org/multipage/#dom-form-item fn IndexedGetter(&self, index: u32) -> Option> { let elements = self.Elements(); elements.IndexedGetter(index) } // https://html.spec.whatwg.org/multipage/#the-form-element%3Adetermine-the-value-of-a-named-property fn NamedGetter(&self, name: DOMString) -> Option { let window = window_from_node(self); let name = Atom::from(name); // Step 1 let mut candidates = RadioNodeList::new_controls_except_image_inputs(&window, self, &name); let mut candidates_length = candidates.Length(); // Step 2 if candidates_length == 0 { candidates = RadioNodeList::new_images(&window, self, &name); candidates_length = candidates.Length(); } let mut past_names_map = self.past_names_map.borrow_mut(); // Step 3 if candidates_length == 0 { if past_names_map.contains_key(&name) { return Some(RadioNodeListOrElement::Element(DomRoot::from_ref( &*past_names_map.get(&name).unwrap().0, ))); } return None; } // Step 4 if candidates_length > 1 { return Some(RadioNodeListOrElement::RadioNodeList(candidates)); } // Step 5 // candidates_length is 1, so we can unwrap item 0 let element_node = candidates.upcast::().Item(0).unwrap(); past_names_map.insert( name, ( Dom::from_ref(element_node.downcast::().unwrap()), NoTrace(Instant::now()), ), ); // Step 6 return Some(RadioNodeListOrElement::Element(DomRoot::from_ref( element_node.downcast::().unwrap(), ))); } // https://html.spec.whatwg.org/multipage/#dom-a-rel fn SetRel(&self, rel: DOMString) { self.upcast::() .set_tokenlist_attribute(&local_name!("rel"), rel); } // https://html.spec.whatwg.org/multipage/#dom-a-rellist fn RelList(&self) -> DomRoot { self.rel_list.or_init(|| { DOMTokenList::new( self.upcast(), &local_name!("rel"), Some(vec![ Atom::from("noopener"), Atom::from("noreferrer"), Atom::from("opener"), ]), ) }) } // https://html.spec.whatwg.org/multipage/#the-form-element:supported-property-names #[allow(non_snake_case)] fn SupportedPropertyNames(&self) -> Vec { // Step 1 #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] enum SourcedNameSource { Id, Name, Past(Duration), } impl SourcedNameSource { fn is_past(&self) -> bool { matches!(self, SourcedNameSource::Past(..)) } } struct SourcedName { name: Atom, element: DomRoot, source: SourcedNameSource, } let mut sourced_names_vec: Vec = Vec::new(); let controls = self.controls.borrow(); // Step 2 for child in controls.iter() { if child .downcast::() .map_or(false, |c| c.is_listed_element()) { if let Some(id_atom) = child.get_id() { let entry = SourcedName { name: id_atom, element: DomRoot::from_ref(child), source: SourcedNameSource::Id, }; sourced_names_vec.push(entry); } if let Some(name_atom) = child.get_name() { let entry = SourcedName { name: name_atom, element: DomRoot::from_ref(child), source: SourcedNameSource::Name, }; sourced_names_vec.push(entry); } } } // Step 3 for child in controls.iter() { if child.is::() { if let Some(id_atom) = child.get_id() { let entry = SourcedName { name: id_atom, element: DomRoot::from_ref(child), source: SourcedNameSource::Id, }; sourced_names_vec.push(entry); } if let Some(name_atom) = child.get_name() { let entry = SourcedName { name: name_atom, element: DomRoot::from_ref(child), source: SourcedNameSource::Name, }; sourced_names_vec.push(entry); } } } // Step 4 let past_names_map = self.past_names_map.borrow(); for (key, val) in past_names_map.iter() { let entry = SourcedName { name: key.clone(), element: DomRoot::from_ref(&*val.0), source: SourcedNameSource::Past(Instant::now().duration_since(val.1 .0)), }; sourced_names_vec.push(entry); } // Step 5 // TODO need to sort as per spec. // if a.CompareDocumentPosition(b) returns 0 that means a=b in which case // the remaining part where sorting is to be done by putting entries whose source is id first, // then entries whose source is name, and finally entries whose source is past, // and sorting entries with the same element and source by their age, oldest first. // if a.CompareDocumentPosition(b) has set NodeConstants::DOCUMENT_POSITION_FOLLOWING // (this can be checked by bitwise operations) then b would follow a in tree order and // Ordering::Less should be returned in the closure else Ordering::Greater sourced_names_vec.sort_by(|a, b| { if a.element .upcast::() .CompareDocumentPosition(b.element.upcast::()) == 0 { if a.source.is_past() && b.source.is_past() { b.source.cmp(&a.source) } else { a.source.cmp(&b.source) } } else if a .element .upcast::() .CompareDocumentPosition(b.element.upcast::()) & NodeConstants::DOCUMENT_POSITION_FOLLOWING == NodeConstants::DOCUMENT_POSITION_FOLLOWING { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater } }); // Step 6 sourced_names_vec.retain(|sn| !sn.name.to_string().is_empty()); // Step 7-8 let mut names_vec: Vec = Vec::new(); for elem in sourced_names_vec.iter() { if names_vec.iter().find(|name| **name == *elem.name).is_none() { names_vec.push(DOMString::from(&*elem.name)); } } names_vec } /// fn CheckValidity(&self) -> bool { self.static_validation().is_ok() } /// fn ReportValidity(&self) -> bool { self.interactive_validation().is_ok() } } #[derive(Clone, Copy, MallocSizeOf, PartialEq)] pub enum SubmittedFrom { FromForm, NotFromForm, } #[derive(Clone, Copy, MallocSizeOf)] pub enum ResetFrom { FromForm, NotFromForm, } impl HTMLFormElement { // https://html.spec.whatwg.org/multipage/#picking-an-encoding-for-the-form fn pick_encoding(&self) -> &'static Encoding { // Step 2 if self .upcast::() .has_attribute(&local_name!("accept-charset")) { // Substep 1 let input = self .upcast::() .get_string_attribute(&local_name!("accept-charset")); // Substep 2, 3, 4 let mut candidate_encodings = split_html_space_chars(&input).filter_map(|c| Encoding::for_label(c.as_bytes())); // Substep 5, 6 return candidate_encodings.next().unwrap_or(UTF_8); } // Step 1, 3 document_from_node(self).encoding() } // https://html.spec.whatwg.org/multipage/#text/plain-encoding-algorithm fn encode_plaintext(&self, form_data: &mut Vec) -> String { // Step 1 let mut result = String::new(); // Step 2 for entry in form_data.iter() { let value = match &entry.value { FormDatumValue::File(f) => f.name(), FormDatumValue::String(s) => s, }; result.push_str(&format!("{}={}\r\n", entry.name, value)); } // Step 3 result } pub fn update_validity(&self) { let controls = self.controls.borrow(); let is_any_invalid = controls .iter() .filter_map(|control| control.as_maybe_validatable()) .any(|validatable| { validatable.is_instance_validatable() && !validatable.validity_state().invalid_flags().is_empty() }); self.upcast::() .set_state(ElementState::VALID, !is_any_invalid); self.upcast::() .set_state(ElementState::INVALID, is_any_invalid); } /// [Form submission](https://html.spec.whatwg.org/multipage/#concept-form-submit) pub fn submit(&self, submit_method_flag: SubmittedFrom, submitter: FormSubmitter) { // Step 1 if self.upcast::().cannot_navigate() { return; } // Step 2 if self.constructing_entry_list.get() { return; } // Step 3 let doc = document_from_node(self); let base = doc.base_url(); // TODO: Handle browsing contexts (Step 4, 5) // Step 6 if submit_method_flag == SubmittedFrom::NotFromForm { // Step 6.1 if self.firing_submission_events.get() { return; } // Step 6.2 self.firing_submission_events.set(true); // Step 6.3 if !submitter.no_validate(self) && self.interactive_validation().is_err() { self.firing_submission_events.set(false); return; } // Step 6.4 // spec calls this "submitterButton" but it doesn't have to be a button, // just not be the form itself let submitter_button = match submitter { FormSubmitter::FormElement(f) => { if f == self { None } else { Some(f.upcast::()) } }, FormSubmitter::InputElement(i) => Some(i.upcast::()), FormSubmitter::ButtonElement(b) => Some(b.upcast::()), }; // Step 6.5 let event = SubmitEvent::new( &self.global(), atom!("submit"), true, true, submitter_button.map(DomRoot::from_ref), ); let event = event.upcast::(); event.fire(self.upcast::()); // Step 6.6 self.firing_submission_events.set(false); // Step 6.7 if event.DefaultPrevented() { return; } // Step 6.8 if self.upcast::().cannot_navigate() { return; } } // Step 7 let encoding = self.pick_encoding(); // Step 8 let mut form_data = match self.get_form_dataset(Some(submitter), Some(encoding)) { Some(form_data) => form_data, None => return, }; // Step 9 if self.upcast::().cannot_navigate() { return; } // Step 10 let mut action = submitter.action(); // Step 11 if action.is_empty() { action = DOMString::from(base.as_str()); } // Step 12-13 let action_components = match base.join(&action) { Ok(url) => url, Err(_) => return, }; // Step 14-16 let scheme = action_components.scheme().to_owned(); let enctype = submitter.enctype(); let method = submitter.method(); // Step 17 let target_attribute_value = if submitter.is_submit_button() && submitter.target() != DOMString::new() { Some(submitter.target()) } else { let form_owner = submitter.form_owner(); let form = form_owner.as_ref().map(|form| &**form).unwrap_or(self); get_element_target(form.upcast::()) }; // Step 18 let noopener = get_element_noopener(self.upcast::(), target_attribute_value.clone()); // Step 19 let source = doc.browsing_context().unwrap(); let (maybe_chosen, _new) = source .choose_browsing_context(target_attribute_value.unwrap_or(DOMString::new()), noopener); // Step 20 let chosen = match maybe_chosen { Some(proxy) => proxy, None => return, }; let target_document = match chosen.document() { Some(doc) => doc, None => return, }; // Step 21 let target_window = target_document.window(); let mut load_data = LoadData::new( LoadOrigin::Script(doc.origin().immutable().clone()), action_components, None, target_window.upcast::().get_referrer(), target_document.get_referrer_policy(), Some(target_window.upcast::().is_secure_context()), ); // Step 22 match (&*scheme, method) { (_, FormMethod::FormDialog) => { // TODO: Submit dialog // https://html.spec.whatwg.org/multipage/#submit-dialog }, // https://html.spec.whatwg.org/multipage/#submit-mutate-action ("http", FormMethod::FormGet) | ("https", FormMethod::FormGet) | ("data", FormMethod::FormGet) => { load_data .headers .typed_insert(ContentType::from(mime::APPLICATION_WWW_FORM_URLENCODED)); self.mutate_action_url(&mut form_data, load_data, encoding, target_window); }, // https://html.spec.whatwg.org/multipage/#submit-body ("http", FormMethod::FormPost) | ("https", FormMethod::FormPost) => { load_data.method = Method::POST; self.submit_entity_body( &mut form_data, load_data, enctype, encoding, target_window, ); }, // https://html.spec.whatwg.org/multipage/#submit-get-action ("file", _) | ("about", _) | ("data", FormMethod::FormPost) | ("ftp", _) | ("javascript", _) => { self.plan_to_navigate(load_data, target_window); }, ("mailto", FormMethod::FormPost) => { // TODO: Mail as body // https://html.spec.whatwg.org/multipage/#submit-mailto-body }, ("mailto", FormMethod::FormGet) => { // TODO: Mail with headers // https://html.spec.whatwg.org/multipage/#submit-mailto-headers }, _ => (), } } // https://html.spec.whatwg.org/multipage/#submit-mutate-action fn mutate_action_url( &self, form_data: &mut [FormDatum], mut load_data: LoadData, encoding: &'static Encoding, target: &Window, ) { let charset = encoding.name(); self.set_url_query_pairs( &mut load_data.url, form_data .iter() .map(|field| (&*field.name, field.replace_value(charset))), ); self.plan_to_navigate(load_data, target); } // https://html.spec.whatwg.org/multipage/#submit-body fn submit_entity_body( &self, form_data: &mut Vec, mut load_data: LoadData, enctype: FormEncType, encoding: &'static Encoding, target: &Window, ) { let boundary = generate_boundary(); let bytes = match enctype { FormEncType::UrlEncoded => { let charset = encoding.name(); load_data .headers .typed_insert(ContentType::from(mime::APPLICATION_WWW_FORM_URLENCODED)); let mut url = load_data.url.clone(); self.set_url_query_pairs( &mut url, form_data .iter() .map(|field| (&*field.name, field.replace_value(charset))), ); url.query().unwrap_or("").to_string().into_bytes() }, FormEncType::FormDataEncoded => { let mime: Mime = format!("multipart/form-data; boundary={}", boundary) .parse() .unwrap(); load_data.headers.typed_insert(ContentType::from(mime)); encode_multipart_form_data(form_data, boundary, encoding) }, FormEncType::TextPlainEncoded => { load_data .headers .typed_insert(ContentType::from(mime::TEXT_PLAIN)); self.encode_plaintext(form_data).into_bytes() }, }; let global = self.global(); let request_body = bytes .extract(&global) .expect("Couldn't extract body.") .into_net_request_body() .0; load_data.data = Some(request_body); self.plan_to_navigate(load_data, target); } fn set_url_query_pairs<'a>( &self, url: &mut servo_url::ServoUrl, pairs: impl Iterator, ) { let encoding = self.pick_encoding(); url.as_mut_url() .query_pairs_mut() .encoding_override(Some(&|s| encoding.encode(s).0)) .clear() .extend_pairs(pairs); } /// [Planned navigation](https://html.spec.whatwg.org/multipage/#planned-navigation) fn plan_to_navigate(&self, mut load_data: LoadData, target: &Window) { // Step 1 // Each planned navigation task is tagged with a generation ID, and // before the task is handled, it first checks whether the HTMLFormElement's // generation ID is the same as its own generation ID. let generation_id = GenerationId(self.generation_id.get().0 + 1); self.generation_id.set(generation_id); // Step 2 let elem = self.upcast::(); let referrer = match elem.get_attribute(&ns!(), &local_name!("rel")) { Some(ref link_types) if link_types.Value().contains("noreferrer") => { Referrer::NoReferrer }, _ => target.upcast::().get_referrer(), }; let referrer_policy = target.Document().get_referrer_policy(); let pipeline_id = target.upcast::().pipeline_id(); load_data.creator_pipeline_id = Some(pipeline_id); load_data.referrer = referrer; load_data.referrer_policy = referrer_policy; // Step 4. let this = Trusted::new(self); let window = Trusted::new(target); let task = task!(navigate_to_form_planned_navigation: move || { if generation_id != this.root().generation_id.get() { return; } window .root() .load_url( HistoryEntryReplacement::Disabled, false, load_data, ); }); // Step 3. target .task_manager() .dom_manipulation_task_source() .queue(task, target.upcast()) .unwrap(); } /// Interactively validate the constraints of form elements /// fn interactive_validation(&self) -> Result<(), ()> { // Step 1-2 let unhandled_invalid_controls = match self.static_validation() { Ok(()) => return Ok(()), Err(err) => err, }; // Step 3 let mut first = true; for elem in unhandled_invalid_controls { if let Some(validatable) = elem.as_maybe_validatable() { println!("Validation error: {}", validatable.validation_message()); } if first { if let Some(html_elem) = elem.downcast::() { html_elem.Focus(); first = false; } } } // Step 4 Err(()) } /// Statitically validate the constraints of form elements /// fn static_validation(&self) -> Result<(), Vec>> { let controls = self.controls.borrow(); // Step 1-3 let invalid_controls = controls .iter() .filter_map(|field| { if let Some(el) = field.downcast::() { let validatable = match el.as_maybe_validatable() { Some(v) => v, None => return None, }; validatable .validity_state() .perform_validation_and_update(ValidationFlags::all()); if !validatable.is_instance_validatable() || validatable.validity_state().invalid_flags().is_empty() { None } else { Some(DomRoot::from_ref(el)) } } else { None } }) .collect::>>(); // Step 4 if invalid_controls.is_empty() { return Ok(()); } // Step 5-6 let unhandled_invalid_controls = invalid_controls .into_iter() .filter_map(|field| { let event = field .upcast::() .fire_cancelable_event(atom!("invalid")); if !event.DefaultPrevented() { return Some(field); } None }) .collect::>>(); // Step 7 Err(unhandled_invalid_controls) } /// /// terminology note: "form data set" = "entry list" /// Steps range from 3 to 5 /// 5.x substeps are mostly handled inside element-specific methods fn get_unclean_dataset( &self, submitter: Option, encoding: Option<&'static Encoding>, ) -> Vec { let controls = self.controls.borrow(); let mut data_set = Vec::new(); for child in controls.iter() { // Step 5.1: The field element is disabled. if child.disabled_state() { continue; } let child = child.upcast::(); // Step 5.1: The field element has a datalist element ancestor. if child .ancestors() .any(|a| DomRoot::downcast::(a).is_some()) { continue; } if let NodeTypeId::Element(ElementTypeId::HTMLElement(element)) = child.type_id() { match element { HTMLElementTypeId::HTMLInputElement => { let input = child.downcast::().unwrap(); data_set.append(&mut input.form_datums(submitter, encoding)); }, HTMLElementTypeId::HTMLButtonElement => { let button = child.downcast::().unwrap(); if let Some(datum) = button.form_datum(submitter) { data_set.push(datum); } }, HTMLElementTypeId::HTMLObjectElement => { // Unimplemented }, HTMLElementTypeId::HTMLSelectElement => { let select = child.downcast::().unwrap(); select.push_form_data(&mut data_set); }, HTMLElementTypeId::HTMLTextAreaElement => { let textarea = child.downcast::().unwrap(); let name = textarea.Name(); if !name.is_empty() { data_set.push(FormDatum { ty: textarea.Type(), name, value: FormDatumValue::String(textarea.Value()), }); } }, _ => (), } } // Step: 5.13. Add an entry if element has dirname attribute // An element can only have a dirname attribute if it is a textarea element // or an input element whose type attribute is in either the Text state or the Search state let child_element = child.downcast::().unwrap(); let input_matches = child_element .downcast::() .map_or(false, |input| { input.input_type() == InputType::Text || input.input_type() == InputType::Search }); let textarea_matches = child_element.is::(); let dirname = child_element.get_string_attribute(&local_name!("dirname")); if (input_matches || textarea_matches) && !dirname.is_empty() { let dir = DOMString::from(child_element.directionality()); data_set.push(FormDatum { ty: DOMString::from("string"), name: dirname, value: FormDatumValue::String(dir), }); } } data_set } /// pub fn get_form_dataset( &self, submitter: Option, encoding: Option<&'static Encoding>, ) -> Option> { fn clean_crlf(s: &str) -> DOMString { // Step 4 let mut buf = "".to_owned(); let mut prev = ' '; for ch in s.chars() { match ch { '\n' if prev != '\r' => { buf.push('\r'); buf.push('\n'); }, '\n' => { buf.push('\n'); }, // This character isn't LF but is // preceded by CR _ if prev == '\r' => { buf.push('\r'); buf.push('\n'); buf.push(ch); }, _ => buf.push(ch), }; prev = ch; } // In case the last character was CR if prev == '\r' { buf.push('\n'); } DOMString::from(buf) } // Step 1 if self.constructing_entry_list.get() { return None; } // Step 2 self.constructing_entry_list.set(true); // Step 3-6 let mut ret = self.get_unclean_dataset(submitter, encoding); for datum in &mut ret { match &*datum.ty { "file" | "textarea" => (), // TODO _ => { datum.name = clean_crlf(&datum.name); datum.value = FormDatumValue::String(clean_crlf(match datum.value { FormDatumValue::String(ref s) => s, FormDatumValue::File(_) => unreachable!(), })); }, } } let window = window_from_node(self); // Step 6 let form_data = FormData::new(Some(ret), &window.global()); // Step 7 let event = FormDataEvent::new( &window.global(), atom!("formdata"), EventBubbles::Bubbles, EventCancelable::NotCancelable, &form_data, ); event.upcast::().fire(self.upcast::()); // Step 8 self.constructing_entry_list.set(false); // Step 9 Some(form_data.datums()) } pub fn reset(&self, _reset_method_flag: ResetFrom) { // https://html.spec.whatwg.org/multipage/#locked-for-reset if self.marked_for_reset.get() { return; } else { self.marked_for_reset.set(true); } let event = self .upcast::() .fire_bubbling_cancelable_event(atom!("reset")); if event.DefaultPrevented() { return; } let controls = self.controls.borrow(); for child in controls.iter() { let child = child.upcast::(); match child.type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLInputElement, )) => { child.downcast::().unwrap().reset(); }, NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLSelectElement, )) => { child.downcast::().unwrap().reset(); }, NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLTextAreaElement, )) => { child.downcast::().unwrap().reset(); }, NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLOutputElement, )) => { child.downcast::().unwrap().reset(); }, _ => {}, } } self.marked_for_reset.set(false); } fn add_control(&self, control: &T) { { let root = self.upcast::().root_element(); let root = root.upcast::(); let mut controls = self.controls.borrow_mut(); controls.insert_pre_order(control.to_element(), root); } self.update_validity(); } fn remove_control(&self, control: &T) { { let control = control.to_element(); let mut controls = self.controls.borrow_mut(); controls .iter() .position(|c| &**c == control) .map(|idx| controls.remove(idx)); // https://html.spec.whatwg.org/multipage#forms.html#the-form-element:past-names-map-5 // "If an element listed in a form element's past names map // changes form owner, then its entries must be removed // from that map." let mut past_names_map = self.past_names_map.borrow_mut(); past_names_map.0.retain(|_k, v| v.0 != control); } self.update_validity(); } } #[derive(Clone, JSTraceable, MallocSizeOf)] pub enum FormDatumValue { #[allow(dead_code)] File(DomRoot), String(DOMString), } #[derive(Clone, JSTraceable, MallocSizeOf)] pub struct FormDatum { pub ty: DOMString, pub name: DOMString, pub value: FormDatumValue, } impl FormDatum { pub fn replace_value(&self, charset: &str) -> String { if self.name.to_ascii_lowercase() == "_charset_" && self.ty == "hidden" { return charset.to_string(); } match self.value { FormDatumValue::File(ref f) => String::from(f.name().clone()), FormDatumValue::String(ref s) => String::from(s.clone()), } } } #[derive(Clone, Copy, MallocSizeOf)] pub enum FormEncType { TextPlainEncoded, UrlEncoded, FormDataEncoded, } #[derive(Clone, Copy, MallocSizeOf)] pub enum FormMethod { FormGet, FormPost, FormDialog, } /// #[derive(Clone, Copy, MallocSizeOf)] pub enum FormSubmitter<'a> { FormElement(&'a HTMLFormElement), InputElement(&'a HTMLInputElement), ButtonElement(&'a HTMLButtonElement), // TODO: implement other types of form associated elements // (including custom elements) that can be passed as submitter. } impl<'a> FormSubmitter<'a> { fn action(&self) -> DOMString { match *self { FormSubmitter::FormElement(form) => form.Action(), FormSubmitter::InputElement(input_element) => input_element.get_form_attribute( &local_name!("formaction"), |i| i.FormAction(), |f| f.Action(), ), FormSubmitter::ButtonElement(button_element) => button_element.get_form_attribute( &local_name!("formaction"), |i| i.FormAction(), |f| f.Action(), ), } } fn enctype(&self) -> FormEncType { let attr = match *self { FormSubmitter::FormElement(form) => form.Enctype(), FormSubmitter::InputElement(input_element) => input_element.get_form_attribute( &local_name!("formenctype"), |i| i.FormEnctype(), |f| f.Enctype(), ), FormSubmitter::ButtonElement(button_element) => button_element.get_form_attribute( &local_name!("formenctype"), |i| i.FormEnctype(), |f| f.Enctype(), ), }; match &*attr { "multipart/form-data" => FormEncType::FormDataEncoded, "text/plain" => FormEncType::TextPlainEncoded, // https://html.spec.whatwg.org/multipage/#attr-fs-enctype // urlencoded is the default _ => FormEncType::UrlEncoded, } } fn method(&self) -> FormMethod { let attr = match *self { FormSubmitter::FormElement(form) => form.Method(), FormSubmitter::InputElement(input_element) => input_element.get_form_attribute( &local_name!("formmethod"), |i| i.FormMethod(), |f| f.Method(), ), FormSubmitter::ButtonElement(button_element) => button_element.get_form_attribute( &local_name!("formmethod"), |i| i.FormMethod(), |f| f.Method(), ), }; match &*attr { "dialog" => FormMethod::FormDialog, "post" => FormMethod::FormPost, _ => FormMethod::FormGet, } } fn target(&self) -> DOMString { match *self { FormSubmitter::FormElement(form) => form.Target(), FormSubmitter::InputElement(input_element) => input_element.get_form_attribute( &local_name!("formtarget"), |i| i.FormTarget(), |f| f.Target(), ), FormSubmitter::ButtonElement(button_element) => button_element.get_form_attribute( &local_name!("formtarget"), |i| i.FormTarget(), |f| f.Target(), ), } } fn no_validate(&self, _form_owner: &HTMLFormElement) -> bool { match *self { FormSubmitter::FormElement(form) => form.NoValidate(), FormSubmitter::InputElement(input_element) => input_element.get_form_boolean_attribute( &local_name!("formnovalidate"), |i| i.FormNoValidate(), |f| f.NoValidate(), ), FormSubmitter::ButtonElement(button_element) => button_element .get_form_boolean_attribute( &local_name!("formnovalidate"), |i| i.FormNoValidate(), |f| f.NoValidate(), ), } } // https://html.spec.whatwg.org/multipage/#concept-submit-button fn is_submit_button(&self) -> bool { match *self { // https://html.spec.whatwg.org/multipage/#image-button-state-(type=image) // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit) FormSubmitter::InputElement(input_element) => input_element.is_submit_button(), // https://html.spec.whatwg.org/multipage/#attr-button-type-submit-state FormSubmitter::ButtonElement(button_element) => button_element.is_submit_button(), _ => false, } } // https://html.spec.whatwg.org/multipage/#form-owner fn form_owner(&self) -> Option> { match *self { FormSubmitter::ButtonElement(button_el) => button_el.form_owner(), FormSubmitter::InputElement(input_el) => input_el.form_owner(), _ => None, } } } pub trait FormControl: DomObject { fn form_owner(&self) -> Option>; fn set_form_owner(&self, form: Option<&HTMLFormElement>); fn to_element(&self) -> ∈ fn is_listed(&self) -> bool { true } // https://html.spec.whatwg.org/multipage/#create-an-element-for-the-token // Part of step 12. // '..suppress the running of the reset the form owner algorithm // when the parser subsequently attempts to insert the element..' fn set_form_owner_from_parser(&self, form: &HTMLFormElement) { let elem = self.to_element(); let node = elem.upcast::(); node.set_flag(NodeFlags::PARSER_ASSOCIATED_FORM_OWNER, true); form.add_control(self); self.set_form_owner(Some(form)); } // https://html.spec.whatwg.org/multipage/#reset-the-form-owner fn reset_form_owner(&self) { let elem = self.to_element(); let node = elem.upcast::(); let old_owner = self.form_owner(); let has_form_id = elem.has_attribute(&local_name!("form")); let nearest_form_ancestor = node .ancestors() .filter_map(DomRoot::downcast::) .next(); // Step 1 if old_owner.is_some() && !(self.is_listed() && has_form_id) && nearest_form_ancestor == old_owner { return; } let new_owner = if self.is_listed() && has_form_id && elem.is_connected() { // Step 3 let doc = document_from_node(node); let form_id = elem.get_string_attribute(&local_name!("form")); doc.GetElementById(form_id) .and_then(DomRoot::downcast::) } else { // Step 4 nearest_form_ancestor }; if old_owner != new_owner { if let Some(o) = old_owner { o.remove_control(self); } if let Some(ref new_owner) = new_owner { new_owner.add_control(self); } self.set_form_owner(new_owner.as_deref()); } } // https://html.spec.whatwg.org/multipage/#association-of-controls-and-forms fn form_attribute_mutated(&self, mutation: AttributeMutation) { match mutation { AttributeMutation::Set(_) => { self.register_if_necessary(); }, AttributeMutation::Removed => { self.unregister_if_necessary(); }, } self.reset_form_owner(); } // https://html.spec.whatwg.org/multipage/#association-of-controls-and-forms fn register_if_necessary(&self) { let elem = self.to_element(); let form_id = elem.get_string_attribute(&local_name!("form")); let node = elem.upcast::(); if self.is_listed() && !form_id.is_empty() && node.is_connected() { let doc = document_from_node(node); doc.register_form_id_listener(form_id, self); } } fn unregister_if_necessary(&self) { let elem = self.to_element(); let form_id = elem.get_string_attribute(&local_name!("form")); if self.is_listed() && !form_id.is_empty() { let doc = document_from_node(elem.upcast::()); doc.unregister_form_id_listener(form_id, self); } } // https://html.spec.whatwg.org/multipage/#association-of-controls-and-forms fn bind_form_control_to_tree(&self) { let elem = self.to_element(); let node = elem.upcast::(); // https://html.spec.whatwg.org/multipage/#create-an-element-for-the-token // Part of step 12. // '..suppress the running of the reset the form owner algorithm // when the parser subsequently attempts to insert the element..' let must_skip_reset = node.get_flag(NodeFlags::PARSER_ASSOCIATED_FORM_OWNER); node.set_flag(NodeFlags::PARSER_ASSOCIATED_FORM_OWNER, false); if !must_skip_reset { self.form_attribute_mutated(AttributeMutation::Set(None)); } } // https://html.spec.whatwg.org/multipage/#association-of-controls-and-forms fn unbind_form_control_from_tree(&self) { let elem = self.to_element(); let has_form_attr = elem.has_attribute(&local_name!("form")); let same_subtree = self .form_owner() .map_or(true, |form| elem.is_in_same_home_subtree(&*form)); self.unregister_if_necessary(); // Since this control has been unregistered from the id->listener map // in the previous step, reset_form_owner will not be invoked on it // when the form owner element is unbound (i.e it is in the same // subtree) if it appears later in the tree order. Hence invoke // reset from here if this control has the form attribute set. if !same_subtree || (self.is_listed() && has_form_attr) { self.reset_form_owner(); } } fn get_form_attribute( &self, attr: &LocalName, input: InputFn, owner: OwnerFn, ) -> DOMString where InputFn: Fn(&Self) -> DOMString, OwnerFn: Fn(&HTMLFormElement) -> DOMString, Self: Sized, { if self.to_element().has_attribute(attr) { input(self) } else { self.form_owner().map_or(DOMString::new(), |t| owner(&t)) } } fn get_form_boolean_attribute( &self, attr: &LocalName, input: InputFn, owner: OwnerFn, ) -> bool where InputFn: Fn(&Self) -> bool, OwnerFn: Fn(&HTMLFormElement) -> bool, Self: Sized, { if self.to_element().has_attribute(attr) { input(self) } else { self.form_owner().map_or(false, |t| owner(&t)) } } // XXXKiChjang: Implement these on inheritors // fn candidate_for_validation(&self) -> bool; // fn satisfies_constraints(&self) -> bool; } impl VirtualMethods for HTMLFormElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::() as &dyn VirtualMethods) } fn unbind_from_tree(&self, context: &UnbindContext) { self.super_type().unwrap().unbind_from_tree(context); // Collect the controls to reset because reset_form_owner // will mutably borrow self.controls rooted_vec!(let mut to_reset); to_reset.extend( self.controls .borrow() .iter() .filter(|c| !c.is_in_same_home_subtree(self)) .cloned(), ); for control in to_reset.iter() { control .as_maybe_form_control() .expect("Element must be a form control") .reset_form_owner(); } } fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { match name { &local_name!("rel") => AttrValue::from_serialized_tokenlist(value.into()), _ => self .super_type() .unwrap() .parse_plain_attribute(name, value), } } } pub trait FormControlElementHelpers { fn as_maybe_form_control(&self) -> Option<&dyn FormControl>; } impl FormControlElementHelpers for Element { fn as_maybe_form_control(&self) -> Option<&dyn FormControl> { let node = self.upcast::(); match node.type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLButtonElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLFieldSetElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLImageElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLInputElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLLabelElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLLegendElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLObjectElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLOutputElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLSelectElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLTextAreaElement, )) => Some(self.downcast::().unwrap() as &dyn FormControl), _ => None, } } } // https://html.spec.whatwg.org/multipage/#multipart/form-data-encoding-algorithm pub fn encode_multipart_form_data( form_data: &mut [FormDatum], boundary: String, encoding: &'static Encoding, ) -> Vec { // Step 1 let mut result = vec![]; // Step 2 for entry in form_data.iter_mut() { // TODO: Step 2.1 // Step 3 // https://tools.ietf.org/html/rfc7578#section-4 // NOTE(izgzhen): The encoding here expected by most servers seems different from // what spec says (that it should start with a '\r\n'). let mut boundary_bytes = format!("--{}\r\n", boundary).into_bytes(); result.append(&mut boundary_bytes); // TODO(eijebong): Everthing related to content-disposition it to redo once typed headers // are capable of it. match entry.value { FormDatumValue::String(ref s) => { let content_disposition = format!("form-data; name=\"{}\"", entry.name); let mut bytes = format!("Content-Disposition: {}\r\n\r\n{}", content_disposition, s) .into_bytes(); result.append(&mut bytes); }, FormDatumValue::File(ref f) => { let charset = encoding.name(); let extra = if charset.to_lowercase() == "utf-8" { format!( "filename=\"{}\"", String::from_utf8(f.name().as_bytes().into()).unwrap() ) } else { format!( "filename*=\"{}\"''{}", charset, http_percent_encode(f.name().as_bytes()) ) }; let content_disposition = format!("form-data; name=\"{}\"; {}", entry.name, extra); // https://tools.ietf.org/html/rfc7578#section-4.4 let content_type: Mime = f .upcast::() .Type() .parse() .unwrap_or(mime::TEXT_PLAIN); let mut type_bytes = format!( "Content-Disposition: {}\r\ncontent-type: {}\r\n\r\n", content_disposition, content_type ) .into_bytes(); result.append(&mut type_bytes); let mut bytes = f.upcast::().get_bytes().unwrap_or(vec![]); result.append(&mut bytes); }, } } let mut boundary_bytes = format!("\r\n--{}--\r\n", boundary).into_bytes(); result.append(&mut boundary_bytes); result } // https://tools.ietf.org/html/rfc7578#section-4.1 pub fn generate_boundary() -> String { let i1 = random::(); let i2 = random::(); format!("---------------------------{0}{1}", i1, i2) }