Bug 769379 - Adds sanitizing algorithm for <input type=date>. r=mounir
authorRaphael Catolino <rcatolino@mozilla.com>
Thu, 27 Dec 2012 18:48:13 +0000
changeset 126216 d44a92b2cbf774a0fdc4b0ce33add7a7dd608a59
parent 126215 0ec205214f5f4d20cd178c92544e9e37b0ac2de0
child 126217 78970aaa8008f602dcbb622b8c573fdc5d1036d1
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmounir
bugs769379
milestone20.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 769379 - Adds sanitizing algorithm for <input type=date>. r=mounir
content/html/content/src/nsHTMLInputElement.cpp
content/html/content/src/nsHTMLInputElement.h
content/html/content/test/Makefile.in
content/html/content/test/forms/Makefile.in
content/html/content/test/forms/test_input_sanitization.html
content/html/content/test/test_bug549475.html
content/html/content/test/test_bug590363.html
--- a/content/html/content/src/nsHTMLInputElement.cpp
+++ b/content/html/content/src/nsHTMLInputElement.cpp
@@ -2752,20 +2752,155 @@ nsHTMLInputElement::SanitizeValue(nsAStr
       {
         nsresult ec;
         PromiseFlatString(aValue).ToDouble(&ec);
         if (NS_FAILED(ec)) {
           aValue.Truncate();
         }
       }
       break;
+    case NS_FORM_INPUT_DATE:
+      {
+        if (!aValue.IsEmpty() && !IsValidDate(aValue)) {
+          aValue.Truncate();
+        }
+      }
+      break;
   }
 }
 
 bool
+nsHTMLInputElement::IsValidDate(nsAString& aValue) const
+{
+/*
+ * Parse the year, month, day values out a date string formatted as 'yyy-mm-dd'.
+ * -The year must be 4 or more digits long, and year > 0
+ * -The month must be exactly 2 digits long, and 01 <= month <= 12
+ * -The day must be exactly 2 digit long, and 01 <= day <= maxday
+ *  Where maxday is the number of days in the month 'month' and year 'year'
+ */
+
+  if (aValue.IsEmpty()) {
+    return false;
+  }
+
+  uint32_t year = 0;
+  uint32_t month = 0;
+  uint32_t day = 0;
+  int32_t fieldMaxSize = 0;
+  int32_t fieldMinSize = 4;
+  enum {
+    YEAR, MONTH, DAY, NONE
+  } field;
+  int32_t fieldSize = 0;
+  nsresult ec;
+
+  field = YEAR;
+  for (uint32_t offset = 0; offset < aValue.Length(); ++offset) {
+    // Test if the fied size is superior to its maximum size.
+    if (fieldMaxSize && fieldSize > fieldMaxSize) {
+      return false;
+    }
+
+    // Illegal char.
+    if (aValue[offset] != '-' && !NS_IsAsciiDigit(aValue[offset])) {
+      return false;
+    }
+
+    // There are more characters in this field.
+    if (aValue[offset] != '-' && offset != aValue.Length()-1) {
+      fieldSize++;
+      continue;
+    }
+
+    // Parse the field.
+    if (fieldSize < fieldMinSize) {
+      return false;
+    }
+
+    switch(field) {
+      case YEAR:
+        year = PromiseFlatString(StringHead(aValue, offset)).ToInteger(&ec);
+        NS_ENSURE_SUCCESS(ec, false);
+
+        if (year <= 0) {
+          return false;
+        }
+
+        // The field after year is month, which have a fixed size of 2 char.
+        field = MONTH;
+        fieldMaxSize = 2;
+        fieldMinSize = 2;
+        break;
+      case MONTH:
+        month = PromiseFlatString(Substring(aValue,
+                                            offset-fieldSize,
+                                            offset)).ToInteger(&ec);
+        NS_ENSURE_SUCCESS(ec, false);
+
+        if (month < 1 || month > 12) {
+          return false;
+        }
+
+        // The next field is the last one, we won't parse a '-',
+        // so the field size will be one char smaller.
+        field = DAY;
+        fieldMinSize = 1;
+        fieldMaxSize = 1;
+        break;
+      case DAY:
+        day = PromiseFlatString(Substring(aValue,
+                                          offset-fieldSize,
+                                          offset + 1)).ToInteger(&ec);
+        NS_ENSURE_SUCCESS(ec, false);
+
+        if (day <  1 || day > NumberOfDaysInMonth(month, year)) {
+          return false;
+        }
+
+        field = NONE;
+        break;
+      default:
+        return false;
+    }
+
+    fieldSize = 0;
+  }
+
+  return field == NONE;
+}
+
+uint32_t
+nsHTMLInputElement::NumberOfDaysInMonth(uint32_t aMonth, uint32_t aYear) const
+{
+/*
+ * Returns the number of days in a month.
+ * Months that are |longMonths| always have 31 days.
+ * Months that are not |longMonths| have 30 days except February (month 2).
+ * February has 29 days during leap years which are years that are divisible by 400.
+ * or divisible by 100 and 4. February has 28 days otherwise.
+ */
+
+  static const bool longMonths[] = { true, false, true, false, true, false,
+                                     true, true, false, true, false, true };
+  MOZ_ASSERT(aMonth <= 12 && aMonth > 0);
+
+  if (longMonths[aMonth-1]) {
+    return 31;
+  }
+
+  if (aMonth != 2) {
+    return 30;
+  }
+
+  return (aYear % 400 == 0 || (aYear % 100 != 0 && aYear % 4 == 0))
+          ? 29 : 28;
+}
+ 
+bool
 nsHTMLInputElement::ParseAttribute(int32_t aNamespaceID,
                                    nsIAtom* aAttribute,
                                    const nsAString& aValue,
                                    nsAttrValue& aResult)
 {
   if (aNamespaceID == kNameSpaceID_None) {
     if (aAttribute == nsGkAtoms::type) {
       // XXX ARG!! This is major evilness. ParseAttribute
--- a/content/html/content/src/nsHTMLInputElement.h
+++ b/content/html/content/src/nsHTMLInputElement.h
@@ -556,16 +556,29 @@ protected:
    * Returns the input element's value as a double-precision float.
    * Returns NaN if the current element's value is not a floating point number.
    *
    * @return the input element's value as a double-precision float.
    */
   double GetValueAsDouble() const;
 
   /**
+   * Parse a date string of the form yyyy-mm-dd
+   * @param the string to be parsed.
+   * @return whether the string is a valid date.
+   * Note : this function does not consider the empty string as valid.
+   */
+  bool IsValidDate(nsAString& aValue) const;
+
+  /**
+   * This methods returns the number of days in a given month, for a given year.
+   */
+  uint32_t NumberOfDaysInMonth(uint32_t aMonth, uint32_t aYear) const;
+
+  /**
    * Sets the value of the element to the string representation of the double.
    *
    * @param aValue The double that will be used to set the value.
    */
   void SetValue(double aValue);
 
   /**
    * Update the HAS_RANGE bit field value.
--- a/content/html/content/test/Makefile.in
+++ b/content/html/content/test/Makefile.in
@@ -154,17 +154,16 @@ MOCHITEST_FILES = \
 		test_bug564001.html \
 		test_bug566046.html \
 		test_bug567938-1.html \
 		test_bug567938-2.html \
 		test_bug567938-3.html \
 		test_bug567938-4.html \
 		test_bug569955.html \
 		test_bug573969.html \
-		test_bug549475.html \
 		test_bug585508.html \
 		test_bug561640.html \
 		test_bug566064.html \
 		test_bug582412-1.html \
 		test_bug582412-2.html \
 		test_bug558788-1.html \
 		test_bug558788-2.html \
 		test_bug561634.html \
--- a/content/html/content/test/forms/Makefile.in
+++ b/content/html/content/test/forms/Makefile.in
@@ -44,12 +44,13 @@ MOCHITEST_FILES = \
 		test_meter_pseudo-classes.html \
 		test_max_attribute.html \
 		test_min_attribute.html \
 		test_step_attribute.html \
 		test_stepup_stepdown.html \
 		test_valueasnumber_attribute.html \
 		test_experimental_forms_pref.html \
 		test_input_number_value.html \
+		test_input_sanitization.html \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
rename from content/html/content/test/test_bug549475.html
rename to content/html/content/test/forms/test_input_sanitization.html
--- a/content/html/content/test/test_bug549475.html
+++ b/content/html/content/test/forms/test_input_sanitization.html
@@ -35,50 +35,124 @@ var todoTypes =
 ];
 
 var valueModeValue =
 [
   "text", "search", "url", "tel", "email", "password", "date", "datetime",
   "month", "week", "time", "datetime-local", "number", "range", "color",
 ];
 
+function checkDateSanitizing(element) {
+  var invalidData =
+  [
+    "1234",
+    "1234-",
+    "12345",
+    "1234-01",
+    "1234-012",
+    "1234-01-",
+    "12-12",
+    "999-01-01",
+    "1234-56-78-91",
+    "1234-567-78",
+    "1234--7-78",
+    "abcd-12-12",
+    "thisinotadate",
+    "2012-13-01",
+    "1234-12-42",
+    " 2012-13-01",
+    " 123-01-01",
+    "2012- 3-01",
+    "12- 10- 01",
+    "  12-0-1",
+    "2012-3-001",
+    "2012-12-00",
+    "2012-12-1r",
+    "2012-11-31",
+    "2011-02-29",
+    "2100-02-29",
+    "a2000-01-01",
+    "2000a-01-0'",
+    "20aa00-01-01",
+    "2000a2000-01-01",
+    "2000-1-1",
+    "2000-1-01",
+    "2000-01-1",
+    "2000-01-01 ",
+    "2000- 01-01",
+    "-1970-01-01",
+    "0000-00-00",
+    "0001-00-00",
+    "0000-01-01",
+    "1234-12 12",
+    "1234 12-12",
+    "1234 12 12",
+  ];
+
+  var validData =
+  [
+    "1970-01-01",
+    "1234-12-12",
+    "1234567890-01-02",
+    "2012-12-31",
+    "2012-02-29",
+    "2000-02-29",
+  ];
+
+  for (data of validData) {
+    element.value = data;
+    is(element.value, data, "valid date should not be sanitized");
+  }
+
+  for (data of invalidData) {
+    element.value = data;
+    is(element.value, "", "invalid date should be sanitized");
+  }
+}
+
 function sanitizeValue(aType, aValue)
 {
-  switch (aType) {
-    case "text":
-    case "password":
-    case "search":
-    case "tel":
-      return aValue.replace(/[\n\r]/g, "");
-    case "url":
-    case "email":
-      return aValue.replace(/[\n\r]/g, "").replace(/^\s+|\s+$/g, "");
-    case "date":
-    case "month":
-    case "week":
-    case "time":
-    case "datetime":
-    case "datetime-local":
-      // TODO: write the sanitize algorithm.
-      return "";
-    case "number":
-      return (parseFloat(aValue) + "" === aValue) ? aValue : "";
-    case "range":
-      // TODO: write the sanitize algorithm.
-      return "";
-    case "color":
-      // TODO: write the sanitize algorithm.
-      return "";
-    default:
-      return aValue;
-  }
+	switch (aType) {
+		case "text":
+		case "password":
+		case "search":
+		case "tel":
+			return aValue.replace(/[\n\r]/g, "");
+		case "url":
+		case "email":
+			return aValue.replace(/[\n\r]/g, "").replace(/^\s+|\s+$/g, "");
+		case "date":
+			return "";
+		case "month":
+		case "week":
+		case "time":
+		case "datetime":
+		case "datetime-local":
+			// TODO: write the sanitize algorithm.
+			return "";
+		case "number":
+			return (parseFloat(aValue) + "" === aValue) ? aValue : "";
+		case "range":
+			// TODO: write the sanitize algorithm.
+			return "";
+		case "color":
+			// TODO: write the sanitize algorithm.
+			return "";
+		default:
+			return aValue;
+	}
 }
 
 function checkSanitizing(element)
 {
+  if (element.type == 'date') {
+    checkDateSanitizing(element);
+    return;
+  }
+
   var testData =
   [
     // For text, password, search, tel, email:
     "\n\rfoo\n\r",
     "foo\n\rbar",
     "  foo  ",
     "  foo\n\r  bar  ",
     // For url:
--- a/content/html/content/test/test_bug590363.html
+++ b/content/html/content/test/test_bug590363.html
@@ -38,25 +38,33 @@ var testData = [
   [ "date",     true ],
   // 'file' is treated separatly.
 ];
 
 var todoTypes = [
   "datetime", "month", "week", "time", "datetime-local", "range", "color"
 ];
 
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({'set': [["dom.experimental_forms", true]]}, function() {
+
 var length = testData.length;
 for (var i=0; i<length; ++i) {
   for (var j=0; j<length; ++j) {
     var e = document.createElement('input');
     e.type = testData[i][0];
 
     var expectedValue;
-    if (testData[i][0] == 'number' || testData[j][0] == 'number') {
+    if ((testData[i][0] == 'number' && testData[j][0] == 'date') ||
+        (testData[i][0] == 'date' && testData[j][0] == 'number')) {
+      expectedValue = '';
+    } else if (testData[i][0] == 'number' || testData[j][0] == 'number') {
       expectedValue = '42';
+    } else if (testData[i][0] == 'date' || testData[j][0] == 'date') {
+      expectedValue = '2012-12-21';
     } else {
       expectedValue = "foo";
     }
     e.value = expectedValue;
 
     e.type = testData[j][0];
     is(e.value, expectedValue, ".value should still return the same value after " +
        "changing type from " + testData[i][0] + " to " + testData[j][0]);
@@ -83,12 +91,15 @@ for (var data of testData) {
 
 // TODO checks
 for (var type of todoTypes) {
   var e = document.createElement('input');
   e.type = type;
   todo_is(e.type, type, type + " type isn't supported yet");
 }
 
+SimpleTest.finish();
+});
+
 </script>
 </pre>
 </body>
 </html>