mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Clean-up navigation <!-- Please describe your changes on the following line: --> 1. Navigation as a result of following a hyperlink should be done in a task: https://html.spec.whatwg.org/multipage/links.html#following-hyperlinks:dom-manipulation-task-source 2. The javascript url navigation should also be done in a task: https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigating-across-documents:dom-manipulation-task-source 3. In `window.load_url`, it seems there is no need to send a message to the script-thread(the entirety of `load_url` should instead be done in a task when appropriate), so we can just do that last part "sync" by calling a method on the script, which will send a message to the constellation(for the parallel navigation steps), or queue task(for the JS navigation), as appropriate. 4. Separate the "normal" navigation flow from the handling of "navigate an iframe" message from constellation, since doing everything in one method as was previously done with `handle_navigate`, is confusing. --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [ ] `./mach build -d` does not report any errors - [ ] `./mach test-tidy` does not report any errors - [ ] These changes fix #___ (GitHub issue number if applicable) <!-- Either: --> - [ ] There are tests for these changes OR - [ ] These changes do not require tests because ___ <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/23368) <!-- Reviewable:end -->
1381 lines
50 KiB
Rust
Executable file
1381 lines
50 KiB
Rust
Executable file
/* 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 crate::dom::bindings::cell::DomRefCell;
|
|
use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrBinding::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::HTMLFormControlsCollectionBinding::HTMLFormControlsCollectionMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding;
|
|
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::WindowBinding::WindowBinding::WindowMethods;
|
|
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};
|
|
use crate::dom::bindings::str::DOMString;
|
|
use crate::dom::blob::Blob;
|
|
use crate::dom::document::Document;
|
|
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::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};
|
|
use crate::dom::node::{Node, NodeFlags, ShadowIncluding};
|
|
use crate::dom::node::{UnbindContext, VecPreOrderInsertionHelper};
|
|
use crate::dom::validitystate::ValidationFlags;
|
|
use crate::dom::virtualmethods::VirtualMethods;
|
|
use crate::dom::window::Window;
|
|
use crate::task_source::TaskSource;
|
|
use dom_struct::dom_struct;
|
|
use encoding_rs::{Encoding, UTF_8};
|
|
use headers::{ContentType, HeaderMapExt};
|
|
use html5ever::{LocalName, Prefix};
|
|
use hyper::Method;
|
|
use mime::{self, Mime};
|
|
use net_traits::http_percent_encode;
|
|
use net_traits::request::Referrer;
|
|
use script_traits::{HistoryEntryReplacement, LoadData, LoadOrigin};
|
|
use servo_rand::random;
|
|
use std::borrow::ToOwned;
|
|
use std::cell::Cell;
|
|
use style::attr::AttrValue;
|
|
use style::str::split_html_space_chars;
|
|
use url::form_urlencoded::Serializer;
|
|
use url::UrlQuery;
|
|
|
|
#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
|
|
pub struct GenerationId(u32);
|
|
|
|
#[dom_struct]
|
|
pub struct HTMLFormElement {
|
|
htmlelement: HTMLElement,
|
|
marked_for_reset: Cell<bool>,
|
|
/// https://html.spec.whatwg.org/multipage/#constructing-entry-list
|
|
constructing_entry_list: Cell<bool>,
|
|
elements: DomOnceCell<HTMLFormControlsCollection>,
|
|
generation_id: Cell<GenerationId>,
|
|
controls: DomRefCell<Vec<Dom<Element>>>,
|
|
}
|
|
|
|
impl HTMLFormElement {
|
|
fn new_inherited(
|
|
local_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
) -> HTMLFormElement {
|
|
HTMLFormElement {
|
|
htmlelement: HTMLElement::new_inherited(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()),
|
|
}
|
|
}
|
|
|
|
#[allow(unrooted_must_root)]
|
|
pub fn new(
|
|
local_name: LocalName,
|
|
prefix: Option<Prefix>,
|
|
document: &Document,
|
|
) -> DomRoot<HTMLFormElement> {
|
|
Node::reflect_node(
|
|
Box::new(HTMLFormElement::new_inherited(local_name, prefix, document)),
|
|
document,
|
|
HTMLFormElementBinding::Wrap,
|
|
)
|
|
}
|
|
}
|
|
|
|
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/#the-form-element:concept-form-submit
|
|
fn Submit(&self) {
|
|
self.submit(SubmittedFrom::FromForm, FormSubmitter::FormElement(self));
|
|
}
|
|
|
|
// 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<HTMLFormControlsCollection> {
|
|
#[derive(JSTraceable, MallocSizeOf)]
|
|
struct ElementsFilter {
|
|
form: DomRoot<HTMLFormElement>,
|
|
}
|
|
impl CollectionFilter for ElementsFilter {
|
|
fn filter<'a>(&self, elem: &'a Element, _root: &'a Node) -> bool {
|
|
let form_owner = match elem.upcast::<Node>().type_id() {
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(t)) => match t {
|
|
HTMLElementTypeId::HTMLButtonElement => {
|
|
elem.downcast::<HTMLButtonElement>().unwrap().form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLFieldSetElement => {
|
|
elem.downcast::<HTMLFieldSetElement>().unwrap().form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLInputElement => {
|
|
let input_elem = elem.downcast::<HTMLInputElement>().unwrap();
|
|
if input_elem.input_type() == InputType::Image {
|
|
return false;
|
|
}
|
|
input_elem.form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLObjectElement => {
|
|
elem.downcast::<HTMLObjectElement>().unwrap().form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLOutputElement => {
|
|
elem.downcast::<HTMLOutputElement>().unwrap().form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLSelectElement => {
|
|
elem.downcast::<HTMLSelectElement>().unwrap().form_owner()
|
|
},
|
|
HTMLElementTypeId::HTMLTextAreaElement => {
|
|
elem.downcast::<HTMLTextAreaElement>().unwrap().form_owner()
|
|
},
|
|
_ => {
|
|
debug_assert!(
|
|
!elem.downcast::<HTMLElement>().unwrap().is_listed_element() ||
|
|
elem.local_name() == &local_name!("keygen")
|
|
);
|
|
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.upcast(), filter)
|
|
}))
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-form-length
|
|
fn Length(&self) -> u32 {
|
|
self.Elements().Length() as u32
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-form-item
|
|
fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
|
|
let elements = self.Elements();
|
|
elements.IndexedGetter(index)
|
|
}
|
|
}
|
|
|
|
#[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::<Element>()
|
|
.has_attribute(&local_name!("accept-charset"))
|
|
{
|
|
// Substep 1
|
|
let input = self
|
|
.upcast::<Element>()
|
|
.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<FormDatum>) -> String {
|
|
// Step 1
|
|
let mut result = String::new();
|
|
|
|
// Step 2
|
|
let encoding = self.pick_encoding();
|
|
|
|
// Step 3
|
|
let charset = encoding.name();
|
|
|
|
for entry in form_data.iter_mut() {
|
|
// Step 4, 5
|
|
let value = entry.replace_value(charset);
|
|
|
|
// Step 6
|
|
result.push_str(&*format!("{}={}\r\n", entry.name, value));
|
|
}
|
|
|
|
// Step 7
|
|
result
|
|
}
|
|
|
|
/// [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::<Element>().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 && !submitter.no_validate(self) {
|
|
if self.interactive_validation().is_err() {
|
|
// TODO: Implement event handlers on all form control elements
|
|
self.upcast::<EventTarget>().fire_event(atom!("invalid"));
|
|
return;
|
|
}
|
|
}
|
|
// Step 7
|
|
if submit_method_flag == SubmittedFrom::NotFromForm {
|
|
let event = self
|
|
.upcast::<EventTarget>()
|
|
.fire_bubbling_cancelable_event(atom!("submit"));
|
|
if event.DefaultPrevented() {
|
|
return;
|
|
}
|
|
|
|
// Step 7-3
|
|
if self.upcast::<Element>().cannot_navigate() {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Step 8
|
|
let encoding = self.pick_encoding();
|
|
|
|
// Step 9
|
|
let mut form_data = match self.get_form_dataset(Some(submitter)) {
|
|
Some(form_data) => form_data,
|
|
None => return,
|
|
};
|
|
|
|
// Step 10
|
|
if self.upcast::<Element>().cannot_navigate() {
|
|
return;
|
|
}
|
|
|
|
// Step 11
|
|
let mut action = submitter.action();
|
|
|
|
// Step 12
|
|
if action.is_empty() {
|
|
action = DOMString::from(base.as_str());
|
|
}
|
|
// Step 13-14
|
|
let action_components = match base.join(&action) {
|
|
Ok(url) => url,
|
|
Err(_) => return,
|
|
};
|
|
// Step 15-17
|
|
let scheme = action_components.scheme().to_owned();
|
|
let enctype = submitter.enctype();
|
|
let method = submitter.method();
|
|
|
|
// Step 18-21
|
|
let target_attribute_value = submitter.target();
|
|
let source = doc.browsing_context().unwrap();
|
|
let (maybe_chosen, _new) = source.choose_browsing_context(target_attribute_value, false);
|
|
let chosen = match maybe_chosen {
|
|
Some(proxy) => proxy,
|
|
None => return,
|
|
};
|
|
let target_document = match chosen.document() {
|
|
Some(doc) => doc,
|
|
None => return,
|
|
};
|
|
let target_window = target_document.window();
|
|
let mut load_data = LoadData::new(
|
|
LoadOrigin::Script(doc.origin().immutable().clone()),
|
|
action_components,
|
|
None,
|
|
Some(Referrer::ReferrerUrl(target_document.url())),
|
|
target_document.get_referrer_policy(),
|
|
);
|
|
|
|
// 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
|
|
},
|
|
_ => return,
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#submit-mutate-action
|
|
fn mutate_action_url(
|
|
&self,
|
|
form_data: &mut Vec<FormDatum>,
|
|
mut load_data: LoadData,
|
|
encoding: &'static Encoding,
|
|
target: &Window,
|
|
) {
|
|
let charset = encoding.name();
|
|
|
|
self.set_encoding_override(load_data.url.as_mut_url().query_pairs_mut())
|
|
.clear()
|
|
.extend_pairs(
|
|
form_data
|
|
.into_iter()
|
|
.map(|field| (field.name.clone(), 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<FormDatum>,
|
|
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_encoding_override(url.as_mut_url().query_pairs_mut())
|
|
.clear()
|
|
.extend_pairs(
|
|
form_data
|
|
.into_iter()
|
|
.map(|field| (field.name.clone(), 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()
|
|
},
|
|
};
|
|
|
|
load_data.data = Some(bytes);
|
|
self.plan_to_navigate(load_data, target);
|
|
}
|
|
|
|
fn set_encoding_override<'a>(
|
|
&self,
|
|
mut serializer: Serializer<UrlQuery<'a>>,
|
|
) -> Serializer<UrlQuery<'a>> {
|
|
let encoding = self.pick_encoding();
|
|
serializer.custom_encoding_override(move |s| encoding.encode(s).0);
|
|
serializer
|
|
}
|
|
|
|
/// [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::<Element>();
|
|
let referrer = match elem.get_attribute(&ns!(), &local_name!("rel")) {
|
|
Some(ref link_types) if link_types.Value().contains("noreferrer") => {
|
|
Referrer::NoReferrer
|
|
},
|
|
_ => Referrer::Client,
|
|
};
|
|
|
|
let referrer_policy = target.Document().get_referrer_policy();
|
|
let pipeline_id = target.upcast::<GlobalScope>().pipeline_id();
|
|
load_data.creator_pipeline_id = Some(pipeline_id);
|
|
load_data.referrer = Some(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
|
|
/// <https://html.spec.whatwg.org/multipage/#interactively-validate-the-constraints>
|
|
fn interactive_validation(&self) -> Result<(), ()> {
|
|
// Step 1-3
|
|
let _unhandled_invalid_controls = match self.static_validation() {
|
|
Ok(()) => return Ok(()),
|
|
Err(err) => err,
|
|
};
|
|
// TODO: Report the problems with the constraints of at least one of
|
|
// the elements given in unhandled invalid controls to the user
|
|
// Step 4
|
|
Err(())
|
|
}
|
|
|
|
/// Statitically validate the constraints of form elements
|
|
/// <https://html.spec.whatwg.org/multipage/#statically-validate-the-constraints>
|
|
fn static_validation(&self) -> Result<(), Vec<FormSubmittableElement>> {
|
|
let node = self.upcast::<Node>();
|
|
// FIXME(#3553): This is an incorrect way of getting controls owned by the
|
|
// form, refactor this when html5ever's form owner PR lands
|
|
// Step 1-3
|
|
let invalid_controls = node
|
|
.traverse_preorder(ShadowIncluding::No)
|
|
.filter_map(|field| {
|
|
if let Some(el) = field.downcast::<Element>() {
|
|
if el.disabled_state() {
|
|
None
|
|
} else {
|
|
let validatable = match el.as_maybe_validatable() {
|
|
Some(v) => v,
|
|
None => return None,
|
|
};
|
|
if !validatable.is_instance_validatable() {
|
|
None
|
|
} else if validatable.validate(ValidationFlags::empty()) {
|
|
None
|
|
} else {
|
|
Some(FormSubmittableElement::from_element(&el))
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<FormSubmittableElement>>();
|
|
// 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
|
|
.as_event_target()
|
|
.fire_cancelable_event(atom!("invalid"));
|
|
if !event.DefaultPrevented() {
|
|
return Some(field);
|
|
}
|
|
None
|
|
})
|
|
.collect::<Vec<FormSubmittableElement>>();
|
|
// Step 7
|
|
Err(unhandled_invalid_controls)
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set>
|
|
/// Steps range from 3 to 5
|
|
fn get_unclean_dataset(&self, submitter: Option<FormSubmitter>) -> Vec<FormDatum> {
|
|
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::<Node>();
|
|
|
|
// Step 5.1: The field element has a datalist element ancestor.
|
|
if child
|
|
.ancestors()
|
|
.any(|a| DomRoot::downcast::<HTMLDataListElement>(a).is_some())
|
|
{
|
|
continue;
|
|
}
|
|
if let NodeTypeId::Element(ElementTypeId::HTMLElement(element)) = child.type_id() {
|
|
match element {
|
|
HTMLElementTypeId::HTMLInputElement => {
|
|
let input = child.downcast::<HTMLInputElement>().unwrap();
|
|
|
|
data_set.append(&mut input.form_datums(submitter));
|
|
},
|
|
HTMLElementTypeId::HTMLButtonElement => {
|
|
let button = child.downcast::<HTMLButtonElement>().unwrap();
|
|
if let Some(datum) = button.form_datum(submitter) {
|
|
data_set.push(datum);
|
|
}
|
|
},
|
|
HTMLElementTypeId::HTMLObjectElement => {
|
|
// Unimplemented
|
|
()
|
|
},
|
|
HTMLElementTypeId::HTMLSelectElement => {
|
|
let select = child.downcast::<HTMLSelectElement>().unwrap();
|
|
select.push_form_data(&mut data_set);
|
|
},
|
|
HTMLElementTypeId::HTMLTextAreaElement => {
|
|
let textarea = child.downcast::<HTMLTextAreaElement>().unwrap();
|
|
let name = textarea.Name();
|
|
if !name.is_empty() {
|
|
data_set.push(FormDatum {
|
|
ty: textarea.Type(),
|
|
name: name,
|
|
value: FormDatumValue::String(textarea.Value()),
|
|
});
|
|
}
|
|
},
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
data_set
|
|
// TODO: Handle `dirnames` (needs directionality support)
|
|
// https://html.spec.whatwg.org/multipage/#the-directionality
|
|
}
|
|
|
|
/// <https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set>
|
|
pub fn get_form_dataset(&self, submitter: Option<FormSubmitter>) -> Option<Vec<FormDatum>> {
|
|
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);
|
|
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::<Event>().fire(self.upcast::<EventTarget>());
|
|
|
|
// 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::<EventTarget>()
|
|
.fire_bubbling_cancelable_event(atom!("reset"));
|
|
if event.DefaultPrevented() {
|
|
return;
|
|
}
|
|
|
|
let controls = self.controls.borrow();
|
|
for child in controls.iter() {
|
|
let child = child.upcast::<Node>();
|
|
|
|
match child.type_id() {
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLInputElement,
|
|
)) => {
|
|
child.downcast::<HTMLInputElement>().unwrap().reset();
|
|
},
|
|
// TODO HTMLKeygenElement unimplemented
|
|
//NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLKeygenElement)) => {
|
|
// // Unimplemented
|
|
// {}
|
|
//}
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLSelectElement,
|
|
)) => {
|
|
child.downcast::<HTMLSelectElement>().unwrap().reset();
|
|
},
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLTextAreaElement,
|
|
)) => {
|
|
child.downcast::<HTMLTextAreaElement>().unwrap().reset();
|
|
},
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLOutputElement,
|
|
)) => {
|
|
// Unimplemented
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
self.marked_for_reset.set(false);
|
|
}
|
|
|
|
fn add_control<T: ?Sized + FormControl>(&self, control: &T) {
|
|
let root = self.upcast::<Element>().root_element();
|
|
let root = root.upcast::<Node>();
|
|
|
|
let mut controls = self.controls.borrow_mut();
|
|
controls.insert_pre_order(control.to_element(), root);
|
|
}
|
|
|
|
fn remove_control<T: ?Sized + FormControl>(&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));
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
|
pub enum FormDatumValue {
|
|
#[allow(dead_code)]
|
|
File(DomRoot<File>),
|
|
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 == "_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(MallocSizeOf)]
|
|
#[allow(dead_code)]
|
|
pub enum FormSubmittableElement {
|
|
ButtonElement(DomRoot<HTMLButtonElement>),
|
|
InputElement(DomRoot<HTMLInputElement>),
|
|
// TODO: HTMLKeygenElement unimplemented
|
|
// KeygenElement(&'a HTMLKeygenElement),
|
|
ObjectElement(DomRoot<HTMLObjectElement>),
|
|
SelectElement(DomRoot<HTMLSelectElement>),
|
|
TextAreaElement(DomRoot<HTMLTextAreaElement>),
|
|
}
|
|
|
|
impl FormSubmittableElement {
|
|
fn as_event_target(&self) -> &EventTarget {
|
|
match *self {
|
|
FormSubmittableElement::ButtonElement(ref button) => button.upcast(),
|
|
FormSubmittableElement::InputElement(ref input) => input.upcast(),
|
|
FormSubmittableElement::ObjectElement(ref object) => object.upcast(),
|
|
FormSubmittableElement::SelectElement(ref select) => select.upcast(),
|
|
FormSubmittableElement::TextAreaElement(ref textarea) => textarea.upcast(),
|
|
}
|
|
}
|
|
|
|
fn from_element(element: &Element) -> FormSubmittableElement {
|
|
if let Some(input) = element.downcast::<HTMLInputElement>() {
|
|
FormSubmittableElement::InputElement(DomRoot::from_ref(&input))
|
|
} else if let Some(input) = element.downcast::<HTMLButtonElement>() {
|
|
FormSubmittableElement::ButtonElement(DomRoot::from_ref(&input))
|
|
} else if let Some(input) = element.downcast::<HTMLObjectElement>() {
|
|
FormSubmittableElement::ObjectElement(DomRoot::from_ref(&input))
|
|
} else if let Some(input) = element.downcast::<HTMLSelectElement>() {
|
|
FormSubmittableElement::SelectElement(DomRoot::from_ref(&input))
|
|
} else if let Some(input) = element.downcast::<HTMLTextAreaElement>() {
|
|
FormSubmittableElement::TextAreaElement(DomRoot::from_ref(&input))
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, MallocSizeOf)]
|
|
pub enum FormSubmitter<'a> {
|
|
FormElement(&'a HTMLFormElement),
|
|
InputElement(&'a HTMLInputElement),
|
|
ButtonElement(&'a HTMLButtonElement), // TODO: image submit, etc etc
|
|
}
|
|
|
|
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(),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait FormControl: DomObject {
|
|
fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>>;
|
|
|
|
fn set_form_owner(&self, form: Option<&HTMLFormElement>);
|
|
|
|
fn to_element<'a>(&'a self) -> &'a Element;
|
|
|
|
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>();
|
|
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::<Node>();
|
|
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::<HTMLFormElement>)
|
|
.next();
|
|
|
|
// Step 1
|
|
if old_owner.is_some() && !(self.is_listed() && has_form_id) {
|
|
if 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::<HTMLFormElement>)
|
|
} 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.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::<Node>();
|
|
|
|
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::<Node>());
|
|
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::<Node>();
|
|
|
|
// 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<InputFn, OwnerFn>(
|
|
&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<InputFn, OwnerFn>(
|
|
&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::<HTMLElement>() as &dyn VirtualMethods)
|
|
}
|
|
|
|
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
|
|
match name {
|
|
&local_name!("name") => AttrValue::from_atomic(value.into()),
|
|
_ => self
|
|
.super_type()
|
|
.unwrap()
|
|
.parse_plain_attribute(name, value),
|
|
}
|
|
}
|
|
|
|
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))
|
|
.map(|c| c.clone()),
|
|
);
|
|
|
|
for control in to_reset.iter() {
|
|
control
|
|
.as_maybe_form_control()
|
|
.expect("Element must be a form control")
|
|
.reset_form_owner();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait FormControlElementHelpers {
|
|
fn as_maybe_form_control<'a>(&'a self) -> Option<&'a dyn FormControl>;
|
|
}
|
|
|
|
impl FormControlElementHelpers for Element {
|
|
fn as_maybe_form_control<'a>(&'a self) -> Option<&'a dyn FormControl> {
|
|
let node = self.upcast::<Node>();
|
|
|
|
match node.type_id() {
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLButtonElement,
|
|
)) => Some(self.downcast::<HTMLButtonElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLFieldSetElement,
|
|
)) => Some(self.downcast::<HTMLFieldSetElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLImageElement,
|
|
)) => Some(self.downcast::<HTMLImageElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLInputElement,
|
|
)) => Some(self.downcast::<HTMLInputElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLLabelElement,
|
|
)) => Some(self.downcast::<HTMLLabelElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLLegendElement,
|
|
)) => Some(self.downcast::<HTMLLegendElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLObjectElement,
|
|
)) => Some(self.downcast::<HTMLObjectElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLOutputElement,
|
|
)) => Some(self.downcast::<HTMLOutputElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLSelectElement,
|
|
)) => Some(self.downcast::<HTMLSelectElement>().unwrap() as &dyn FormControl),
|
|
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
HTMLElementTypeId::HTMLTextAreaElement,
|
|
)) => Some(self.downcast::<HTMLTextAreaElement>().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 Vec<FormDatum>,
|
|
boundary: String,
|
|
encoding: &'static Encoding,
|
|
) -> Vec<u8> {
|
|
// Step 1
|
|
let mut result = vec![];
|
|
|
|
// Step 2
|
|
let charset = encoding.name();
|
|
|
|
// Step 3
|
|
for entry in form_data.iter_mut() {
|
|
// 3.1
|
|
if entry.name == "_charset_" && entry.ty == "hidden" {
|
|
entry.value = FormDatumValue::String(DOMString::from(charset.clone()));
|
|
}
|
|
// TODO: 3.2
|
|
|
|
// Step 4
|
|
// 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 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::<Blob>()
|
|
.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::<Blob>().get_bytes().unwrap_or(vec![]);
|
|
|
|
result.append(&mut bytes);
|
|
},
|
|
}
|
|
}
|
|
|
|
let mut boundary_bytes = format!("\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::<u32>();
|
|
let i2 = random::<u32>();
|
|
|
|
format!("---------------------------{0}{1}", i1, i2)
|
|
}
|