Bug 1358944 - [Form Autofill] Support "country-name" fields. r=MattN, seanlee draft
authorLuke Chang <lchang@mozilla.com>
Fri, 26 May 2017 19:30:07 +0800
changeset 599457 fc9457ab35b4e1763f5fd021b90a9f8d760ed025
parent 599255 b1b9129838ade91684574f42219b2010928d7db4
child 634784 257602999d9ecaf80d95ba4067bcaa887291e487
push id65533
push userbmo:lchang@mozilla.com
push dateFri, 23 Jun 2017 06:45:51 +0000
reviewersMattN, seanlee
bugs1358944
milestone56.0a1
Bug 1358944 - [Form Autofill] Support "country-name" fields. r=MattN, seanlee MozReview-Commit-ID: IV0WGFhQ35R
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/ProfileStorage.jsm
browser/extensions/formautofill/content/manageProfiles.js
browser/extensions/formautofill/test/unit/test_transformFields.js
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -20,16 +20,17 @@ this.FormAutofillUtils = {
     "street-address": "address",
     "address-line1": "address",
     "address-line2": "address",
     "address-line3": "address",
     "address-level1": "address",
     "address-level2": "address",
     "postal-code": "address",
     "country": "address",
+    "country-name": "address",
     "tel": "tel",
     "email": "email",
     "cc-name": "creditCard",
     "cc-number": "creditCard",
     "cc-exp-month": "creditCard",
     "cc-exp-year": "creditCard",
   },
 
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -88,53 +88,58 @@ ProfileAutoCompleteResult.prototype = {
    * Get the secondary label based on the focused field name and related field names
    * in the same form.
    * @param   {string} focusedFieldName The field name of the focused input
    * @param   {Array<Object>} allFieldNames The field names in the same section
    * @param   {object} profile The profile providing the labels to show.
    * @returns {string} The secondary label
    */
   _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
-    /* TODO: Since "name" is a special case here, so the secondary "name" label
-       will be refined when the handling rule for "name" is ready.
-    */
-    const possibleNameFields = [
-      "name",
-      "given-name",
-      "additional-name",
-      "family-name",
-    ];
-
-    focusedFieldName = possibleNameFields.includes(focusedFieldName) ?
-                       "name" : focusedFieldName;
+    // We group similar fields into the same field name so we won't pick another
+    // field in the same group as the secondary label.
+    const GROUP_FIELDS = {
+      "name": [
+        "name",
+        "given-name",
+        "additional-name",
+        "family-name",
+      ],
+      "country-name": [
+        "country",
+        "country-name",
+      ],
+    };
 
     const secondaryLabelOrder = [
       "street-address",  // Street address
       "name",            // Full name
       "address-level2",  // City/Town
       "organization",    // Company or organization name
       "address-level1",  // Province/State (Standardized code if possible)
-      "country",         // Country
+      "country-name",    // Country name
       "postal-code",     // Postal code
       "tel",             // Phone number
       "email",           // Email address
     ];
 
+    for (let field in GROUP_FIELDS) {
+      if (GROUP_FIELDS[field].includes(focusedFieldName)) {
+        focusedFieldName = field;
+        break;
+      }
+    }
+
     for (const currentFieldName of secondaryLabelOrder) {
-      if (focusedFieldName == currentFieldName ||
-          !profile[currentFieldName]) {
+      if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
         continue;
       }
 
-      let matching;
-      if (currentFieldName == "name") {
-        matching = allFieldNames.some(fieldName => possibleNameFields.includes(fieldName));
-      } else {
-        matching = allFieldNames.includes(currentFieldName);
-      }
+      let matching = GROUP_FIELDS[currentFieldName] ?
+        allFieldNames.some(fieldName => GROUP_FIELDS[currentFieldName].includes(fieldName)) :
+        allFieldNames.includes(currentFieldName);
 
       if (matching) {
         return profile[currentFieldName];
       }
     }
 
     return ""; // Nothing matched.
   },
--- a/browser/extensions/formautofill/ProfileStorage.jsm
+++ b/browser/extensions/formautofill/ProfileStorage.jsm
@@ -30,16 +30,17 @@
  *       email,
  *
  *       // computed fields (These fields are not stored in the file as they are
  *       // generated at runtime.)
  *       name,
  *       address-line1,
  *       address-line2,
  *       address-line3,
+ *       country-name,
  *
  *       // metadata
  *       timeCreated,          // in ms
  *       timeLastUsed,         // in ms
  *       timeLastModified,     // in ms
  *       timesUsed
  *     }
  *   ],
@@ -90,16 +91,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/JSONFile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillNameUtils",
                                   "resource://formautofill/FormAutofillNameUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyGetter(this, "REGION_NAMES", function() {
+  let regionNames = {};
+  let countries = Services.strings.createBundle("chrome://global/locale/regionNames.properties").getSimpleEnumeration();
+  while (countries.hasMoreElements()) {
+    let country = countries.getNext().QueryInterface(Components.interfaces.nsIPropertyElement);
+    regionNames[country.key.toUpperCase()] = country.value;
+  }
+  return regionNames;
+});
+
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
 const CREDIT_CARD_SCHEMA_VERSION = 1;
 
 const VALID_PROFILE_FIELDS = [
   "given-name",
@@ -413,16 +424,30 @@ class Addresses extends AutofillRecords 
       // TODO: we should prevent the dataloss by concatenating the rest of lines
       //       with a locale-specific character in the future (bug 1360114).
       for (let i = 0; i < 3; i++) {
         if (streetAddress[i]) {
           profile["address-line" + (i + 1)] = streetAddress[i];
         }
       }
     }
+
+    // Compute country name
+    if (profile.country) {
+      if (profile.country == "US") {
+        let countryName = REGION_NAMES[profile.country];
+        if (countryName) {
+          profile["country-name"] = countryName;
+        }
+      } else {
+        // TODO: We only support US in MVP so hide the field if it's not. We
+        //       are going to support more countries in bug 1370193.
+        delete profile.country;
+      }
+    }
   }
 
   _recordWriteProcessor(profile) {
     // Normalize name
     if (profile.name) {
       let nameParts = FormAutofillNameUtils.splitName(profile.name);
       if (!profile["given-name"] && nameParts.given) {
         profile["given-name"] = nameParts.given;
@@ -454,16 +479,35 @@ class Addresses extends AutofillRecords 
         return value;
       });
 
       // Concatenate "address-line*" if "street-address" is omitted.
       if (!profile["street-address"]) {
         profile["street-address"] = addressLines.join("\n");
       }
     }
+
+    // Normalize country
+    if (profile.country) {
+      let country = profile.country.toUpperCase();
+      // Only values included in the region list will be saved.
+      if (REGION_NAMES[country]) {
+        profile.country = country;
+      } else {
+        delete profile.country;
+      }
+    } else if (profile["country-name"]) {
+      for (let region in REGION_NAMES) {
+        if (REGION_NAMES[region].toLowerCase() == profile["country-name"].toLowerCase()) {
+          profile.country = region;
+          break;
+        }
+      }
+    }
+    delete profile["country-name"];
   }
 
   /**
    * Merge new address into the specified address if mergeable.
    *
    * @param  {string} guid
    *         Indicates which address to merge.
    * @param  {Object} address
--- a/browser/extensions/formautofill/content/manageProfiles.js
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -140,17 +140,17 @@ ManageProfileDialog.prototype = {
     //       as option text. Possibly improve the algorithm in
     //       ProfileAutoCompleteResult.jsm and reuse it here.
     const fieldOrder = [
       "name",
       "street-address",  // Street address
       "address-level2",  // City/Town
       "organization",    // Company or organization name
       "address-level1",  // Province/State (Standardized code if possible)
-      "country",         // Country
+      "country-name",    // Country name
       "postal-code",     // Postal code
       "tel",             // Phone number
       "email",           // Email address
     ];
 
     let parts = [];
     for (const fieldName of fieldOrder) {
       let string = address[fieldName];
--- a/browser/extensions/formautofill/test/unit/test_transformFields.js
+++ b/browser/extensions/formautofill/test/unit/test_transformFields.js
@@ -60,32 +60,44 @@ const ADDRESS_COMPUTE_TESTCASES = [
   {
     description: "\"street-address\" with multiple lines but line2 is omitted",
     address: {
       "street-address": "line1\n\nline3",
     },
     expectedResult: {
       "street-address": "line1\n\nline3",
       "address-line1": "line1",
-      "address-line2": "",
+      "address-line2": undefined,
       "address-line3": "line3",
     },
   },
   {
     description: "\"street-address\" with 4 lines",
     address: {
       "street-address": "line1\nline2\nline3\nline4",
     },
     expectedResult: {
       "street-address": "line1\nline2\nline3\nline4",
       "address-line1": "line1",
       "address-line2": "line2",
       "address-line3": "line3",
     },
   },
+
+  // Country
+  {
+    description: "Has \"country\"",
+    address: {
+      "country": "US",
+    },
+    expectedResult: {
+      "country": "US",
+      "country-name": "United States",
+    },
+  },
 ];
 
 const ADDRESS_NORMALIZE_TESTCASES = [
   // Empty
   {
     description: "Empty address",
     address: {
     },
@@ -173,16 +185,78 @@ const ADDRESS_NORMALIZE_TESTCASES = [
       "street-address": "street address\nstreet address line 2",
       "address-line2": "line2",
       "address-line3": "line3",
     },
     expectedResult: {
       "street-address": "street address\nstreet address line 2",
     },
   },
+
+  // Country
+  {
+    description: "Has \"country\" in lowercase",
+    address: {
+      "country": "us",
+    },
+    expectedResult: {
+      "country": "US",
+    },
+  },
+  {
+    description: "Has unknown \"country\"",
+    address: {
+      "country": "AA",
+    },
+    expectedResult: {
+      "country": undefined,
+    },
+  },
+  {
+    description: "Has \"country-name\"",
+    address: {
+      "country-name": "united states",
+    },
+    expectedResult: {
+      "country": "US",
+      "country-name": undefined,
+    },
+  },
+  {
+    description: "Has unknown \"country-name\"",
+    address: {
+      "country-name": "unknown country name",
+    },
+    expectedResult: {
+      "country": undefined,
+      "country-name": undefined,
+    },
+  },
+  {
+    description: "Has \"country\" and unknown \"country-name\"",
+    address: {
+      "country": "us",
+      "country-name": "unknown country name",
+    },
+    expectedResult: {
+      "country": "US",
+      "country-name": undefined,
+    },
+  },
+  {
+    description: "Has \"country-name\" and unknown \"country\"",
+    address: {
+      "country": "AA",
+      "country-name": "united states",
+    },
+    expectedResult: {
+      "country": undefined,
+      "country-name": undefined,
+    },
+  },
 ];
 
 const CREDIT_CARD_COMPUTE_TESTCASES = [
   // Empty
   {
     description: "Empty credit card",
     creditCard: {
     },
@@ -236,17 +310,17 @@ const CREDIT_CARD_NORMALIZE_TESTCASES = 
     expectedResult: {
       "cc-name": "John Doe",
     },
   },
 ];
 
 let do_check_record_matches = (expectedRecord, record) => {
   for (let key in expectedRecord) {
-    do_check_eq(expectedRecord[key], record[key] || "");
+    do_check_eq(expectedRecord[key], record[key]);
   }
 };
 
 add_task(async function test_computeAddressFields() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new ProfileStorage(path);
   await profileStorage.initialize();
@@ -272,17 +346,17 @@ add_task(async function test_normalizeAd
   await profileStorage.initialize();
 
   ADDRESS_NORMALIZE_TESTCASES.forEach(testcase => profileStorage.addresses.add(testcase.address));
   await profileStorage._saveImmediately();
 
   profileStorage = new ProfileStorage(path);
   await profileStorage.initialize();
 
-  let addresses = profileStorage.addresses.getAll();
+  let addresses = profileStorage.addresses.getAll({noComputedFields: true});
 
   for (let i in addresses) {
     do_print("Verify testcase: " + ADDRESS_NORMALIZE_TESTCASES[i].description);
     do_check_record_matches(ADDRESS_NORMALIZE_TESTCASES[i].expectedResult, addresses[i]);
   }
 });
 
 add_task(async function test_computeCreditCardFields() {