summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/content
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /browser/extensions/formautofill/content
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'browser/extensions/formautofill/content')
-rw-r--r--browser/extensions/formautofill/content/FormAutofillContent.jsm134
-rw-r--r--browser/extensions/formautofill/content/FormAutofillParent.jsm173
-rw-r--r--browser/extensions/formautofill/content/ProfileStorage.jsm251
3 files changed, 558 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/content/FormAutofillContent.jsm b/browser/extensions/formautofill/content/FormAutofillContent.jsm
new file mode 100644
index 000000000..bde397580
--- /dev/null
+++ b/browser/extensions/formautofill/content/FormAutofillContent.jsm
@@ -0,0 +1,134 @@
+/* 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 by DOM content to request Form Autofill.
+ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/**
+ * Handles profile autofill for a DOM Form element.
+ * @param {HTMLFormElement} form Form that need to be auto filled
+ */
+function FormAutofillHandler(form) {
+ this.form = form;
+ this.fieldDetails = [];
+}
+
+FormAutofillHandler.prototype = {
+ /**
+ * DOM Form element to which this object is attached.
+ */
+ form: null,
+
+ /**
+ * Array of collected data about relevant form fields. Each item is an object
+ * storing the identifying details of the field and a reference to the
+ * originally associated element from the form.
+ *
+ * The "section", "addressType", "contactType", and "fieldName" values are
+ * used to identify the exact field when the serializable data is received
+ * from the backend. There cannot be multiple fields which have
+ * the same exact combination of these values.
+ *
+ * A direct reference to the associated element cannot be sent to the user
+ * interface because processing may be done in the parent process.
+ */
+ fieldDetails: null,
+
+ /**
+ * Returns information from the form about fields that can be autofilled, and
+ * populates the fieldDetails array on this object accordingly.
+ *
+ * @returns {Array<Object>} Serializable data structure that can be sent to the user
+ * interface, or null if the operation failed because the constraints
+ * on the allowed fields were not honored.
+ */
+ collectFormFields() {
+ let autofillData = [];
+
+ for (let element of this.form.elements) {
+ // Query the interface and exclude elements that cannot be autocompleted.
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+ continue;
+ }
+
+ // Exclude elements to which no autocomplete field has been assigned.
+ let info = element.getAutocompleteInfo();
+ if (!info.fieldName || ["on", "off"].includes(info.fieldName)) {
+ continue;
+ }
+
+ // Store the association between the field metadata and the element.
+ if (this.fieldDetails.some(f => f.section == info.section &&
+ f.addressType == info.addressType &&
+ f.contactType == info.contactType &&
+ f.fieldName == info.fieldName)) {
+ // A field with the same identifier already exists.
+ return null;
+ }
+
+ let inputFormat = {
+ section: info.section,
+ addressType: info.addressType,
+ contactType: info.contactType,
+ fieldName: info.fieldName,
+ };
+ // Clone the inputFormat for caching the fields and elements together
+ let formatWithElement = Object.assign({}, inputFormat);
+
+ inputFormat.index = autofillData.length;
+ autofillData.push(inputFormat);
+
+ formatWithElement.element = element;
+ this.fieldDetails.push(formatWithElement);
+ }
+
+ return autofillData;
+ },
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * data provided by backend.
+ *
+ * @param {Array<Object>} autofillResult
+ * Data returned by the user interface.
+ * [{
+ * section: Value originally provided to the user interface.
+ * addressType: Value originally provided to the user interface.
+ * contactType: Value originally provided to the user interface.
+ * fieldName: Value originally provided to the user interface.
+ * value: String with which the field should be updated.
+ * index: Index to match the input in fieldDetails
+ * }],
+ * }
+ */
+ autofillFormFields(autofillResult) {
+ for (let field of autofillResult) {
+ // Get the field details, if it was processed by the user interface.
+ let fieldDetail = this.fieldDetails[field.index];
+
+ // Avoid the invalid value set
+ if (!fieldDetail || !field.value) {
+ continue;
+ }
+
+ let info = fieldDetail.element.getAutocompleteInfo();
+ if (field.section != info.section ||
+ field.addressType != info.addressType ||
+ field.contactType != info.contactType ||
+ field.fieldName != info.fieldName) {
+ Cu.reportError("Autocomplete tokens mismatched");
+ continue;
+ }
+
+ fieldDetail.element.setUserInput(field.value);
+ }
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["FormAutofillHandler"];
diff --git a/browser/extensions/formautofill/content/FormAutofillParent.jsm b/browser/extensions/formautofill/content/FormAutofillParent.jsm
new file mode 100644
index 000000000..bdfe0f478
--- /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"];
diff --git a/browser/extensions/formautofill/content/ProfileStorage.jsm b/browser/extensions/formautofill/content/ProfileStorage.jsm
new file mode 100644
index 000000000..843177d4e
--- /dev/null
+++ b/browser/extensions/formautofill/content/ProfileStorage.jsm
@@ -0,0 +1,251 @@
+/* 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 an interface of the storage of Form Autofill.
+ *
+ * The data is stored in JSON format, without indentation, using UTF-8 encoding.
+ * With indentation applied, the file would look like this:
+ *
+ * {
+ * version: 1,
+ * profiles: [
+ * {
+ * guid, // 12 character...
+ *
+ * // profile
+ * organization, // Company
+ * streetAddress, // (Multiline)
+ * addressLevel2, // City/Town
+ * addressLevel1, // Province (Standardized code if possible)
+ * postalCode,
+ * country, // ISO 3166
+ * tel,
+ * email,
+ *
+ * // metadata
+ * timeCreated, // in ms
+ * timeLastUsed, // in ms
+ * timeLastModified, // in ms
+ * timesUsed
+ * },
+ * {
+ * // ...
+ * }
+ * ]
+ * }
+ */
+
+"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://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+ "resource://gre/modules/JSONFile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const SCHEMA_VERSION = 1;
+
+// Name-related fields will be handled in follow-up bugs due to the complexity.
+const VALID_FIELDS = [
+ "organization",
+ "streetAddress",
+ "addressLevel2",
+ "addressLevel1",
+ "postalCode",
+ "country",
+ "tel",
+ "email",
+];
+
+function ProfileStorage(path) {
+ this._path = path;
+}
+
+ProfileStorage.prototype = {
+ /**
+ * Loads the profile data from file to memory.
+ *
+ * @returns {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ initialize() {
+ this._store = new JSONFile({
+ path: this._path,
+ dataPostProcessor: this._dataPostProcessor.bind(this),
+ });
+ return this._store.load();
+ },
+
+ /**
+ * Adds a new profile.
+ *
+ * @param {Profile} profile
+ * The new profile for saving.
+ */
+ add(profile) {
+ this._store.ensureDataReady();
+
+ let profileToSave = this._normalizeProfile(profile);
+
+ profileToSave.guid = gUUIDGenerator.generateUUID().toString()
+ .replace(/[{}-]/g, "").substring(0, 12);
+
+ // Metadata
+ let now = Date.now();
+ profileToSave.timeCreated = now;
+ profileToSave.timeLastModified = now;
+ profileToSave.timeLastUsed = 0;
+ profileToSave.timesUsed = 0;
+
+ this._store.data.profiles.push(profileToSave);
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Update the specified profile.
+ *
+ * @param {string} guid
+ * Indicates which profile to update.
+ * @param {Profile} profile
+ * The new profile used to overwrite the old one.
+ */
+ update(guid, profile) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ let profileToUpdate = this._normalizeProfile(profile);
+ for (let field of VALID_FIELDS) {
+ if (profileToUpdate[field] !== undefined) {
+ profileFound[field] = profileToUpdate[field];
+ } else {
+ delete profileFound[field];
+ }
+ }
+
+ profileFound.timeLastModified = Date.now();
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Notifies the stroage of the use of the specified profile, so we can update
+ * the metadata accordingly.
+ *
+ * @param {string} guid
+ * Indicates which profile to be notified.
+ */
+ notifyUsed(guid) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ profileFound.timesUsed++;
+ profileFound.timeLastUsed = Date.now();
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Removes the specified profile. No error occurs if the profile isn't found.
+ *
+ * @param {string} guid
+ * Indicates which profile to remove.
+ */
+ remove(guid) {
+ this._store.ensureDataReady();
+
+ this._store.data.profiles =
+ this._store.data.profiles.filter(profile => profile.guid != guid);
+ this._store.saveSoon();
+ },
+
+ /**
+ * Returns the profile with the specified GUID.
+ *
+ * @param {string} guid
+ * Indicates which profile to retrieve.
+ * @returns {Profile}
+ * A clone of the profile.
+ */
+ get(guid) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ // Profile is cloned to avoid accidental modifications from outside.
+ return this._clone(profileFound);
+ },
+
+ /**
+ * Returns all profiles.
+ *
+ * @returns {Array.<Profile>}
+ * An array containing clones of all profiles.
+ */
+ getAll() {
+ this._store.ensureDataReady();
+
+ // Profiles are cloned to avoid accidental modifications from outside.
+ return this._store.data.profiles.map(this._clone);
+ },
+
+ _clone(profile) {
+ return Object.assign({}, profile);
+ },
+
+ _findByGUID(guid) {
+ return this._store.data.profiles.find(profile => profile.guid == guid);
+ },
+
+ _normalizeProfile(profile) {
+ let result = {};
+ for (let key in profile) {
+ if (!VALID_FIELDS.includes(key)) {
+ throw new Error(`"${key}" is not a valid field.`);
+ }
+ if (typeof profile[key] !== "string" &&
+ typeof profile[key] !== "number") {
+ throw new Error(`"${key}" contains invalid data type.`);
+ }
+
+ result[key] = profile[key];
+ }
+ return result;
+ },
+
+ _dataPostProcessor(data) {
+ data.version = SCHEMA_VERSION;
+ if (!data.profiles) {
+ data.profiles = [];
+ }
+ return data;
+ },
+
+ // For test only.
+ _saveImmediately() {
+ return this._store._save();
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["ProfileStorage"];