Bug 1300990 - Implement an API in the parent process to decide which values of an autofill profile would be filled in which field; r=mattn draft
authorLuke Chang <lchang@mozilla.com>
Tue, 08 Nov 2016 14:45:54 +0800
changeset 435728 0056b25d51f5e9b714a1ee01daad76e34ec6f8cc
parent 434636 908557c762f798605a2f96e4c943791cbada1b50
child 536373 46074f13d66f08a2f8523c92f84593c8408f2d61
push id35113
push userbmo:lchang@mozilla.com
push dateWed, 09 Nov 2016 03:42:36 +0000
reviewersmattn
bugs1300990
milestone52.0a1
Bug 1300990 - Implement an API in the parent process to decide which values of an autofill profile would be filled in which field; r=mattn MozReview-Commit-ID: 41MdWifXa23
browser/extensions/formautofill/content/FormAutofillParent.jsm
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/test_populateFieldValues.js
browser/extensions/formautofill/test/unit/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/FormAutofillParent.jsm
@@ -0,0 +1,173 @@
+/* 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/. */
+
+/*
+ * Implements a service used to access storage and communicate with content.
+ *
+ * A "fields" array is used to communicate with FormAutofillContent. Each item
+ * represents a single input field in the content page as well as its
+ * @autocomplete properties. The schema is as below. Please refer to
+ * FormAutofillContent.jsm for more details.
+ *
+ * [
+ *   {
+ *     section,
+ *     addressType,
+ *     contactType,
+ *     fieldName,
+ *     value,
+ *     index
+ *   },
+ *   {
+ *     // ...
+ *   }
+ * ]
+ */
+
+/* exported FormAutofillParent */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileStorage",
+                                  "resource://formautofill/ProfileStorage.jsm");
+
+const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
+
+let FormAutofillParent = {
+  _profileStore: null,
+
+  /**
+   * Initializes ProfileStorage and registers the message handler.
+   */
+  init: function() {
+    let storePath =
+      OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME);
+
+    this._profileStore = new ProfileStorage(storePath);
+    this._profileStore.initialize();
+
+    let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+               .getService(Ci.nsIMessageListenerManager);
+    mm.addMessageListener("FormAutofill:PopulateFieldValues", this);
+  },
+
+  /**
+   * Handles the message coming from FormAutofillContent.
+   *
+   * @param   {string} message.name The name of the message.
+   * @param   {object} message.data The data of the message.
+   * @param   {nsIFrameMessageManager} message.target Caller's message manager.
+   */
+  receiveMessage: function({name, data, target}) {
+    switch (name) {
+      case "FormAutofill:PopulateFieldValues":
+        this._populateFieldValues(data, target);
+        break;
+    }
+  },
+
+  /**
+   * Returns the instance of ProfileStorage. To avoid syncing issues, anyone
+   * who needs to access the profile should request the instance by this instead
+   * of creating a new one.
+   *
+   * @returns {ProfileStorage}
+   */
+  getProfileStore: function() {
+    return this._profileStore;
+  },
+
+  /**
+   * Uninitializes FormAutofillParent. This is for testing only.
+   *
+   * @private
+   */
+  _uninit: function() {
+    if (this._profileStore) {
+      this._profileStore._saveImmediately();
+      this._profileStore = null;
+    }
+
+    let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+               .getService(Ci.nsIMessageListenerManager);
+    mm.removeMessageListener("FormAutofill:PopulateFieldValues", this);
+  },
+
+  /**
+   * Populates the field values and notifies content to fill in. Exception will
+   * be thrown if there's no matching profile.
+   *
+   * @private
+   * @param  {string} data.guid
+   *         Indicates which profile to populate
+   * @param  {Fields} data.fields
+   *         The "fields" array collected from content.
+   * @param  {nsIFrameMessageManager} target
+   *         Content's message manager.
+   */
+  _populateFieldValues({guid, fields}, target) {
+    this._profileStore.notifyUsed(guid);
+    this._fillInFields(this._profileStore.get(guid), fields);
+    target.sendAsyncMessage("FormAutofill:fillForm", {fields});
+  },
+
+  /**
+   * Transforms a word with hyphen into camel case.
+   * (e.g. transforms "address-type" into "addressType".)
+   *
+   * @private
+   * @param   {string} str The original string with hyphen.
+   * @returns {string} The camel-cased output string.
+   */
+  _camelCase(str) {
+    return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
+  },
+
+  /**
+   * Get the corresponding value from the specified profile according to a valid
+   * @autocomplete field name.
+   *
+   * Note that the field name doesn't need to match the property name defined in
+   * Profile object. This method can transform the raw data to fulfill it. (e.g.
+   * inputting "country-name" as "fieldName" will get a full name transformed
+   * from the country code that is recorded in "country" field.)
+   *
+   * @private
+   * @param   {Profile} profile   The specified profile.
+   * @param   {string}  fieldName A valid @autocomplete field name.
+   * @returns {string}  The corresponding value. Returns "undefined" if there's
+   *                    no matching field.
+   */
+  _getDataByFieldName(profile, fieldName) {
+    let key = this._camelCase(fieldName);
+
+    // TODO: Transform the raw profile data to fulfill "fieldName" here.
+
+    return profile[key];
+  },
+
+  /**
+   * Fills in the "fields" array by the specified profile.
+   *
+   * @private
+   * @param   {Profile} profile The specified profile to fill in.
+   * @param   {Fields}  fields  The "fields" array collected from content.
+   */
+  _fillInFields(profile, fields) {
+    for (let field of fields) {
+      let value = this._getDataByFieldName(profile, field.fieldName);
+      if (value !== undefined) {
+        field.value = value;
+      }
+    }
+  },
+};
+
+this.EXPORTED_SYMBOLS = ["FormAutofillParent"];
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -7,16 +7,26 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://testing-common/MockDocument.jsm");
 
+// Redirect the path of the resouce in addon to the exact file path.
+let defineLazyModuleGetter = XPCOMUtils.defineLazyModuleGetter;
+XPCOMUtils.defineLazyModuleGetter = function() {
+  let result = /^resource\:\/\/formautofill\/(.+)$/.exec(arguments[2]);
+  if (result) {
+    arguments[2] = Services.io.newFileURI(do_get_file(result[1])).spec;
+  }
+  return defineLazyModuleGetter.apply(this, arguments);
+};
+
 // Load the module by Service newFileURI API for running extension's XPCShell test
 function importAutofillModule(module) {
   return Cu.import(Services.io.newFileURI(do_get_file(module)).spec);
 }
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                   "resource://gre/modules/DownloadPaths.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_populateFieldValues.js
@@ -0,0 +1,106 @@
+/*
+ * Test for populating field values in Form Autofill Parent.
+ */
+
+/* global FormAutofillParent */
+
+"use strict";
+
+importAutofillModule("FormAutofillParent.jsm");
+
+do_get_profile();
+
+const TEST_FIELDS = [
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "organization"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "postal-code"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
+  {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+];
+
+const TEST_GUID = "test-guid";
+
+const TEST_PROFILE = {
+  guid: TEST_GUID,
+  organization: "World Wide Web Consortium",
+  streetAddress: "32 Vassar Street\nMIT Room 32-G524",
+  addressLevel2: "Cambridge",
+  addressLevel1: "MA",
+  postalCode: "02139",
+  country: "US",
+  tel: "+1 617 253 5702",
+  email: "timbl@w3.org",
+};
+
+function camelCase(str) {
+  return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
+}
+
+add_task(function* test_populateFieldValues() {
+  FormAutofillParent.init();
+
+  let store = FormAutofillParent.getProfileStore();
+  do_check_neq(store, null);
+
+  store.get = function(guid) {
+    do_check_eq(guid, TEST_GUID);
+    return store._clone(TEST_PROFILE);
+  };
+
+  let notifyUsedCalledCount = 0;
+  store.notifyUsed = function(guid) {
+    do_check_eq(guid, TEST_GUID);
+    notifyUsedCalledCount++;
+  };
+
+  yield new Promise((resolve) => {
+    FormAutofillParent.receiveMessage({
+      name: "FormAutofill:PopulateFieldValues",
+      data: {
+        guid: TEST_GUID,
+        fields: TEST_FIELDS,
+      },
+      target: {
+        sendAsyncMessage: function(name, data) {
+          do_check_eq(name, "FormAutofill:fillForm");
+
+          let fields = data.fields;
+          do_check_eq(fields.length, TEST_FIELDS.length);
+
+          for (let i = 0; i < fields.length; i++) {
+            do_check_eq(fields[i].fieldName, TEST_FIELDS[i].fieldName);
+            do_check_eq(fields[i].value,
+              TEST_PROFILE[camelCase(fields[i].fieldName)]);
+          }
+
+          resolve();
+        },
+      },
+    });
+  });
+
+  do_check_eq(notifyUsedCalledCount, 1);
+
+  FormAutofillParent._uninit();
+  do_check_null(FormAutofillParent.getProfileStore());
+});
+
+add_task(function* test_populateFieldValues_with_invalid_guid() {
+  FormAutofillParent.init();
+
+  Assert.throws(() => {
+    FormAutofillParent.receiveMessage({
+      name: "FormAutofill:PopulateFieldValues",
+      data: {
+        guid: "invalid-guid",
+        fields: TEST_FIELDS,
+      },
+      target: {},
+    });
+  }, /No matching profile\./);
+
+  FormAutofillParent._uninit();
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 head = head.js
 tail =
 support-files =
   ../../content/FormAutofillContent.jsm
+  ../../content/FormAutofillParent.jsm
   ../../content/ProfileStorage.jsm
 
 [test_autofillFormFields.js]
 [test_collectFormFields.js]
+[test_populateFieldValues.js]
 [test_profileStorage.js]