diff --git a/components/script/dom/filelist.rs b/components/script/dom/filelist.rs index 96bfc4ce0d3e..4f8e976f5f7d 100644 --- a/components/script/dom/filelist.rs +++ b/components/script/dom/filelist.rs @@ -9,6 +9,7 @@ use dom::bindings::js::{JS, Root}; use dom::bindings::reflector::{Reflector, reflect_dom_object}; use dom::file::File; use dom::window::Window; +use std::slice::Iter; // https://w3c.github.io/FileAPI/#dfn-filelist #[dom_struct] @@ -32,6 +33,10 @@ impl FileList { GlobalRef::Window(window), FileListBinding::Wrap) } + + pub fn iter_files(&self) -> Iter> { + self.list.iter() + } } impl FileListMethods for FileList { diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index 9d7ce208ee2b..72b8cc19c169 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -37,11 +37,9 @@ use dom::htmlselectelement::HTMLSelectElement; use dom::htmltextareaelement::HTMLTextAreaElement; use dom::node::{Node, document_from_node, window_from_node}; use dom::virtualmethods::VirtualMethods; -use dom::window::Window; use encoding::EncodingRef; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; -use encoding::types::DecoderTrap; use hyper::header::{Charset, ContentDisposition, ContentType, DispositionParam, DispositionType}; use hyper::method::Method; use msg::constellation_msg::{LoadData, PipelineId}; @@ -54,7 +52,6 @@ use string_cache::Atom; use style::attr::AttrValue; use style::str::split_html_space_chars; use task_source::TaskSource; -use url::form_urlencoded; #[derive(JSTraceable, PartialEq, Clone, Copy, HeapSizeOf)] pub struct GenerationId(u32); @@ -280,39 +277,38 @@ impl HTMLFormElement { // https://html.spec.whatwg.org/multipage/#multipart/form-data-encoding-algorithm fn encode_multipart_form_data(&self, form_data: &mut Vec, - encoding: Option, - boundary: String) -> String { + boundary: String, encoding: EncodingRef) -> Vec { // Step 1 - let mut result = "".to_owned(); + let mut result = vec![]; // Step 2 - // (maybe take encoding as input) - let encoding = encoding.unwrap_or(self.pick_encoding()); - - // Step 3 let charset = &*encoding.whatwg_name().unwrap_or("UTF-8"); - // Step 4 + // Step 3 for entry in form_data.iter_mut() { - // Substep 1 + // 3.1 if entry.name == "_charset_" && entry.ty == "hidden" { entry.value = FormDatumValue::String(DOMString::from(charset.clone())); } - // TODO: Substep 2 + // TODO: 3.2 - // Step 5 + // Step 4 // https://tools.ietf.org/html/rfc7578#section-4 - result.push_str(&*format!("\r\n--{}\r\n", boundary)); + // 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); let mut content_disposition = ContentDisposition { disposition: DispositionType::Ext("form-data".to_owned()), parameters: vec![DispositionParam::Ext("name".to_owned(), String::from(entry.name.clone()))] }; match entry.value { - FormDatumValue::String(ref s) => - result.push_str(&*format!("Content-Disposition: {}\r\n\r\n{}", - content_disposition, - s)), + FormDatumValue::String(ref s) => { + let mut bytes = format!("Content-Disposition: {}\r\n\r\n{}", + content_disposition, s).into_bytes(); + result.append(&mut bytes); + } FormDatumValue::File(ref f) => { content_disposition.parameters.push( DispositionParam::Filename(Charset::Ext(String::from(charset.clone())), @@ -321,20 +317,20 @@ impl HTMLFormElement { // https://tools.ietf.org/html/rfc7578#section-4.4 let content_type = ContentType(f.upcast::().Type() .parse().unwrap_or(mime!(Text / Plain))); - result.push_str(&*format!("Content-Disposition: {}\r\n{}\r\n\r\n", - content_disposition, - content_type)); + let mut type_bytes = format!("Content-Disposition: {}\r\n{}\r\n\r\n", + content_disposition, + content_type).into_bytes(); + result.append(&mut type_bytes); - let bytes = &f.upcast::().get_bytes().unwrap_or(vec![])[..]; + let mut bytes = f.upcast::().get_bytes().unwrap_or(vec![]); - let decoded = encoding.decode(bytes, DecoderTrap::Replace) - .expect("Invalid encoding in file"); - result.push_str(&decoded); + result.append(&mut bytes); } } } - result.push_str(&*format!("\r\n--{}--", boundary)); + let mut boundary_bytes = format!("\r\n--{}--", boundary).into_bytes(); + result.append(&mut boundary_bytes); result } @@ -351,18 +347,11 @@ impl HTMLFormElement { let charset = &*encoding.whatwg_name().unwrap(); for entry in form_data.iter_mut() { - // Step 4 - if entry.name == "_charset_" && entry.ty == "hidden" { - entry.value = FormDatumValue::String(DOMString::from(charset.clone())); - } - - // Step 5 - if entry.ty == "file" { - entry.value = FormDatumValue::String(DOMString::from(entry.value_str())); - } + // Step 4, 5 + let value = entry.replace_value(charset); // Step 6 - result.push_str(&*format!("{}={}\r\n", entry.name, entry.value_str())); + result.push_str(&*format!("{}={}\r\n", entry.name, value)); } // Step 7 @@ -374,7 +363,7 @@ impl HTMLFormElement { // Step 1 let doc = document_from_node(self); let base = doc.url(); - // TODO: Handle browsing contexts + // TODO: Handle browsing contexts (Step 2, 3) // Step 4 if submit_method_flag == SubmittedFrom::NotFromForm && !submitter.no_validate(self) @@ -397,13 +386,18 @@ impl HTMLFormElement { } // Step 6 let mut form_data = self.get_form_dataset(Some(submitter)); + // Step 7 - let mut action = submitter.action(); + let encoding = self.pick_encoding(); + // Step 8 + let mut action = submitter.action(); + + // Step 9 if action.is_empty() { action = DOMString::from(base.as_str()); } - // Step 9-11 + // Step 10-11 let action_components = match base.join(&action) { Ok(url) => url, Err(_) => return @@ -417,57 +411,87 @@ impl HTMLFormElement { let mut load_data = LoadData::new(action_components, doc.get_referrer_policy(), Some(doc.url().clone())); - let parsed_data = match enctype { + // Step 18 + 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.set(ContentType::form_url_encoded()); + self.mutate_action_url(&mut form_data, load_data, encoding); + } + // 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); + } + // https://html.spec.whatwg.org/multipage/#submit-get-action + ("file", _) | ("about", _) | ("data", FormMethod::FormPost) | + ("ftp", _) | ("javascript", _) => { + self.plan_to_navigate(load_data); + } + ("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, mut load_data: LoadData, encoding: EncodingRef) { + let charset = &*encoding.whatwg_name().unwrap(); + + load_data.url.query_pairs_mut().clear() + .encoding_override(Some(self.pick_encoding())) + .extend_pairs(form_data.into_iter() + .map(|field| (field.name.clone(), field.replace_value(charset)))); + + self.plan_to_navigate(load_data); + } + + // https://html.spec.whatwg.org/multipage/#submit-body + fn submit_entity_body(&self, form_data: &mut Vec, mut load_data: LoadData, + enctype: FormEncType, encoding: EncodingRef) { + let boundary = self.generate_boundary(); + let bytes = match enctype { FormEncType::UrlEncoded => { + let mut url = load_data.url.clone(); + let charset = &*encoding.whatwg_name().unwrap(); load_data.headers.set(ContentType::form_url_encoded()); - form_urlencoded::Serializer::new(String::new()) - .encoding_override(Some(self.pick_encoding())) - .extend_pairs(form_data.into_iter().map(|field| (field.name.clone(), field.value_str()))) - .finish() + url.query_pairs_mut().clear() + .encoding_override(Some(self.pick_encoding())) + .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 boundary = self.generate_boundary(); let mime = mime!(Multipart / FormData; Boundary =(&boundary)); load_data.headers.set(ContentType(mime)); - - self.encode_multipart_form_data(&mut form_data, None, boundary) + self.encode_multipart_form_data(form_data, boundary, encoding) } FormEncType::TextPlainEncoded => { load_data.headers.set(ContentType(mime!(Text / Plain))); - - self.encode_plaintext(&mut form_data) + self.encode_plaintext(form_data).into_bytes() } }; - // Step 18 - let win = window_from_node(self); - match (&*scheme, method) { - // https://html.spec.whatwg.org/multipage/#submit-dialog - (_, FormMethod::FormDialog) => return, // Unimplemented - // https://html.spec.whatwg.org/multipage/#submit-mutate-action - ("http", FormMethod::FormGet) | ("https", FormMethod::FormGet) => { - // FIXME(SimonSapin): use url.query_pairs_mut() here. - load_data.url.set_query(Some(&*parsed_data)); - self.plan_to_navigate(load_data, &win); - } - // https://html.spec.whatwg.org/multipage/#submit-body - ("http", FormMethod::FormPost) | ("https", FormMethod::FormPost) => { - load_data.method = Method::Post; - load_data.data = Some(parsed_data.into_bytes()); - self.plan_to_navigate(load_data, &win); - } - // https://html.spec.whatwg.org/multipage/#submit-get-action - ("file", _) | ("about", _) | ("data", FormMethod::FormGet) | - ("ftp", _) | ("javascript", _) => { - self.plan_to_navigate(load_data, &win); - } - _ => return // Unimplemented (data and mailto) - } + load_data.data = Some(bytes); + self.plan_to_navigate(load_data); } /// [Planned navigation](https://html.spec.whatwg.org/multipage/#planned-navigation) - fn plan_to_navigate(&self, load_data: LoadData, window: &Window) { + fn plan_to_navigate(&self, load_data: LoadData) { + let window = window_from_node(self); + // Step 1 // Each planned navigation runnable is tagged with a generation ID, and // before the runnable is handled, it first checks whether the HTMLFormElement's @@ -485,7 +509,7 @@ impl HTMLFormElement { }; // Step 3 - window.dom_manipulation_task_source().queue(nav, GlobalRef::Window(window)).unwrap(); + window.dom_manipulation_task_source().queue(nav, GlobalRef::Window(&window)).unwrap(); } /// Interactively validate the constraints of form elements @@ -558,10 +582,8 @@ impl HTMLFormElement { match element { HTMLElementTypeId::HTMLInputElement => { let input = child.downcast::().unwrap(); - // Step 3.2-3.7 - if let Some(datum) = input.form_datum(submitter) { - data_set.push(datum); - } + + data_set.append(&mut input.form_datums(submitter)); } HTMLElementTypeId::HTMLButtonElement => { let button = child.downcast::().unwrap(); @@ -709,10 +731,14 @@ pub struct FormDatum { } impl FormDatum { - pub fn value_str(&self) -> String { + 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()), - FormDatumValue::File(ref f) => String::from(f.name().clone()) } } } diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 7f524cac29d8..6f7a7a755f56 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -648,7 +648,7 @@ impl HTMLInputElement { /// https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set /// Steps range from 3.1 to 3.7 (specific to HTMLInputElement) - pub fn form_datum(&self, submitter: Option) -> Option { + pub fn form_datums(&self, submitter: Option) -> Vec { // 3.1: disabled state check is in get_unclean_dataset // Step 3.2 @@ -664,26 +664,55 @@ impl HTMLInputElement { match ty { // Step 3.1: it's a button but it is not submitter. - atom!("submit") | atom!("button") | atom!("reset") if !is_submitter => return None, + atom!("submit") | atom!("button") | atom!("reset") if !is_submitter => return vec![], // Step 3.1: it's the "Checkbox" or "Radio Button" and whose checkedness is false. atom!("radio") | atom!("checkbox") => if !self.Checked() || name.is_empty() { - return None; + return vec![]; }, + atom!("file") => { + let mut datums = vec![]; + + // Step 3.2-3.7 + let name = self.Name(); + let type_ = self.Type(); + + match self.GetFiles() { + Some(fl) => { + for f in fl.iter_files() { + datums.push(FormDatum { + ty: type_.clone(), + name: name.clone(), + value: FormDatumValue::File(Root::from_ref(&f)), + }); + } + } + None => { + datums.push(FormDatum { + // XXX(izgzhen): Spec says 'application/octet-stream' as the type, + // but this is _type_ of element rather than content right? + ty: type_.clone(), + name: name.clone(), + value: FormDatumValue::String(DOMString::from("")), + }) + } + } - atom!("image") | atom!("file") => return None, // Unimplemented + return datums; + } + atom!("image") => return vec![], // Unimplemented // Step 3.1: it's not the "Image Button" and doesn't have a name attribute. _ => if name.is_empty() { - return None; + return vec![]; } } // Step 3.9 - Some(FormDatum { + vec![FormDatum { ty: DOMString::from(&*ty), // FIXME(ajeffrey): Convert directly from Atoms to DOMStrings name: name, value: FormDatumValue::String(self.Value()) - }) + }] } // https://html.spec.whatwg.org/multipage/#radio-button-group diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 6fd9384d01e1..20107d1e1053 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -5806,16 +5806,16 @@ "url": "/_mozilla/css/word_break_a.html" } ], - "mozilla/blob_url_upload.html": [ + "mozilla/FileAPI/blob_url_upload.html": [ { - "path": "mozilla/blob_url_upload.html", + "path": "mozilla/FileAPI/blob_url_upload.html", "references": [ [ - "/_mozilla/mozilla/blob_url_upload_ref.html", + "/_mozilla/mozilla/FileAPI/blob_url_upload_ref.html", "==" ] ], - "url": "/_mozilla/mozilla/blob_url_upload.html" + "url": "/_mozilla/mozilla/FileAPI/blob_url_upload.html" } ], "mozilla/canvas/drawimage_html_image_1.html": [ @@ -6234,6 +6234,24 @@ "url": "/_mozilla/mozilla/Event.html" } ], + "mozilla/FileAPI/blob.html": [ + { + "path": "mozilla/FileAPI/blob.html", + "url": "/_mozilla/mozilla/FileAPI/blob.html" + } + ], + "mozilla/FileAPI/file-select.html": [ + { + "path": "mozilla/FileAPI/file-select.html", + "url": "/_mozilla/mozilla/FileAPI/file-select.html" + } + ], + "mozilla/FileAPI/file-upload.html": [ + { + "path": "mozilla/FileAPI/file-upload.html", + "url": "/_mozilla/mozilla/FileAPI/file-upload.html" + } + ], "mozilla/FocusEvent.html": [ { "path": "mozilla/FocusEvent.html", @@ -6258,12 +6276,6 @@ "url": "/_mozilla/mozilla/binding_keyword.html" } ], - "mozilla/blob.html": [ - { - "path": "mozilla/blob.html", - "url": "/_mozilla/mozilla/blob.html" - } - ], "mozilla/body_listener.html": [ { "path": "mozilla/body_listener.html", @@ -6540,12 +6552,6 @@ "url": "/_mozilla/mozilla/event_listener.html" } ], - "mozilla/file_upload.html": [ - { - "path": "mozilla/file_upload.html", - "url": "/_mozilla/mozilla/file_upload.html" - } - ], "mozilla/focus_blur.html": [ { "path": "mozilla/focus_blur.html", @@ -14936,16 +14942,16 @@ "url": "/_mozilla/css/word_break_a.html" } ], - "mozilla/blob_url_upload.html": [ + "mozilla/FileAPI/blob_url_upload.html": [ { - "path": "mozilla/blob_url_upload.html", + "path": "mozilla/FileAPI/blob_url_upload.html", "references": [ [ - "/_mozilla/mozilla/blob_url_upload_ref.html", + "/_mozilla/mozilla/FileAPI/blob_url_upload_ref.html", "==" ] ], - "url": "/_mozilla/mozilla/blob_url_upload.html" + "url": "/_mozilla/mozilla/FileAPI/blob_url_upload.html" } ], "mozilla/canvas/drawimage_html_image_1.html": [ diff --git a/tests/wpt/mozilla/meta/mozilla/blob_url_upload.html.ini b/tests/wpt/mozilla/meta/mozilla/FileAPI/blob_url_upload.html.ini similarity index 100% rename from tests/wpt/mozilla/meta/mozilla/blob_url_upload.html.ini rename to tests/wpt/mozilla/meta/mozilla/FileAPI/blob_url_upload.html.ini diff --git a/tests/wpt/mozilla/meta/mozilla/file_upload.html.ini b/tests/wpt/mozilla/meta/mozilla/FileAPI/file-select.html.ini similarity index 81% rename from tests/wpt/mozilla/meta/mozilla/file_upload.html.ini rename to tests/wpt/mozilla/meta/mozilla/FileAPI/file-select.html.ini index 8c9a7d45d7d4..b047e230d992 100644 --- a/tests/wpt/mozilla/meta/mozilla/file_upload.html.ini +++ b/tests/wpt/mozilla/meta/mozilla/FileAPI/file-select.html.ini @@ -1,3 +1,3 @@ -[file_upload.html] +[file-select.html] type: testharness prefs: [dom.testing.htmlinputelement.select_files.enabled:true] diff --git a/tests/wpt/mozilla/meta/mozilla/FileAPI/file-upload.html.ini b/tests/wpt/mozilla/meta/mozilla/FileAPI/file-upload.html.ini new file mode 100644 index 000000000000..f3715f47bf95 --- /dev/null +++ b/tests/wpt/mozilla/meta/mozilla/FileAPI/file-upload.html.ini @@ -0,0 +1,3 @@ +[file-upload.html] + type: testharness + prefs: [dom.testing.htmlinputelement.select_files.enabled:true] diff --git a/tests/wpt/mozilla/tests/mozilla/blob.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/blob.html similarity index 100% rename from tests/wpt/mozilla/tests/mozilla/blob.html rename to tests/wpt/mozilla/tests/mozilla/FileAPI/blob.html diff --git a/tests/wpt/mozilla/tests/mozilla/blob_url_upload.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/blob_url_upload.html similarity index 100% rename from tests/wpt/mozilla/tests/mozilla/blob_url_upload.html rename to tests/wpt/mozilla/tests/mozilla/FileAPI/blob_url_upload.html diff --git a/tests/wpt/mozilla/tests/mozilla/blob_url_upload_ref.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/blob_url_upload_ref.html similarity index 79% rename from tests/wpt/mozilla/tests/mozilla/blob_url_upload_ref.html rename to tests/wpt/mozilla/tests/mozilla/FileAPI/blob_url_upload_ref.html index 2ae08600b458..6f95c43ac324 100644 --- a/tests/wpt/mozilla/tests/mozilla/blob_url_upload_ref.html +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/blob_url_upload_ref.html @@ -2,6 +2,6 @@ Reference: Blob URL with File Upload - + diff --git a/tests/wpt/mozilla/tests/mozilla/file_upload.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-select.html similarity index 92% rename from tests/wpt/mozilla/tests/mozilla/file_upload.html rename to tests/wpt/mozilla/tests/mozilla/FileAPI/file-select.html index ad911097f6c7..06a5f30dd441 100644 --- a/tests/wpt/mozilla/tests/mozilla/file_upload.html +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-select.html @@ -1,6 +1,6 @@ -Test of uploading a file through input element +Test of selecting a file through input element diff --git a/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload-frame.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload-frame.html new file mode 100644 index 000000000000..13951bb37d06 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload-frame.html @@ -0,0 +1,8 @@ +
+ +
+ + diff --git a/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload.html b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload.html new file mode 100644 index 000000000000..bff5fb1ee7a0 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/file-upload.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/file-submission.py b/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/file-submission.py new file mode 100644 index 000000000000..aa278cb4c4d1 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/file-submission.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# 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 http://mozilla.org/MPL/2.0/. + + +def fail(msg): + return ([("Content-Type", "text/plain")], "FAIL: " + msg) + + +def main(request, response): + content_type = request.headers.get('Content-Type').split("; ") + + if len(content_type) != 2: + return fail("content type length is incorrect") + + if content_type[0] != 'multipart/form-data': + return fail("content type first field is incorrect") + + boundary = content_type[1].strip("boundary=") + + body = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file-input\"; filename=\"upload.txt\"" + body += "\r\n" + "text/plain\r\n\r\nHello\r\n--" + boundary + "--" + + if body != request.body: + return fail("request body doesn't match: " + body + "+++++++" + request.body) + + return ([("Content-Type", "text/plain")], "OK") diff --git a/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/upload.txt b/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/upload.txt new file mode 100644 index 000000000000..5ab2f8a4323a --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/FileAPI/resource/upload.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file