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
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]