Bug 827161, part 2 - Implement HTML 5's ValidityState.badInput and implement the state for HTMLInputElement's number type. r=smaug
authorJonathan Watt <jwatt@jwatt.org>
Thu, 30 Jan 2014 12:54:12 +0000
changeset 166553 e67fbfeab5d5499b8c5c3dc6b1c337085c0103d4
parent 166552 771ed4be9dee43525318a560d146a3cc5d853ad8
child 166554 5e41335ed0a400968e49b96ad21a8f5c037a9fe1
push id26131
push userjwatt@jwatt.org
push dateSun, 02 Feb 2014 22:29:47 +0000
treeherdermozilla-central@e67fbfeab5d5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs827161
milestone29.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
Bug 827161, part 2 - Implement HTML 5's ValidityState.badInput and implement the state for HTMLInputElement's number type. r=smaug
content/html/content/public/nsIConstraintValidation.h
content/html/content/src/HTMLInputElement.cpp
content/html/content/src/HTMLInputElement.h
content/html/content/src/ValidityState.cpp
content/html/content/src/ValidityState.h
content/html/content/src/nsIConstraintValidation.cpp
content/html/content/test/forms/mochitest.ini
content/html/content/test/forms/test_input_number_data.js
content/html/content/test/forms/test_input_number_l10n.html
content/html/content/test/forms/test_input_number_validation.html
content/html/content/test/forms/test_input_typing_sanitization.html
content/html/content/test/forms/test_validation.html
dom/interfaces/html/nsIDOMValidityState.idl
dom/locales/en-US/chrome/dom/dom.properties
dom/webidl/ValidityState.webidl
layout/forms/nsNumberControlFrame.cpp
layout/forms/nsNumberControlFrame.h
webapprt/locales/en-US/webapprt/overrides/dom.properties
--- a/content/html/content/public/nsIConstraintValidation.h
+++ b/content/html/content/public/nsIConstraintValidation.h
@@ -14,18 +14,18 @@ class nsIDOMValidityState;
 
 namespace mozilla {
 namespace dom {
 class ValidityState;
 }
 }
 
 #define NS_ICONSTRAINTVALIDATION_IID \
-{ 0xca3824dc, 0x4f5c, 0x4878, \
- { 0xa6, 0x8a, 0x95, 0x54, 0x5f, 0xfa, 0x4b, 0xf9 } }
+{ 0x983829da, 0x1aaf, 0x449c, \
+ { 0xa3, 0x06, 0x85, 0xd4, 0xf0, 0x31, 0x1c, 0xf6 } }
 
 /**
  * This interface is for form elements implementing the validity constraint API.
  * See: http://dev.w3.org/html5/spec/forms.html#the-constraint-validation-api
  *
  * This interface has to be implemented by all elements implementing the API
  * and only them.
  */
@@ -46,24 +46,26 @@ public:
   bool IsCandidateForConstraintValidation() const {
            return !mBarredFromConstraintValidation;
          }
 
   NS_IMETHOD GetValidationMessage(nsAString& aValidationMessage);
 
   enum ValidityStateType
   {
-    VALIDITY_STATE_VALUE_MISSING    = 0x01, // 0b00000001
-    VALIDITY_STATE_TYPE_MISMATCH    = 0x02, // 0b00000010
-    VALIDITY_STATE_PATTERN_MISMATCH = 0x04, // 0b00000100
-    VALIDITY_STATE_TOO_LONG         = 0x08, // 0b00001000
-    VALIDITY_STATE_RANGE_UNDERFLOW  = 0x10, // 0b00010000
-    VALIDITY_STATE_RANGE_OVERFLOW   = 0x20, // 0b00100000
-    VALIDITY_STATE_STEP_MISMATCH    = 0x40, // 0b01000000
-    VALIDITY_STATE_CUSTOM_ERROR     = 0x80  // 0b10000000
+    VALIDITY_STATE_VALUE_MISSING    = 0x1 <<  0,
+    VALIDITY_STATE_TYPE_MISMATCH    = 0x1 <<  1,
+    VALIDITY_STATE_PATTERN_MISMATCH = 0x1 <<  2,
+    VALIDITY_STATE_TOO_LONG         = 0x1 <<  3,
+  //VALIDITY_STATE_TOO_SHORT        = 0x1 <<  4,
+    VALIDITY_STATE_RANGE_UNDERFLOW  = 0x1 <<  5,
+    VALIDITY_STATE_RANGE_OVERFLOW   = 0x1 <<  6,
+    VALIDITY_STATE_STEP_MISMATCH    = 0x1 <<  7,
+    VALIDITY_STATE_BAD_INPUT        = 0x1 <<  8,
+    VALIDITY_STATE_CUSTOM_ERROR     = 0x1 <<  9,
   };
 
   void SetValidityState(ValidityStateType mState,
                         bool mValue);
 
   // Web IDL binding methods
   bool WillValidate() const {
     return IsCandidateForConstraintValidation();
@@ -98,17 +100,17 @@ protected:
   nsRefPtr<mozilla::dom::ValidityState>  mValidity;
 
 private:
 
   /**
    * A bitfield representing the current validity state of the element.
    * Each bit represent an error. All bits to zero means the element is valid.
    */
-  int8_t                        mValidityBitField;
+  int16_t                       mValidityBitField;
 
   /**
    * Keeps track whether the element is barred from constraint validation.
    */
   bool                          mBarredFromConstraintValidation;
 
   /**
    * The string representing the custom error.
--- a/content/html/content/src/HTMLInputElement.cpp
+++ b/content/html/content/src/HTMLInputElement.cpp
@@ -2762,23 +2762,27 @@ HTMLInputElement::SetValueInternal(const
       // At the moment, only single line text control have to sanitize their value
       // Because we have to create a new string for that, we should prevent doing
       // it if it's useless.
       nsAutoString value(aValue);
 
       if (!mParserCreating) {
         SanitizeValue(value);
       }
+      // else SanitizeValue will be called by DoneCreatingElement
 
       if (aSetValueChanged) {
         SetValueChanged(true);
       }
 
       if (IsSingleLineTextControl(false)) {
         mInputData.mState->SetValue(value, aUserInput, aSetValueChanged);
+        if (mType == NS_FORM_INPUT_EMAIL) {
+          UpdateAllValidityStates(mParserCreating);
+        }
       } else {
         mInputData.mValue = ToNewUnicode(value);
         if (aSetValueChanged) {
           SetValueChanged(true);
         }
         OnValueChanged(!mParserCreating);
 
         if (mType == NS_FORM_INPUT_NUMBER) {
@@ -3466,17 +3470,17 @@ HTMLInputElement::PreHandleEvent(nsEvent
     }
     if (textControl && aVisitor.mEvent->originalTarget == textControl) {
       if (aVisitor.mEvent->message == NS_FORM_INPUT) {
         // Propogate the anon text control's new value to our HTMLInputElement:
         nsAutoString value;
         numberControlFrame->GetValueOfAnonTextControl(value);
         numberControlFrame->HandlingInputEvent(true);
         nsWeakFrame weakNumberControlFrame(numberControlFrame);
-        SetValueInternal(value, false, true);
+        SetValueInternal(value, true, true);
         if (weakNumberControlFrame.IsAlive()) {
           numberControlFrame->HandlingInputEvent(false);
         }
       }
       else if (aVisitor.mEvent->message == NS_FORM_CHANGE) {
         // We cancel the DOM 'change' event that is fired for any change to our
         // anonymous text control since we fire our own 'change' events and
         // content shouldn't be seeing two 'change' events. Besides that we
@@ -6417,16 +6421,113 @@ HTMLInputElement::HasStepMismatch() cons
   if (step == kStepAny) {
     return false;
   }
 
   // Value has to be an integral multiple of step.
   return NS_floorModulo(value - GetStepBase(), step) != 0;
 }
 
+/**
+ * Splits the string on the first "@" character and punycode encodes the first
+ * and second parts separately before rejoining them with an "@" and returning
+ * the result via the aEncodedEmail out-param. Returns false if there is no
+ * "@" caracter, if the "@" character is at the start or end, or if the
+ * conversion to punycode fails.
+ *
+ * This function exists because ConvertUTF8toACE() treats 'username@domain' as
+ * a single label, but we need to encode the username and domain parts
+ * separately.
+ */
+static bool PunycodeEncodeEmailAddress(const nsAString& aEmail,
+                                       nsAutoCString& aEncodedEmail,
+                                       uint32_t* aIndexOfAt)
+{
+  nsAutoCString value = NS_ConvertUTF16toUTF8(aEmail);
+  uint32_t length = value.Length();
+
+  uint32_t atPos = (uint32_t)value.FindChar('@');
+  // Email addresses must contain a '@', but can't begin or end with it.
+  if (atPos == (uint32_t)kNotFound || atPos == 0 || atPos == length - 1) {
+    return false;
+  }
+
+  nsCOMPtr<nsIIDNService> idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID);
+  if (!idnSrv) {
+    NS_ERROR("nsIIDNService isn't present!");
+    return false;
+  }
+
+  const nsDependentCSubstring username = Substring(value, 0, atPos);
+  bool ace;
+  if (NS_SUCCEEDED(idnSrv->IsACE(username, &ace)) && !ace) {
+    nsAutoCString usernameACE;
+    // TODO: Bug 901347: Usernames longer than 63 chars are not converted by
+    // ConvertUTF8toACE(). For now, continue on even if the conversion fails.
+    if (NS_SUCCEEDED(idnSrv->ConvertUTF8toACE(username, usernameACE))) {
+      value.Replace(0, atPos, usernameACE);
+      atPos = usernameACE.Length();
+    }
+  }
+
+  const nsDependentCSubstring domain = Substring(value, atPos + 1);
+  if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) {
+    nsAutoCString domainACE;
+    if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) {
+      return false;
+    }
+    value.Replace(atPos + 1, domain.Length(), domainACE);
+  }
+
+  aEncodedEmail = value;
+  *aIndexOfAt = atPos;
+  return true;
+}
+
+bool
+HTMLInputElement::HasBadInput() const
+{
+  if (mType == NS_FORM_INPUT_NUMBER) {
+    nsAutoString value;
+    GetValueInternal(value);
+    if (!value.IsEmpty()) {
+      // The input can't be bad, otherwise it would have been sanitized to the
+      // empty string.
+      NS_ASSERTION(!GetValueAsDecimal().isNaN(), "Should have sanitized");
+      return false;
+    }
+    nsNumberControlFrame* numberControlFrame =
+      do_QueryFrame(GetPrimaryFrame());
+    if (numberControlFrame &&
+        !numberControlFrame->AnonTextControlIsEmpty()) {
+      // The input the user entered failed to parse as a number.
+      return true;
+    }
+    return false;
+  }
+  if (mType == NS_FORM_INPUT_EMAIL) {
+    // With regards to suffering from bad input the spec says that only the
+    // punycode conversion works, so we don't care whether the email address is
+    // valid or not here. (If the email address is invalid then we will be
+    // suffering from a type mismatch.)
+    nsAutoString value;
+    nsAutoCString unused;
+    uint32_t unused2;
+    NS_ENSURE_SUCCESS(GetValueInternal(value), false);
+    HTMLSplitOnSpacesTokenizer tokenizer(value, ',');
+    while (tokenizer.hasMoreTokens()) {
+      if (!PunycodeEncodeEmailAddress(tokenizer.nextToken(), unused, &unused2)) {
+        return true;
+      }
+    }
+    return false;
+  }
+  return false;
+}
+
 void
 HTMLInputElement::UpdateTooLongValidityState()
 {
   // TODO: this code will be re-enabled with bug 613016 and bug 613019.
 #if 0
   SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong());
 #endif
 }
@@ -6514,26 +6615,33 @@ HTMLInputElement::UpdateRangeUnderflowVa
 
 void
 HTMLInputElement::UpdateStepMismatchValidityState()
 {
   SetValidityState(VALIDITY_STATE_STEP_MISMATCH, HasStepMismatch());
 }
 
 void
+HTMLInputElement::UpdateBadInputValidityState()
+{
+  SetValidityState(VALIDITY_STATE_BAD_INPUT, HasBadInput());
+}
+
+void
 HTMLInputElement::UpdateAllValidityStates(bool aNotify)
 {
   bool validBefore = IsValid();
   UpdateTooLongValidityState();
   UpdateValueMissingValidityState();
   UpdateTypeMismatchValidityState();
   UpdatePatternMismatchValidityState();
   UpdateRangeOverflowValidityState();
   UpdateRangeUnderflowValidityState();
   UpdateStepMismatchValidityState();
+  UpdateBadInputValidityState();
 
   if (validBefore != IsValid()) {
     UpdateState(aNotify);
   }
 }
 
 void
 HTMLInputElement::UpdateBarredFromConstraintValidation()
@@ -6748,16 +6856,32 @@ HTMLInputElement::GetValidationMessage(n
         rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
                                                    "FormValidationStepMismatchOneValue",
                                                    params, message);
       }
 
       aValidationMessage = message;
       break;
     }
+    case VALIDITY_STATE_BAD_INPUT:
+    {
+      nsXPIDLString message;
+      nsAutoCString key;
+      if (mType == NS_FORM_INPUT_NUMBER) {
+        key.AssignLiteral("FormValidationBadInputNumber");
+      } else if (mType == NS_FORM_INPUT_EMAIL) {
+        key.AssignLiteral("FormValidationInvalidEmail");
+      } else {
+        return NS_ERROR_UNEXPECTED;
+      }
+      rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
+                                              key.get(), message);
+      aValidationMessage = message;
+      break;
+    }
     default:
       rv = nsIConstraintValidation::GetValidationMessage(aValidationMessage, aType);
   }
 
   return rv;
 }
 
 //static
@@ -6774,61 +6898,35 @@ HTMLInputElement::IsValidEmailAddressLis
 
   return !tokenizer.separatorAfterCurrentToken();
 }
 
 //static
 bool
 HTMLInputElement::IsValidEmailAddress(const nsAString& aValue)
 {
-  nsAutoCString value = NS_ConvertUTF16toUTF8(aValue);
-  uint32_t i = 0;
+  // Email addresses can't be empty and can't end with a '.' or '-'.
+  if (aValue.IsEmpty() || aValue.Last() == '.' || aValue.Last() == '-') {
+    return false;
+  }
+
+  uint32_t atPos;
+  nsAutoCString value;
+  if (!PunycodeEncodeEmailAddress(aValue, value, &atPos)) {
+    return false;
+  }
+
   uint32_t length = value.Length();
 
-  // Email addresses can't be empty and can't end with a '.' or '-'.
-  if (length == 0 || value[length - 1] == '.' || value[length - 1] == '-') {
-    return false;
-  }
-
-  uint32_t atPos = (uint32_t)value.FindChar('@');
   // Email addresses must contain a '@', but can't begin or end with it.
   if (atPos == (uint32_t)kNotFound || atPos == 0 || atPos == length - 1) {
     return false;
   }
 
-  // Puny-encode the string if needed before running the validation algorithm.
-  nsCOMPtr<nsIIDNService> idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID);
-  if (idnSrv) {
-    // ConvertUTF8toACE() treats 'username@domain' as a single label so we need
-    // to puny-encode the username and domain parts separately.
-    const nsDependentCSubstring username = Substring(value, 0, atPos);
-    bool ace;
-    if (NS_SUCCEEDED(idnSrv->IsACE(username, &ace)) && !ace) {
-      nsAutoCString usernameACE;
-      // TODO: Bug 901347: Usernames longer than 63 chars are not converted by
-      // ConvertUTF8toACE(). For now, continue on even if the conversion fails.
-      if (NS_SUCCEEDED(idnSrv->ConvertUTF8toACE(username, usernameACE))) {
-        value.Replace(0, atPos, usernameACE);
-        atPos = usernameACE.Length();
-      }
-    }
-
-    const nsDependentCSubstring domain = Substring(value, atPos + 1);
-    if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) {
-      nsAutoCString domainACE;
-      if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) {
-        return false;
-      }
-      value.Replace(atPos + 1, domain.Length(), domainACE);
-    }
-
-    length = value.Length();
-  } else {
-    NS_ERROR("nsIIDNService isn't present!");
-  }
+  uint32_t i = 0;
 
   // Parsing the username.
   for (; i < atPos; ++i) {
     char16_t c = value[i];
 
     // The username characters have to be in this list to be valid.
     if (!(nsCRT::IsAsciiAlpha(c) || nsCRT::IsAsciiDigit(c) ||
           c == '.' || c == '!' || c == '#' || c == '$' || c == '%' ||
--- a/content/html/content/src/HTMLInputElement.h
+++ b/content/html/content/src/HTMLInputElement.h
@@ -252,23 +252,25 @@ public:
   // nsIConstraintValidation
   bool     IsTooLong();
   bool     IsValueMissing() const;
   bool     HasTypeMismatch() const;
   bool     HasPatternMismatch() const;
   bool     IsRangeOverflow() const;
   bool     IsRangeUnderflow() const;
   bool     HasStepMismatch() const;
+  bool     HasBadInput() const;
   void     UpdateTooLongValidityState();
   void     UpdateValueMissingValidityState();
   void     UpdateTypeMismatchValidityState();
   void     UpdatePatternMismatchValidityState();
   void     UpdateRangeOverflowValidityState();
   void     UpdateRangeUnderflowValidityState();
   void     UpdateStepMismatchValidityState();
+  void     UpdateBadInputValidityState();
   void     UpdateAllValidityStates(bool aNotify);
   void     UpdateBarredFromConstraintValidation();
   nsresult GetValidationMessage(nsAString& aValidationMessage,
                                 ValidityStateType aType) MOZ_OVERRIDE;
   /**
    * Update the value missing validity state for radio elements when they have
    * a group.
    *
--- a/content/html/content/src/ValidityState.cpp
+++ b/content/html/content/src/ValidityState.cpp
@@ -72,16 +72,23 @@ ValidityState::GetRangeOverflow(bool* aR
 NS_IMETHODIMP
 ValidityState::GetStepMismatch(bool* aStepMismatch)
 {
   *aStepMismatch = StepMismatch();
   return NS_OK;
 }
 
 NS_IMETHODIMP
+ValidityState::GetBadInput(bool* aBadInput)
+{
+  *aBadInput = BadInput();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 ValidityState::GetCustomError(bool* aCustomError)
 {
   *aCustomError = CustomError();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 ValidityState::GetValid(bool* aValid)
--- a/content/html/content/src/ValidityState.h
+++ b/content/html/content/src/ValidityState.h
@@ -55,16 +55,20 @@ public:
   bool RangeOverflow() const
   {
     return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_RANGE_OVERFLOW);
   }
   bool StepMismatch() const
   {
     return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_STEP_MISMATCH);
   }
+  bool BadInput() const
+  {
+    return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_BAD_INPUT);
+  }
   bool CustomError() const
   {
     return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_CUSTOM_ERROR);
   }
   bool Valid() const
   {
     return !mConstraintValidation || mConstraintValidation->IsValid();
   }
--- a/content/html/content/src/nsIConstraintValidation.cpp
+++ b/content/html/content/src/nsIConstraintValidation.cpp
@@ -81,16 +81,18 @@ nsIConstraintValidation::GetValidationMe
     } else if (GetValidityState(VALIDITY_STATE_PATTERN_MISMATCH)) {
       GetValidationMessage(aValidationMessage, VALIDITY_STATE_PATTERN_MISMATCH);
     } else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW)) {
       GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_OVERFLOW);
     } else if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW)) {
       GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_UNDERFLOW);
     } else if (GetValidityState(VALIDITY_STATE_STEP_MISMATCH)) {
       GetValidationMessage(aValidationMessage, VALIDITY_STATE_STEP_MISMATCH);
+    } else if (GetValidityState(VALIDITY_STATE_BAD_INPUT)) {
+      GetValidationMessage(aValidationMessage, VALIDITY_STATE_BAD_INPUT);
     } else {
       // There should not be other validity states.
       return NS_ERROR_UNEXPECTED;
     }
   } else {
     aValidationMessage.Truncate();
   }
 
--- a/content/html/content/test/forms/mochitest.ini
+++ b/content/html/content/test/forms/mochitest.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 support-files =
   save_restore_radio_groups.sjs
   submit_invalid_file.sjs
+  test_input_number_data.js
 
 [test_button_attributes_reflection.html]
 [test_change_event.html]
 [test_datalist_element.html]
 [test_experimental_forms_pref.html]
 [test_form_attribute-1.html]
 [test_form_attribute-2.html]
 [test_form_attribute-3.html]
@@ -27,16 +28,19 @@ support-files =
 # We don't build ICU for Firefox for Android or Firefox OS:
 skip-if = os == "android" || appname == "b2g"
 [test_input_number_key_events.html]
 [test_input_number_mouse_events.html]
 # Not run on Firefox OS and Firefox for Android where the spin buttons are hidden:
 skip-if = os == "android" || appname == "b2g"
 [test_input_number_rounding.html]
 skip-if = os == "android"
+[test_input_number_validation.html]
+# We don't build ICU for Firefox for Android or Firefox OS:
+skip-if = os == "android" || appname == "b2g"
 [test_input_range_attr_order.html]
 [test_input_range_key_events.html]
 [test_input_range_mouse_and_touch_events.html]
 [test_input_range_rounding.html]
 [test_input_sanitization.html]
 [test_input_textarea_set_value_no_scroll.html]
 [test_input_typing_sanitization.html]
 [test_input_untrusted_key_events.html]
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/forms/test_input_number_data.js
@@ -0,0 +1,23 @@
+
+var tests = [
+  { desc: "British English",
+    langTag: "en-GB", inputWithGrouping: "123,456.78",
+    inputWithoutGrouping: "123456.78", value: 123456.78
+  },
+  { desc: "Farsi",
+    langTag: "fa", inputWithGrouping: "۱۲۳٬۴۵۶٫۷۸",
+    inputWithoutGrouping: "۱۲۳۴۵۶٫۷۸", value: 123456.78
+  },
+  { desc: "French",
+    langTag: "fr-FR", inputWithGrouping: "123 456,78",
+    inputWithoutGrouping: "123456,78", value: 123456.78
+  },
+  { desc: "German",
+    langTag: "de", inputWithGrouping: "123.456,78",
+    inputWithoutGrouping: "123456,78", value: 123456.78
+  },
+  { desc: "Hebrew",
+    langTag: "he", inputWithGrouping: "123,456.78",
+    inputWithoutGrouping: "123456.78", value: 123456.78
+  },
+];
--- a/content/html/content/test/forms/test_input_number_l10n.html
+++ b/content/html/content/test/forms/test_input_number_l10n.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=844744
 -->
 <head>
   <title>Test localization of number control input</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="test_input_number_data.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta charset="UTF-8">
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844744">Mozilla Bug 844744</a>
 <p id="display"></p>
 <div id="content">
   <input id="input" type="number" step="any">
@@ -26,39 +27,16 @@ https://bugzilla.mozilla.org/show_bug.cg
  **/
 SimpleTest.waitForExplicitFinish();
 
 SimpleTest.waitForFocus(function() {
   startTests();
   SimpleTest.finish();
 });
 
-var tests = [
-  { desc: "British English",
-    langTag: "en-GB", inputWithGrouping: "123,456.78",
-    inputWithoutGrouping: "123456.78", value: 123456.78
-  },
-  { desc: "Farsi",
-    langTag: "fa", inputWithGrouping: "۱۲۳٬۴۵۶٫۷۸",
-    inputWithoutGrouping: "۱۲۳۴۵۶٫۷۸", value: 123456.78
-  },
-  { desc: "French",
-    langTag: "fr-FR", inputWithGrouping: "123 456,78",
-    inputWithoutGrouping: "123456,78", value: 123456.78
-  },
-  { desc: "German",
-    langTag: "de", inputWithGrouping: "123.456,78",
-    inputWithoutGrouping: "123456,78", value: 123456.78
-  },
-  { desc: "Hebrew",
-    langTag: "he", inputWithGrouping: "123,456.78",
-    inputWithoutGrouping: "123456.78", value: 123456.78
-  },
-];
-
 var elem;
 
 function runTest(test) {
   elem.lang = test.langTag;
   elem.value = 0;
   elem.focus();
   elem.select();
   sendString(test.inputWithGrouping);
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/forms/test_input_number_validation.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=827161
+-->
+<head>
+  <title>Test validation of number control input</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="test_input_number_data.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827161">Mozilla Bug 827161</a>
+<p id="display"></p>
+<div id="content">
+  <input id="input" type="number" step="0.01" oninvalid="invalidEventHandler(event);">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 827161.
+ * This test checks that validation works correctly for <input type=number>.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+  startTests();
+  SimpleTest.finish();
+});
+
+var elem;
+
+function runTest(test) {
+  elem.lang = test.langTag;
+
+  gInvalid = false; // reset
+  elem.value = 0;
+  elem.focus();
+  elem.select();
+  sendString(test.inputWithGrouping);
+  checkIsValid(elem, test.desc + " ('" + test.langTag + "') with grouping separator");
+  sendChar("a");
+  checkIsInvalid(elem, test.desc + " ('" + test.langTag + "') with grouping separator");
+
+  gInvalid = false; // reset
+  elem.value = 0;
+  elem.select();
+  sendString(test.inputWithoutGrouping);
+  checkIsValid(elem, test.desc + " ('" + test.langTag + "') without grouping separator");
+  sendChar("a");
+  checkIsInvalid(elem, test.desc + " ('" + test.langTag + "') without grouping separator");
+}
+
+function startTests() {
+  elem = document.getElementById("input");
+  for (var test of tests) {
+    runTest(test);
+  }
+}
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+  is(e.type, "invalid", "Invalid event type should be 'invalid'");
+  gInvalid = true;
+}
+
+function checkIsValid(element, infoStr)
+{
+  ok(!element.validity.badInput,
+     "Element should not suffer from bad input for " + infoStr);
+  ok(element.validity.valid, "Element should be valid for " + infoStr);
+  ok(element.checkValidity(), "checkValidity() should return true for " + infoStr);
+  ok(!gInvalid, "The invalid event should not have been thrown for " + infoStr);
+  is(element.validationMessage, '',
+     "Validation message should be the empty string for " + infoStr);
+  ok(element.mozMatchesSelector(":valid"), ":valid pseudo-class should apply for " + infoStr);
+}
+
+function checkIsInvalid(element, infoStr)
+{
+  ok(element.validity.badInput,
+     "Element should suffer from bad input for " + infoStr);
+  ok(!element.validity.valid, "Element should not be valid for " + infoStr);
+  ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr);
+  ok(gInvalid, "The invalid event should have been thrown for " + infoStr);
+  is(element.validationMessage, "Please enter a number.",
+     "Validation message is not the expected message for " + infoStr);
+  ok(element.mozMatchesSelector(":invalid"), ":invalid pseudo-class should apply for " + infoStr);
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/content/html/content/test/forms/test_input_typing_sanitization.html
+++ b/content/html/content/test/forms/test_input_typing_sanitization.html
@@ -26,41 +26,61 @@ https://bugzilla.mozilla.org/show_bug.cg
  * This test checks that when a user types in some input types, it will not be
  * in a state where the value will be un-sanitized and usable (by a script).
  */
 
 var input = document.getElementById('i');
 var form = document.getElementById('f');
 var submitFrame = document.getElementsByTagName('iframe')[0];
 var testData = [];
+var gCurrentTest = null;
 var gValidData = [];
 var gInvalidData = [];
 
 function submitForm() {
   form.submit();
 }
 
 function sendKeyEventToSubmitForm() {
   sendKey("return");
 }
 
 function urlify(aStr) {
   return aStr.replace(':', '%3A', 'g');
 }
 
+function runTestsForNextInputType()
+{
+  try {
+    testRunner.next();
+  } catch (e) {
+    if (e.toString() == '[object StopIteration]') {
+      SimpleTest.finish();
+    } else {
+      throw StopIteration;
+    }
+  }
+}
+
 function checkValueSubmittedIsValid()
 {
   is(frames['submit_frame'].location.href,
      'http://mochi.test:8888/tests/content/html/content/test/forms/foo?i='
      + urlify(gValidData[valueIndex++]),
      "The submitted value should not have been sanitized");
 
   input.value = "";
 
   if (valueIndex >= gValidData.length) {
+    if (gCurrentTest.canHaveBadInputValidityState) {
+      // Don't run the submission tests on the invalid input if submission
+      // will be blocked by invalid input.
+      runTestsForNextInputType();
+      return;
+    }
     valueIndex = 0;
     submitFrame.onload = checkValueSubmittedIsInvalid;
     testData = gInvalidData;
   }
   testSubmissions();
 }
 
 function checkValueSubmittedIsInvalid()
@@ -69,25 +89,17 @@ function checkValueSubmittedIsInvalid()
      'http://mochi.test:8888/tests/content/html/content/test/forms/foo?i=',
      "The submitted value should have been sanitized");
 
   valueIndex++;
   input.value = "";
 
   if (valueIndex >= gInvalidData.length) {
     if (submitMethod == sendKeyEventToSubmitForm) {
-      try {
-        testRunner.next();
-      } catch (e) {
-        if (e.toString() == '[object StopIteration]') {
-          SimpleTest.finish();
-        } else {
-          throw StopIteration;
-        }
-      }
+      runTestsForNextInputType();
       return;
     }
     valueIndex = 0;
     submitMethod = sendKeyEventToSubmitForm;
     submitFrame.onload = checkValueSubmittedIsValid;
     testData = gValidData;
   }
   testSubmissions();
@@ -104,16 +116,17 @@ var submitMethod = submitForm;
 
 SimpleTest.waitForExplicitFinish();
 
 function runTest()
 {
   var data = [
     {
       type: 'number',
+      canHaveBadInputValidityState: true,
       validData: [
         "42",
         "-42", // should work for negative values
         "42.1234",
         "123.12345678912345",  // double precision
         "1e2", // e should be usable
         "2e1",
         "1e-1", // value after e can be negative
@@ -168,16 +181,18 @@ function runTest()
     },
     { type: 'week', todo: true },
     { type: 'month', todo: true },
     { type: 'datetime', todo: true },
     { type: 'datetime-local', todo: true },
   ];
 
   for (test of data) {
+    gCurrentTest = test;
+
     if (test.todo) {
       input.type = test.type;
       todo_is(input.type, test.type, test.type + " is not implemented");
       continue;
     }
 
     input.type = test.type;
     gValidData = test.validData;
--- a/content/html/content/test/forms/test_validation.html
+++ b/content/html/content/test/forms/test_validation.html
@@ -52,16 +52,17 @@ function checkConstraintValidationAPIExi
   ok('willValidate' in element, "willValidate is not available in the DOM");
   ok('validationMessage' in element, "validationMessage is not available in the DOM");
   ok('validity' in element, "validity is not available in the DOM");
 
   if ('validity' in element) {
     validity = element.validity;
     ok('valueMissing' in validity, "validity.valueMissing is not available in the DOM");
     ok('typeMismatch' in validity, "validity.typeMismatch is not available in the DOM");
+    ok('badInput' in validity, "validity.badInput is not available in the DOM");
     ok('patternMismatch' in validity, "validity.patternMismatch is not available in the DOM");
     ok('tooLong' in validity, "validity.tooLong is not available in the DOM");
     ok('rangeUnderflow' in validity, "validity.rangeUnderflow is not available in the DOM");
     ok('rangeOverflow' in validity, "validity.rangeOverflow is not available in the DOM");
     ok('stepMismatch' in validity, "validity.stepMismatch is not available in the DOM");
     ok('customError' in validity, "validity.customError is not available in the DOM");
     ok('valid' in validity, "validity.valid is not available in the DOM");
   }
@@ -70,16 +71,17 @@ function checkConstraintValidationAPIExi
 function checkConstraintValidationAPIDefaultValues(element)
 {
   // Not checking willValidate because the default value depends of the element
 
   is(element.validationMessage, "", "validationMessage default value should be empty string");
 
   ok(!element.validity.valueMissing, "The element should not suffer from a constraint validation");
   ok(!element.validity.typeMismatch, "The element should not suffer from a constraint validation");
+  ok(!element.validity.badInput, "The element should not suffer from a constraint validation");
   ok(!element.validity.patternMismatch, "The element should not suffer from a constraint validation");
   ok(!element.validity.tooLong, "The element should not suffer from a constraint validation");
   ok(!element.validity.rangeUnderflow, "The element should not suffer from a constraint validation");
   ok(!element.validity.rangeOverflow, "The element should not suffer from a constraint validation");
   ok(!element.validity.stepMismatch, "The element should not suffer from a constraint validation");
   ok(!element.validity.customError, "The element should not suffer from a constraint validation");
   ok(element.validity.valid, "The element should be valid by default");
 
@@ -147,16 +149,20 @@ function checkSpecificWillValidate()
   i.type = "image";
   ok(!i.willValidate, "Image state input should be barred from constraint validation");
   is(window.getComputedStyle(i, null).getPropertyValue('background-color'),
      "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
   i.type = "submit";
   ok(!i.willValidate, "Submit state input should be barred from constraint validation");
   is(window.getComputedStyle(i, null).getPropertyValue('background-color'),
      "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+  i.type = "number";
+  ok(i.willValidate, "Number state input should not be barred from constraint validation");
+  is(window.getComputedStyle(i, null).getPropertyValue('background-color'),
+     "rgb(0, 255, 0)", ":valid pseudo-class should apply");
   i.type = "";
   i.readOnly = 'true';
   ok(!i.willValidate, "Readonly input should be barred from constraint validation");
   is(window.getComputedStyle(i, null).getPropertyValue('background-color'),
      "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
   i.removeAttribute('readOnly');
   ok(i.willValidate, "Default input element should not be barred from constraint validation");
   is(window.getComputedStyle(i, null).getPropertyValue('background-color'),
@@ -270,16 +276,18 @@ function checkValidityStateObjectAliveWi
 
   var v = document.createElement(element).validity;
   SpecialPowers.gc();
 
   ok(!v.valueMissing,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.typeMismatch,
     "When the element is not alive, it shouldn't suffer from constraint validation");
+  ok(!v.badInput,
+    "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.patternMismatch,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.tooLong,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.rangeUnderflow,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.rangeOverflow,
     "When the element is not alive, it shouldn't suffer from constraint validation");
--- a/dom/interfaces/html/nsIDOMValidityState.idl
+++ b/dom/interfaces/html/nsIDOMValidityState.idl
@@ -8,21 +8,22 @@
 /**
  * The nsIDOMValidityState interface is the interface to a ValidityState
  * object which represents the validity states of an element.
  *
  * For more information on this interface please see
  * http://www.whatwg.org/specs/web-apps/current-work/#validitystate
  */
 
-[scriptable, uuid(5e62197a-9b74-4812-b5a2-ca102e886f7a)]
+[scriptable, uuid(00bed276-f1f7-492f-a039-dbd9b9efc10b)]
 interface nsIDOMValidityState : nsISupports
 {
   readonly attribute boolean valueMissing;
   readonly attribute boolean typeMismatch;
   readonly attribute boolean patternMismatch;
   readonly attribute boolean tooLong;
   readonly attribute boolean rangeUnderflow;
   readonly attribute boolean rangeOverflow;
   readonly attribute boolean stepMismatch;
+  readonly attribute boolean badInput;
   readonly attribute boolean customError;
   readonly attribute boolean valid;
 };
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -36,16 +36,17 @@ FormValidationPatternMismatchWithTitle=P
 # LOCALIZATION NOTE (FormValidationRangeOverflow): %S can be a number, a date or a time.
 FormValidationRangeOverflow=Please select a value that is lower than %S.
 # LOCALIZATION NOTE (FormValidationRangeUnderflow): %S can be a number, a date or a time.
 FormValidationRangeUnderflow=Please select a value that is higher than %S.
 # LOCALIZATION NOTE (FormValidationStepMismatch): both %S can be a number, a date or a time.
 FormValidationStepMismatch=Please select a valid value. The two nearest valid values are %S and %S.
 # LOCALIZATION NOTE (FormValidationStepMismatchOneValue): %S can be a number, a date or a time. This is called instead of FormValidationStepMismatch when the second value is the same as the first.
 FormValidationStepMismatchOneValue=Please select a valid value. The nearest valid value is %S.
+FormValidationBadInputNumber=Please enter a number.
 GetAttributeNodeWarning=Use of getAttributeNode() is deprecated. Use getAttribute() instead.
 SetAttributeNodeWarning=Use of setAttributeNode() is deprecated. Use setAttribute() instead.
 GetAttributeNodeNSWarning=Use of getAttributeNodeNS() is deprecated. Use getAttributeNS() instead.
 SetAttributeNodeNSWarning=Use of setAttributeNodeNS() is deprecated. Use setAttributeNS() instead.
 RemoveAttributeNodeWarning=Use of removeAttributeNode() is deprecated. Use removeAttribute() instead.
 CreateAttributeWarning=Use of document.createAttribute() is deprecated. Use element.setAttribute() instead.
 CreateAttributeNSWarning=Use of document.createAttributeNS() is deprecated. Use element.setAttributeNS() instead.
 SpecifiedWarning=Use of attributes' specified attribute is deprecated. It always returns true.
--- a/dom/webidl/ValidityState.webidl
+++ b/dom/webidl/ValidityState.webidl
@@ -13,13 +13,13 @@
 interface ValidityState {
   readonly attribute boolean valueMissing;
   readonly attribute boolean typeMismatch;
   readonly attribute boolean patternMismatch;
   readonly attribute boolean tooLong;
   readonly attribute boolean rangeUnderflow;
   readonly attribute boolean rangeOverflow;
   readonly attribute boolean stepMismatch;
-//  readonly attribute boolean badInput;
+  readonly attribute boolean badInput;
   readonly attribute boolean customError;
   readonly attribute boolean valid;
 };
 
--- a/layout/forms/nsNumberControlFrame.cpp
+++ b/layout/forms/nsNumberControlFrame.cpp
@@ -590,16 +590,27 @@ nsNumberControlFrame::GetValueOfAnonText
   if (NS_finite(value) &&
       !HTMLInputElement::StringToDecimal(aValue).isFinite()) {
     aValue.Truncate();
     aValue.AppendFloat(value);
   }
 #endif
 }
 
+bool
+nsNumberControlFrame::AnonTextControlIsEmpty()
+{
+  if (!mTextField) {
+    return true;
+  }
+  nsAutoString value;
+  HTMLInputElement::FromContent(mTextField)->GetValue(value);
+  return value.IsEmpty();
+}
+
 Element*
 nsNumberControlFrame::GetPseudoElement(nsCSSPseudoElements::Type aType)
 {
   if (aType == nsCSSPseudoElements::ePseudo_mozNumberWrapper) {
     return mOuterWrapper;
   }
 
   if (aType == nsCSSPseudoElements::ePseudo_mozNumberText) {
--- a/layout/forms/nsNumberControlFrame.h
+++ b/layout/forms/nsNumberControlFrame.h
@@ -90,16 +90,18 @@ public:
    * This method gets the string value of our anonymous text control,
    * attempts to normalizes (de-localizes) it, then sets the outparam aValue to
    * the result. It's called when user input changes the text value of our
    * anonymous text control so that we can sync up the internal value of our
    * HTMLInputElement.
    */
   void GetValueOfAnonTextControl(nsAString& aValue);
 
+  bool AnonTextControlIsEmpty();
+
   /**
    * Called to notify this frame that its HTMLInputElement is currently
    * processing a DOM 'input' event.
    */
   void HandlingInputEvent(bool aHandlingEvent)
   {
     mHandlingInputEvent = aHandlingEvent;
   }
--- a/webapprt/locales/en-US/webapprt/overrides/dom.properties
+++ b/webapprt/locales/en-US/webapprt/overrides/dom.properties
@@ -36,16 +36,17 @@ FormValidationPatternMismatchWithTitle=P
 # LOCALIZATION NOTE (FormValidationRangeOverflow): %S can be a number, a date or a time.
 FormValidationRangeOverflow=Please select a value that is lower than %S.
 # LOCALIZATION NOTE (FormValidationRangeUnderflow): %S can be a number, a date or a time.
 FormValidationRangeUnderflow=Please select a value that is higher than %S.
 # LOCALIZATION NOTE (FormValidationStepMismatch): both %S can be a number, a date or a time.
 FormValidationStepMismatch=Please select a valid value. The two nearest valid values are %S and %S.
 # LOCALIZATION NOTE (FormValidationStepMismatchOneValue): %S can be a number, a date or a time. This is called instead of FormValidationStepMismatch when the second value is the same as the first.
 FormValidationStepMismatchOneValue=Please select a valid value. The nearest valid value is %S.
+FormValidationBadInputNumber=Please enter a number.
 GetAttributeNodeWarning=Use of getAttributeNode() is deprecated. Use getAttribute() instead.
 SetAttributeNodeWarning=Use of setAttributeNode() is deprecated. Use setAttribute() instead.
 GetAttributeNodeNSWarning=Use of getAttributeNodeNS() is deprecated. Use getAttributeNS() instead.
 SetAttributeNodeNSWarning=Use of setAttributeNodeNS() is deprecated. Use setAttributeNS() instead.
 RemoveAttributeNodeWarning=Use of removeAttributeNode() is deprecated. Use removeAttribute() instead.
 CreateAttributeWarning=Use of document.createAttribute() is deprecated. Use element.setAttribute() instead.
 CreateAttributeNSWarning=Use of document.createAttributeNS() is deprecated. Use element.setAttributeNS() instead.
 SpecifiedWarning=Use of attributes' specified attribute is deprecated. It always returns true.