servo: Merge #18262 - Implement value sanitization on HTMLInputElement (from KiChjang:value-sanitization); r=nox
authorKeith Yeung <kungfukeith11@gmail.com>
Thu, 09 Nov 2017 18:35:07 -0600
changeset 444488 02a1162f7939ab288da327684c9cee8abb9e9239
parent 444487 18e252e533d9d5d2a16580a359ed2878ba8f6ea2
child 444489 c551e2e1642a172e1e0984e162584d951b452d27
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnox
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #18262 - Implement value sanitization on HTMLInputElement (from KiChjang:value-sanitization); r=nox https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm Source-Repo: https://github.com/servo/servo Source-Revision: 338e2ae5206d5270a6feaf7adbb8279fe25040e2
servo/components/script/dom/bindings/str.rs
servo/components/script/dom/htmlinputelement.rs
servo/components/script/lib.rs
servo/components/script/textinput.rs
--- a/servo/components/script/dom/bindings/str.rs
+++ b/servo/components/script/dom/bindings/str.rs
@@ -180,16 +180,39 @@ impl DOMString {
     pub fn truncate(&mut self, new_len: usize) {
         self.0.truncate(new_len);
     }
 
     /// An iterator over the bytes of this `DOMString`.
     pub fn bytes(&self) -> Bytes {
         self.0.bytes()
     }
+
+    /// Removes newline characters according to <https://infra.spec.whatwg.org/#strip-newlines>.
+    pub fn strip_newlines(&mut self) {
+        self.0.retain(|c| c != '\r' && c != '\n');
+    }
+
+    /// Removes leading and trailing ASCII whitespaces according to
+    /// <https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace>.
+    pub fn strip_leading_and_trailing_ascii_whitespace(&mut self) {
+        if self.0.len() == 0 { return; }
+
+        let last_non_whitespace = match self.0.rfind(|ref c| !char::is_ascii_whitespace(c)) {
+            Some(idx) => idx + 1,
+            None => {
+                self.0.clear();
+                return;
+            }
+        };
+        let first_non_whitespace = self.0.find(|ref c| !char::is_ascii_whitespace(c)).unwrap();
+
+        self.0.truncate(last_non_whitespace);
+        let _ = self.0.splice(0..first_non_whitespace, "");
+    }
 }
 
 impl Borrow<str> for DOMString {
     #[inline]
     fn borrow(&self) -> &str {
         &self.0
     }
 }
--- a/servo/components/script/dom/htmlinputelement.rs
+++ b/servo/components/script/dom/htmlinputelement.rs
@@ -41,21 +41,22 @@ use mime_guess;
 use net_traits::{CoreResourceMsg, IpcSend};
 use net_traits::blob_url_store::get_blob_origin;
 use net_traits::filemanager_thread::{FileManagerThreadMsg, FilterPattern};
 use script_layout_interface::rpc::TextIndexResponse;
 use script_traits::ScriptToConstellationChan;
 use servo_atoms::Atom;
 use std::borrow::ToOwned;
 use std::cell::Cell;
+use std::mem;
 use std::ops::Range;
 use style::attr::AttrValue;
 use style::element_state::ElementState;
 use style::str::split_commas;
-use textinput::{SelectionDirection, TextInput};
+use textinput::{Direction, Selection, SelectionDirection, TextInput};
 use textinput::KeyReaction::{DispatchInput, Nothing, RedrawSelection, TriggerDefaultAction};
 use textinput::Lines::Single;
 
 const DEFAULT_SUBMIT_VALUE: &'static str = "Submit";
 const DEFAULT_RESET_VALUE: &'static str = "Reset";
 const PASSWORD_REPLACEMENT_CHAR: char = '‚óŹ';
 
 #[derive(Clone, Copy, JSTraceable, PartialEq)]
@@ -170,17 +171,17 @@ impl HTMLInputElement {
     }
 
     pub fn type_(&self) -> Atom {
         self.upcast::<Element>()
             .get_attribute(&ns!(), &local_name!("type"))
             .map_or_else(|| atom!(""), |a| a.value().as_atom().to_owned())
     }
 
-    // https://html.spec.whatwg.org/multipage/#input-type-attr-summary
+    // https://html.spec.whatwg.org/multipage/#dom-input-value
     fn value_mode(&self) -> ValueMode {
         match self.input_type.get() {
             InputType::InputSubmit |
             InputType::InputReset |
             InputType::InputButton |
             InputType::InputImage => ValueMode::Default,
             InputType::InputCheckbox |
             InputType::InputRadio => ValueMode::DefaultOn,
@@ -404,18 +405,27 @@ impl HTMLInputElementMethods for HTMLInp
             }
         }
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-input-value
     fn SetValue(&self, value: DOMString) -> ErrorResult {
         match self.value_mode() {
             ValueMode::Value => {
-                self.textinput.borrow_mut().set_content(value);
+                // Steps 1-2.
+                let old_value = mem::replace(self.textinput.borrow_mut().single_line_content_mut(), value);
+                // Step 3.
                 self.value_dirty.set(true);
+                // Step 4.
+                self.sanitize_value();
+                // Step 5.
+                if *self.textinput.borrow().single_line_content() != old_value {
+                    self.textinput.borrow_mut()
+                        .adjust_horizontal_to_limit(Direction::Forward, Selection::NotSelected);
+                }
             }
             ValueMode::Default |
             ValueMode::DefaultOn => {
                 self.upcast::<Element>().set_string_attribute(&local_name!("value"), value);
             }
             ValueMode::Filename => {
                 if value.is_empty() {
                     let window = window_from_node(self);
@@ -854,16 +864,33 @@ impl HTMLInputElement {
         } else {
             let filelist = FileList::new(&window, files);
             self.filelist.set(Some(&filelist));
 
             target.fire_bubbling_event(atom!("input"));
             target.fire_bubbling_event(atom!("change"));
         }
     }
+
+    // https://html.spec.whatwg.org/multipage/#value-sanitization-algorithm
+    fn sanitize_value(&self) {
+        match self.type_() {
+            atom!("text") | atom!("search") | atom!("tel") | atom!("password") => {
+                self.textinput.borrow_mut().single_line_content_mut().strip_newlines();
+            }
+            atom!("url") => {
+                let mut textinput = self.textinput.borrow_mut();
+                let content = textinput.single_line_content_mut();
+                content.strip_newlines();
+                content.strip_leading_and_trailing_ascii_whitespace();
+            }
+            // TODO: Implement more value sanitization algorithms for different types of inputs
+            _ => ()
+        }
+    }
 }
 
 impl VirtualMethods for HTMLInputElement {
     fn super_type(&self) -> Option<&VirtualMethods> {
         Some(self.upcast::<HTMLElement>() as &VirtualMethods)
     }
 
     fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
@@ -967,17 +994,18 @@ impl VirtualMethods for HTMLInputElement
                         }
 
                         // Step 5
                         if new_type == InputType::InputRadio {
                             self.radio_group_updated(
                                 self.radio_group_name().as_ref());
                         }
 
-                        // TODO: Step 6 - value sanitization
+                        // Step 6
+                        self.sanitize_value();
                     },
                     AttributeMutation::Removed => {
                         if self.input_type.get() == InputType::InputRadio {
                             broadcast_radio_checked(
                                 self,
                                 self.radio_group_name().as_ref());
                         }
                         self.input_type.set(InputType::InputText);
--- a/servo/components/script/lib.rs
+++ b/servo/components/script/lib.rs
@@ -1,19 +1,22 @@
 /* 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/. */
 
 #![cfg_attr(feature = "unstable", feature(core_intrinsics))]
 #![cfg_attr(feature = "unstable", feature(on_unimplemented))]
+#![feature(ascii_ctype)]
 #![feature(conservative_impl_trait)]
 #![feature(const_fn)]
 #![feature(mpsc_select)]
 #![feature(plugin)]
 #![feature(proc_macro)]
+#![feature(splice)]
+#![feature(string_retain)]
 
 #![deny(unsafe_code)]
 #![allow(non_snake_case)]
 
 #![doc = "The script crate contains all matters DOM."]
 
 #![plugin(script_plugins)]
 #![cfg_attr(not(feature = "unrooted_must_root_lint"), allow(unknown_lints))]
--- a/servo/components/script/textinput.rs
+++ b/servo/components/script/textinput.rs
@@ -749,16 +749,28 @@ impl<T: ClipboardProvider> TextInput<T> 
             content.push_str(&line);
             if i < self.lines.len() - 1 {
                 content.push('\n');
             }
         }
         DOMString::from(content)
     }
 
+    /// Get a reference to the contents of a single-line text input. Panics if self is a multiline input.
+    pub fn single_line_content(&self) -> &DOMString {
+        assert!(!self.multiline);
+        &self.lines[0]
+    }
+
+    /// Get a mutable reference to the contents of a single-line text input. Panics if self is a multiline input.
+    pub fn single_line_content_mut(&mut self) -> &mut DOMString {
+        assert!(!self.multiline);
+        &mut self.lines[0]
+    }
+
     /// Set the current contents of the text input. If this is control supports multiple lines,
     /// any \n encountered will be stripped and force a new logical line.
     pub fn set_content(&mut self, content: DOMString) {
         self.lines = if self.multiline {
             content.split('\n').map(DOMString::from).collect()
         } else {
             vec!(content)
         };