Bug 1349490 - Part 2: Use a set of regexp to recognize the input autofill type.; r?MattN draft
authorSean Lee <selee@mozilla.com>
Fri, 28 Apr 2017 17:14:15 -0700
changeset 586717 cfe695cfe92c829921490410f67a56455d2dca9e
parent 585823 01e83900707b007c4205e477032ad65930abdff6
child 586718 0146a36e25da1c91ce7cd17b40ad6600ee3c22f4
push id61503
push userbmo:selee@mozilla.com
push dateWed, 31 May 2017 00:11:43 +0000
reviewersMattN
bugs1349490
milestone55.0a1
Bug 1349490 - Part 2: Use a set of regexp to recognize the input autofill type.; r?MattN MozReview-Commit-ID: B6ypdjBxlIV
browser/extensions/formautofill/FormAutofillHeuristics.jsm
browser/extensions/formautofill/content/heuristicsRegexp.js
browser/extensions/formautofill/test/fixtures/autocomplete_basic.html
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/heuristics/test_basic.js
browser/extensions/formautofill/test/unit/heuristics/third_party/test_CostCo.js
browser/extensions/formautofill/test/unit/heuristics/third_party/test_HomeDepot.js
browser/extensions/formautofill/test/unit/heuristics/third_party/test_Walmart.js
browser/extensions/formautofill/test/unit/test_collectFormFields.js
browser/extensions/formautofill/test/unit/test_getInfo.js
browser/extensions/formautofill/test/unit/xpcshell.ini
toolkit/content/license.html
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -7,48 +7,58 @@
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FormAutofillHeuristics"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 /**
  * Returns the autocomplete information of fields according to heuristics.
  */
 this.FormAutofillHeuristics = {
-  VALID_FIELDS: [
-    "name",
-    "given-name",
-    "additional-name",
-    "family-name",
-    "organization",
-    "street-address",
-    "address-line1",
-    "address-line2",
-    "address-line3",
-    "address-level2",
-    "address-level1",
-    "postal-code",
-    "country",
-    "tel",
-    "email",
-  ],
+  FIELD_GROUPS: {
+    NAME: [
+      "name",
+      "given-name",
+      "additional-name",
+      "family-name",
+    ],
+    ADDRESS: [
+      "organization",
+      "street-address",
+      "address-line1",
+      "address-line2",
+      "address-line3",
+      "address-level2",
+      "address-level1",
+      "postal-code",
+      "country",
+    ],
+    TEL: ["tel"],
+    EMAIL: ["email"],
+  },
+
+  RULES: null,
 
   getFormInfo(form) {
     let fieldDetails = [];
+    if (form.autocomplete == "off") {
+      return [];
+    }
     for (let element of form.elements) {
       // Exclude elements to which no autocomplete field has been assigned.
-      let info = this.getInfo(element);
+      let info = this.getInfo(element, fieldDetails);
       if (!info) {
         continue;
       }
 
       // Store the association between the field metadata and the element.
       if (fieldDetails.some(f => f.section == info.section &&
                                  f.addressType == info.addressType &&
                                  f.contactType == info.contactType &&
@@ -67,22 +77,124 @@ this.FormAutofillHeuristics = {
       };
 
       fieldDetails.push(formatWithElement);
     }
 
     return fieldDetails;
   },
 
-  getInfo(element) {
-    if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+  /**
+   * Get the autocomplete info (e.g. fieldName) determined by the regexp
+   * (this.RULES) matching to a feature string. The result is based on the
+   * existing field names to prevent duplicating predictions
+   * (e.g. address-line[1-3).
+   *
+   * @param {string} string a feature string to be determined.
+   * @param {Array<string>} existingFieldNames the array of exising field names
+   *                        in a form.
+   * @returns {Object}
+   *          Provide the predicting result including the field name.
+   *
+   */
+  _matchStringToFieldName(string, existingFieldNames) {
+    let result = {
+      fieldName: "",
+      section: "",
+      addressType: "",
+      contactType: "",
+    };
+    if (this.RULES.email.test(string)) {
+      result.fieldName = "email";
+      return result;
+    }
+    if (this.RULES.tel.test(string)) {
+      result.fieldName = "tel";
+      return result;
+    }
+    for (let fieldName of this.FIELD_GROUPS.ADDRESS) {
+      if (this.RULES[fieldName].test(string)) {
+        // If "address-line1" or "address-line2" exist already, the string
+        // could be satisfied with "address-line2" or "address-line3".
+        if ((fieldName == "address-line1" &&
+            existingFieldNames.includes("address-line1")) ||
+            (fieldName == "address-line2" &&
+            existingFieldNames.includes("address-line2"))) {
+          continue;
+        }
+        result.fieldName = fieldName;
+        return result;
+      }
+    }
+    for (let fieldName of this.FIELD_GROUPS.NAME) {
+      if (this.RULES[fieldName].test(string)) {
+        result.fieldName = fieldName;
+        return result;
+      }
+    }
+    return null;
+  },
+
+  getInfo(element, fieldDetails) {
+    if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
+        !["text", "email", "tel", "number"].includes(element.type) ||
+        element.autocomplete == "off") {
       return null;
     }
 
     let info = element.getAutocompleteInfo();
-    if (!info || !info.fieldName ||
-        !this.VALID_FIELDS.includes(info.fieldName)) {
+    // An input[autocomplete="on"] will not be early return here since it stll
+    // needs to find the field name.
+    if (info && info.fieldName && info.fieldName != "on") {
+      return info;
+    }
+
+    // "email" type of input is accurate for heuristics to determine its Email
+    // field or not. However, "tel" type is used for ZIP code for some web site
+    // (e.g. HomeDepot, BestBuy), so "tel" type should be not used for "tel"
+    // prediction.
+    if (element.type == "email") {
+      return {
+        fieldName: "email",
+        section: "",
+        addressType: "",
+        contactType: "",
+      };
+    }
+
+    let existingFieldNames = fieldDetails ? fieldDetails.map(i => i.fieldName) : "";
+
+    for (let elementString of [element.id, element.name]) {
+      let fieldNameResult = this._matchStringToFieldName(elementString,
+                                                         existingFieldNames);
+      if (fieldNameResult) {
+        return fieldNameResult;
+      }
+    }
+    let labels = FormAutofillUtils.findLabelElements(element);
+    if (!labels || labels.length == 0) {
+      log.debug("No label found for", element);
       return null;
     }
+    for (let label of labels) {
+      let strings = FormAutofillUtils.extractLabelStrings(label);
+      for (let string of strings) {
+        let fieldNameResult = this._matchStringToFieldName(string,
+                                                           existingFieldNames);
+        if (fieldNameResult) {
+          return fieldNameResult;
+        }
+      }
+    }
 
-    return info;
+    return null;
   },
 };
+
+XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "RULES", () => {
+  let sandbox = {};
+  let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+                       .getService(Ci.mozIJSSubScriptLoader);
+  const HEURISTICS_REGEXP = "chrome://formautofill/content/heuristicsRegexp.js";
+  scriptLoader.loadSubScript(HEURISTICS_REGEXP, sandbox, "utf-8");
+  return sandbox.HeuristicsRegExp.RULES;
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/heuristicsRegexp.js
@@ -0,0 +1,201 @@
+/* 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/. */
+
+/*
+ * Form Autofill field Heuristics RegExp.
+ */
+
+/* exported HeuristicsRegExp */
+
+"use strict";
+
+var HeuristicsRegExp = {
+  // These regular expressions are from Chromium source codes [1]. Most of them
+  // converted to JS format have the same meaning with the original ones except
+  // the first line of "address-level1".
+  // [1] https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_regex_constants.cc
+  RULES: {
+    // ==== Email ====
+    "email": new RegExp(
+      "e.?mail" +
+      "|courriel" +                                 // fr
+      "|メールアドレス" +                           // ja-JP
+      "|Электронной.?Почты" +                       // ru
+      "|邮件|邮箱" +                                // zh-CN
+      "|電郵地址" +                                 // zh-TW
+      "|(?:이메일|전자.?우편|[Ee]-?mail)(.?주소)?", // ko-KR
+      "iu"
+    ),
+
+    // ==== Telephone ====
+    "tel": new RegExp(
+      "phone|mobile|contact.?number" +
+      "|telefonnummer" +                             // de-DE
+      "|telefono|teléfono" +                         // es
+      "|telfixe" +                                   // fr-FR
+      "|電話" +                                      // ja-JP
+      "|telefone|telemovel" +                        // pt-BR, pt-PT
+      "|телефон" +                                   // ru
+      "|电话" +                                      // zh-CN
+      "|(?:전화|핸드폰|휴대폰|휴대전화)(?:.?번호)?", // ko-KR
+      "iu"
+    ),
+
+    // ==== Address Fields ====
+    "organization": new RegExp(
+      "company|business|organization|organisation" +
+      "|firma|firmenname" +   // de-DE
+      "|empresa" +            // es
+      "|societe|société" +    // fr-FR
+      "|ragione.?sociale" +   // it-IT
+      "|会社" +               // ja-JP
+      "|название.?компании" + // ru
+      "|单位|公司" +          // zh-CN
+      "|회사|직장",           // ko-KR
+      "iu"
+    ),
+    "street-address": new RegExp(
+      "streetaddress|street-address",
+      "iu"
+    ),
+    "address-line1": new RegExp(
+      "^address$|address[_-]?line(one)?|address1|addr1|street" +
+      "|(?:shipping|billing)address$" +
+      "|strasse|straße|hausnummer|housenumber" + // de-DE
+      "|house.?name" + // en-GB
+      "|direccion|dirección" + // es
+      "|adresse" + // fr-FR
+      "|indirizzo" + // it-IT
+      "|^住所$|住所1" + // ja-JP
+      "|morada|endereço" +  // pt-BR, pt-PT
+      "|Адрес" + // ru
+      "|地址" +  // zh-CN
+      "|^주소.?$|주소.?1",  // ko-KR
+      "iu"
+    ),
+    "address-line2": new RegExp(
+      "address[_-]?line(2|two)|address2|addr2|street|suite|unit" +
+      "|adresszusatz|ergänzende.?angaben" + // de-DE
+      "|direccion2|colonia|adicional" + // es
+      "|addresssuppl|complementnom|appartement" + // fr-FR
+      "|indirizzo2" + // it-IT
+      "|住所2" + // ja-JP
+      "|complemento|addrcomplement" + // pt-BR, pt-PT
+      "|Улица" + // ru
+      "|地址2" + // zh-CN
+      "|주소.?2",  // ko-KR
+      "iu"
+    ),
+    "address-line3": new RegExp(
+      "address[_-]?line(3|three)|address3|addr3|street|suite|unit" +
+      "|adresszusatz|ergänzende.?angaben" + // de-DE
+      "|direccion3|colonia|adicional" + // es
+      "|addresssuppl|complementnom|appartement" + // fr-FR
+      "|indirizzo3" + // it-IT
+      "|住所3" + // ja-JP
+      "|complemento|addrcomplement" + // pt-BR, pt-PT
+      "|Улица" + // ru
+      "|地址3" + // zh-CN
+      "|주소.?3",  // ko-KR
+      "iu"
+    ),
+    "address-level2": new RegExp(
+      "city|town" +
+      "|\\bort\\b|stadt" + // de-DE
+      "|suburb" + // en-AU
+      "|ciudad|provincia|localidad|poblacion" + // es
+      "|ville|commune" + // fr-FR
+      "|localita" +  // it-IT
+      "|市区町村" +  // ja-JP
+      "|cidade" + // pt-BR, pt-PT
+      "|Город" + // ru
+      "|市" + // zh-CN
+      "|分區" + // zh-TW
+      "|^시[^도·・]|시[·・]?군[·・]?구",  // ko-KR
+      "iu"
+    ),
+    "address-level1": new RegExp(
+      // TODO: [Bug 1358960] JS does not support backward matching, and we
+      // should apply this pattern in JS rather than regexp.
+      // "(?<!united )state|county|region|province"
+      "state|county|region|province" +
+      "|land" + // de-DE
+      "|county|principality" + // en-UK
+      "|都道府県" + // ja-JP
+      "|estado|provincia" + // pt-BR, pt-PT
+      "|область" + // ru
+      "|省" + // zh-CN
+      "|地區" + // zh-TW
+      "|^시[·・]?도",  // ko-KR
+      "iu"
+    ),
+    "postal-code": new RegExp(
+      "zip|postal|post.*code|pcode" +
+      "|pin.?code" +               // en-IN
+      "|postleitzahl" +            // de-DE
+      "|\\bcp\\b" +                // es
+      "|\\bcdp\\b" +               // fr-FR
+      "|\\bcap\\b" +               // it-IT
+      "|郵便番号" +                // ja-JP
+      "|codigo|codpos|\\bcep\\b" + // pt-BR, pt-PT
+      "|Почтовый.?Индекс" +        // ru
+      "|邮政编码|邮编" +           // zh-CN
+      "|郵遞區號" +                // zh-TW
+      "|우편.?번호",               // ko-KR
+      "iu"
+    ),
+    "country": new RegExp(
+      "country|countries" +
+      "|país|pais" + // es
+      "|国" +        // ja-JP
+      "|国家" +      // zh-CN
+      "|국가|나라",  // ko-KR
+      "iu"
+    ),
+
+    // ==== Name Fields ====
+    "given-name": new RegExp(
+      "first.*name|initials|fname|first$|given.*name" +
+      "|vorname" +                // de-DE
+      "|nombre" +                 // es
+      "|forename|prénom|prenom" + // fr-FR
+      "|名" +                     // ja-JP
+      "|nome" +                   // pt-BR, pt-PT
+      "|Имя" +                    // ru
+      "|이름",                    // ko-KR
+      "iu"
+    ),
+    "additional-name": new RegExp(
+      "middle.*name|mname|middle$" +
+      "|apellido.?materno|lastlastname" + // es
+
+      // This rule is for middle initial.
+      "middle.*initial|m\\.i\\.|mi$|\\bmi\\b",
+      "iu"
+    ),
+    "family-name": new RegExp(
+      "last.*name|lname|surname|last$|secondname|family.*name" +
+      "|nachname" +                           // de-DE
+      "|apellido" +                           // es
+      "|famille|^nom" +                       // fr-FR
+      "|cognome" +                            // it-IT
+      "|姓" +                                 // ja-JP
+      "|morada|apelidos|surename|sobrenome" + // pt-BR, pt-PT
+      "|Фамилия" +                            // ru
+      "|\\b성(?:[^명]|\\b)",                  // ko-KR
+      "iu"
+    ),
+    "name": new RegExp(
+      "^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name" +
+      "|name.*first.*last|firstandlastname" +
+      "|nombre.*y.*apellidos" + // es
+      "|^nom" +                 // fr-FR
+      "|お名前|氏名" +          // ja-JP
+      "|^nome" +                // pt-BR, pt-PT
+      "|姓名" +                 // zh-CN
+      "|성명",                  // ko-KR
+      "iu"
+    ),
+  },
+};
--- a/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html
+++ b/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html
@@ -13,11 +13,27 @@
     <p><label>addressLevel1: <input type="text" id="address-level1" name="address-level1" autocomplete="address-level1" /></label></p>
     <p><label>postalCode: <input type="text" id="postal-code" name="postal-code" autocomplete="postal-code" /></label></p>
     <p><label>country: <input type="text" id="country" name="country" autocomplete="country" /></label></p>
     <p><label>tel: <input type="text" id="tel" name="tel" autocomplete="tel" /></label></p>
     <p><label>email: <input type="text" id="email" name="email" autocomplete="email" /></label></p>
     <p><input type="submit" /></p>
     <p><button type="reset">Reset</button></p>
   </form>
+
+  <form id="formB">
+    <p><label>Organization: <input type="text" /></label></p>
+    <p><label><input type="text" id="B_address-line1" /></label></p>
+    <p><label><input type="text" name="address-line2" /></label></p>
+    <p><label><input type="text" id="B_address-line3" name="address-line3" /></label></p>
+    <p><label>City: <input type="text" name="address-level2" /></label></p>
+    <p><label>State: <input type="text" id="B_address-level1" /></label></p>
+    <p><input type="text" id="B_postal-code" name="postal-code" /></p>
+    <p><label>Country: <input type="text" id="B_country" name="country" /></label></p>
+    <p><label>Telephone: <input type="text" id="B_tel" name="tel" /></label></p>
+    <p><label>Email: <input type="text" id="B_email" name="email" /></label></p>
+    <p><input type="submit" /></p>
+    <p><button type="reset">Reset</button></p>
+  </form>
+
 </body>
 </html>
 
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -101,18 +101,18 @@ function runHeuristicsTest(patterns, fix
           }
         }
       }
 
       Assert.equal(forms.length, testPattern.expectedResult.length, "Expected form count.");
 
       forms.forEach((form, formIndex) => {
         let formInfo = FormAutofillHeuristics.getFormInfo(form);
-        // TODO: This line should be uncommented to make sure every field are verified.
-        // Assert.equal(formInfo.length, testPattern.expectedResult[formIndex].length, "Expected field count.");
+        do_print("FieldName Prediction Results: " + formInfo.map(i => i.fieldName));
+        Assert.equal(formInfo.length, testPattern.expectedResult[formIndex].length, "Expected field count.");
         formInfo.forEach((field, fieldIndex) => {
           let expectedField = testPattern.expectedResult[formIndex][fieldIndex];
           expectedField.elementWeakRef = field.elementWeakRef;
           Assert.deepEqual(field, expectedField);
         });
       });
     });
   });
--- a/browser/extensions/formautofill/test/unit/heuristics/test_basic.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/test_basic.js
@@ -11,12 +11,24 @@ runHeuristicsTest([
         {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       ],
+      [
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line3"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
+      ],
     ],
   },
 ], "../../fixtures/");
 
--- a/browser/extensions/formautofill/test/unit/heuristics/third_party/test_CostCo.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/third_party/test_CostCo.js
@@ -10,31 +10,31 @@ runHeuristicsTest([
       [],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "additional-name"}, // middle-name initial
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"}, // TODO: select,country
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"}, // TODO: fix the regexp
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"}, // city
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"}, // TODO: select,state
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       ],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "additional-name"}, // middle-name initial
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"}, // TODO: select,country
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"}, // TODO: fix the regexp
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"}, // city
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"}, // TODO: select,state
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       ],
       [],
       [
@@ -60,31 +60,31 @@ runHeuristicsTest([
       [],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "additional-name"}, // middle-name initial
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"}, // TODO: select, country
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"}, // TODO: fix the regexp
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"}, // city
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"}, // TODO: select, state
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       ],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "additional-name"}, // middle-name initial
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"}, // TODO: select, country
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"}, // TODO: fix the regexp
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"}, // TODO: select, state
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       ],
       [],
       [
--- a/browser/extensions/formautofill/test/unit/heuristics/third_party/test_HomeDepot.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/third_party/test_HomeDepot.js
@@ -8,18 +8,18 @@ runHeuristicsTest([
     expectedResult: [
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-csc"},
       ],
     ],
   }, {
     fixturePath: "SignIn.html",
     expectedResult: [
       [
--- a/browser/extensions/formautofill/test/unit/heuristics/third_party/test_Walmart.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/third_party/test_Walmart.js
@@ -4,18 +4,17 @@
 
 runHeuristicsTest([
   {
     fixturePath: "Checkout.html",
     expectedResult: [
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
       ],
-      [
-      ],
+      [],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "password"}, // ac-off
       ],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "email"}, // ac-off
@@ -26,17 +25,17 @@ runHeuristicsTest([
   }, {
     fixturePath: "Payment.html",
     expectedResult: [
       [
       ],
       [
         {"section": "section-payment", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "section-payment", "addressType": "", "contactType": "", "fieldName": "family-name"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
+        {"section": "section-payment", "addressType": "", "contactType": "", "fieldName": "cc-number"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-csc"},
         {"section": "section-payment", "addressType": "", "contactType": "", "fieldName": "tel"},
       ],
     ],
   }, {
     fixturePath: "Shipping.html",
@@ -46,17 +45,17 @@ runHeuristicsTest([
       ],
       [
       ],
       [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
-//      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"}, // TODO: fix the regexp
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"}, // city
 //      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"}, // TODO: select, state
         {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
       ],
     ],
   },
 ], "../../../fixtures/third_party/Walmart/");
 
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -7,18 +7,26 @@
 Cu.import("resource://formautofill/FormAutofillHandler.jsm");
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><input id="country">
                <input id='email'><input id="tel"></form>`,
-    returnedFormat: [],
-    fieldDetails: [],
+    fieldDetails: [
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
+    ],
+    ids: ["given-name", "family-name", "street-addr", "city", "country", "email", "tel"],
   },
   {
     description: "Form with autocomplete properties and 1 token",
     document: `<form><input id="given-name" autocomplete="given-name">
                <input id="family-name" autocomplete="family-name">
                <input id="street-addr" autocomplete="street-address">
                <input id="city" autocomplete="address-level2">
                <input id="country" autocomplete="country">
@@ -79,19 +87,24 @@ for (let tc of TESTCASES) {
     let testcase = tc;
     add_task(function* () {
       do_print("Starting testcase: " + testcase.description);
 
       let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                 testcase.document);
       let form = doc.querySelector("form");
 
-      testcase.fieldDetails.forEach((detail) => {
-        detail.elementWeakRef = Cu.getWeakReference(doc.querySelector(
-          "input[autocomplete*='" + detail.fieldName + "']"));
+      testcase.fieldDetails.forEach((detail, index) => {
+        let elementRef;
+        if (testcase.ids && testcase.ids[index]) {
+          elementRef = doc.getElementById(testcase.ids[index]);
+        } else {
+          elementRef = doc.querySelector("input[autocomplete*='" + detail.fieldName + "']");
+        }
+        detail.elementWeakRef = Cu.getWeakReference(elementRef);
       });
       let handler = new FormAutofillHandler(form);
 
       handler.collectFormFields();
 
       handler.fieldDetails.forEach((detail, index) => {
         Assert.equal(detail.section, testcase.fieldDetails[index].section);
         Assert.equal(detail.addressType, testcase.fieldDetails[index].addressType);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getInfo.js
@@ -0,0 +1,198 @@
+"use strict";
+
+Cu.import("resource://formautofill/FormAutofillHeuristics.jsm");
+
+const TESTCASES = [
+  {
+    description: "Input element in a label element",
+    document: `<form>
+                 <label> E-Mail
+                   <input id="targetElement" type="text">
+                 </label>
+               </form>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "A label element is out of the form contains the related input",
+    document: `<label for="targetElement"> E-Mail</label>
+               <form>
+                 <input id="targetElement" type="text">
+               </form>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "A label element contains span element",
+    document: `<label for="targetElement">FOO<span>E-Mail</span>BAR</label>
+               <form>
+                 <input id="targetElement" type="text">
+               </form>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "The signature in 'name' attr of an input",
+    document: `<input id="targetElement" name="email" type="text">`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "The signature in 'id' attr of an input",
+    document: `<input id="targetElement_email" name="tel" type="text">`,
+    elementId: "targetElement_email",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "2 address line inputs",
+    document: `<label for="targetElement">street</label>
+               <input id="targetElement" type="text">`,
+    elementId: "targetElement",
+    fieldDetails: [{fieldName: "address-line1"}],
+    expectedReturnValue: {
+      fieldName: "address-line2",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "3 address line inputs",
+    document: `<label for="targetElement">street</label>
+               <input id="targetElement" type="text">`,
+    elementId: "targetElement",
+    fieldDetails: [
+      {fieldName: "address-line1"},
+      {fieldName: "address-line2"},
+    ],
+    expectedReturnValue: {
+      fieldName: "address-line3",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "CJK character - Traditional Chinese",
+    document: `<label> 郵遞區號
+                 <input id="targetElement" />
+               </label>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "postal-code",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "CJK character - Japanese",
+    document: `<label> 郵便番号
+                 <input id="targetElement" />
+               </label>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "postal-code",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "CJK character - Korean",
+    document: `<label> 우편 번호
+                 <input id="targetElement" />
+               </label>`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "postal-code",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "",
+    document: `<input id="targetElement" name="fullname">`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "name",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "non-input element",
+    document: `<label id="targetElement">street</label>`,
+    elementId: "targetElement",
+    expectedReturnValue: null,
+  },
+  {
+    description: "input element with \"submit\" type",
+    document: `<input id="targetElement" type="submit" />`,
+    elementId: "targetElement",
+    expectedReturnValue: null,
+  },
+  {
+    description: "The signature in 'name' attr of an email input",
+    document: `<input id="targetElement" name="email" type="number">`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+  {
+    description: "input element with \"email\" type",
+    document: `<input id="targetElement" type="email" />`,
+    elementId: "targetElement",
+    expectedReturnValue: {
+      fieldName: "email",
+      section: "",
+      addressType: "",
+      contactType: "",
+    },
+  },
+];
+
+TESTCASES.forEach(testcase => {
+  add_task(function* () {
+    do_print("Starting testcase: " + testcase.description);
+
+    let doc = MockDocument.createTestDocument(
+      "http://localhost:8080/test/", testcase.document);
+
+    let element = doc.getElementById(testcase.elementId);
+    let value = FormAutofillHeuristics.getInfo(element, testcase.fieldDetails);
+
+    Assert.deepEqual(value, testcase.expectedReturnValue);
+  });
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -20,15 +20,16 @@ support-files =
 [test_addressRecords.js]
 [test_autofillFormFields.js]
 [test_collectFormFields.js]
 [test_creditCardRecords.js]
 [test_extractLabelStrings.js]
 [test_findLabelElements.js]
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
+[test_getInfo.js]
 [test_isCJKName.js]
 [test_markAsAutofillField.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
 [test_profileAutocompleteResult.js]
 [test_savedFieldNames.js]
 [test_transformFields.js]
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -2712,18 +2712,21 @@ WITH THE USE OR PERFORMANCE OF THIS SOFT
 
 
     <hr>
 
     <h1><a id="chromium"></a>Chromium License</h1>
 
     <p>This license applies to parts of the code in:</p>
     <ul>
+#ifndef RELEASE_OR_BETA
+        <li><code>browser/extensions/formautofill/content/heuristicsRegexp.js</code></li>
         <li><code>browser/extensions/formautofill/content/nameReferences.js</code></li>
         <li><code>browser/extensions/formautofill/FormAutofillNameUtils.jsm</code></li>
+#endif
         <li><code>browser/extensions/mortar/host/common/opengles2-utils.jsm</code></li>
         <li><code>editor/libeditor/EditorEventListener.cpp</code></li>
         <li><code>security/sandbox/</code></li>
         <li><code>widget/cocoa/GfxInfo.mm</code></li>
     </ul>
     <p>and also some files in these directories:</p>
     <ul>
         <li><code>browser/extensions/mortar/ppapi/</code></li>