summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/FormAutofillContentService.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillContentService.js')
-rw-r--r--toolkit/components/formautofill/FormAutofillContentService.js272
1 files changed, 272 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofillContentService.js b/toolkit/components/formautofill/FormAutofillContentService.js
new file mode 100644
index 000000000..ee8e978ad
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillContentService.js
@@ -0,0 +1,272 @@
+/* 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, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ *
+ * See the nsIFormAutofillContentService documentation for details.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * Handles requestAutocomplete for a DOM Form element.
+ */
+function FormHandler(aForm, aWindow) {
+ this.form = aForm;
+ this.window = aWindow;
+
+ this.fieldDetails = [];
+}
+
+FormHandler.prototype = {
+ /**
+ * DOM Form element to which this object is attached.
+ */
+ form: null,
+
+ /**
+ * nsIDOMWindow to which this object is attached.
+ */
+ window: 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 requestAutocomplete user interface. 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,
+
+ /**
+ * Handles requestAutocomplete and generates the DOM events when finished.
+ */
+ handleRequestAutocomplete: Task.async(function* () {
+ // Start processing the request asynchronously. At the end, the "reason"
+ // variable will contain the outcome of the operation, where an empty
+ // string indicates that an unexpected exception occurred.
+ let reason = "";
+ try {
+ reason = yield this.promiseRequestAutocomplete();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // The type of event depends on whether this is a success condition.
+ let event = (reason == "success")
+ ? new this.window.Event("autocomplete", { bubbles: true })
+ : new this.window.AutocompleteErrorEvent("autocompleteerror",
+ { bubbles: true,
+ reason: reason });
+ yield this.waitForTick();
+ this.form.dispatchEvent(event);
+ }),
+
+ /**
+ * Handles requestAutocomplete and returns the outcome when finished.
+ *
+ * @return {Promise}
+ * @resolves The "reason" value indicating the outcome of the
+ * requestAutocomplete operation, including "success" if the
+ * operation completed successfully.
+ */
+ promiseRequestAutocomplete: Task.async(function* () {
+ let data = this.collectFormFields();
+ if (!data) {
+ return "disabled";
+ }
+
+ // Access the frame message manager of the window starting the request.
+ let rootDocShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell);
+ let frameMM = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ // We need to set up a temporary message listener for our result before we
+ // send the request to the parent process. At present, there is no check
+ // for reentrancy (bug 1020459), thus it is possible that we'll receive a
+ // message for a different request, but this will not be normally allowed.
+ let promiseRequestAutocompleteResult = new Promise((resolve, reject) => {
+ frameMM.addMessageListener("FormAutofill:RequestAutocompleteResult",
+ function onResult(aMessage) {
+ frameMM.removeMessageListener(
+ "FormAutofill:RequestAutocompleteResult", onResult);
+ // Exceptions in the parent process are serialized and propagated in
+ // the response message that we received.
+ if ("exception" in aMessage.data) {
+ reject(aMessage.data.exception);
+ } else {
+ resolve(aMessage.data);
+ }
+ });
+ });
+
+ // Send the message to the parent process, and wait for the result. This
+ // will throw an exception if one occurred in the parent process.
+ frameMM.sendAsyncMessage("FormAutofill:RequestAutocomplete", data);
+ let result = yield promiseRequestAutocompleteResult;
+ if (result.canceled) {
+ return "cancel";
+ }
+
+ this.autofillFormFields(result);
+
+ return "success";
+ }),
+
+ /**
+ * Returns information from the form about fields that can be autofilled, and
+ * populates the fieldDetails array on this object accordingly.
+ *
+ * @returns 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: function () {
+ let autofillData = {
+ sections: [],
+ };
+
+ 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"].indexOf(info.fieldName) != -1) {
+ 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;
+ }
+ this.fieldDetails.push({
+ section: info.section,
+ addressType: info.addressType,
+ contactType: info.contactType,
+ fieldName: info.fieldName,
+ element: element,
+ });
+
+ // The first level is the custom section.
+ let section = autofillData.sections
+ .find(s => s.name == info.section);
+ if (!section) {
+ section = {
+ name: info.section,
+ addressSections: [],
+ };
+ autofillData.sections.push(section);
+ }
+
+ // The second level is the address section.
+ let addressSection = section.addressSections
+ .find(s => s.addressType == info.addressType);
+ if (!addressSection) {
+ addressSection = {
+ addressType: info.addressType,
+ fields: [],
+ };
+ section.addressSections.push(addressSection);
+ }
+
+ // The third level contains all the fields within the section.
+ let field = {
+ fieldName: info.fieldName,
+ contactType: info.contactType,
+ };
+ addressSection.fields.push(field);
+ }
+
+ return autofillData;
+ },
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * data provided by RequestAutocompleteUI.
+ *
+ * @param aAutofillResult
+ * Data returned by the user interface.
+ * {
+ * fields: [
+ * 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.
+ * ],
+ * }
+ */
+ autofillFormFields: function (aAutofillResult) {
+ for (let field of aAutofillResult.fields) {
+ // Get the field details, if it was processed by the user interface.
+ let fieldDetail = this.fieldDetails
+ .find(f => f.section == field.section &&
+ f.addressType == field.addressType &&
+ f.contactType == field.contactType &&
+ f.fieldName == field.fieldName);
+ if (!fieldDetail) {
+ continue;
+ }
+
+ fieldDetail.element.value = field.value;
+ }
+ },
+
+ /**
+ * Waits for one tick of the event loop before resolving the returned promise.
+ */
+ waitForTick: function () {
+ return new Promise(function (resolve) {
+ Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ },
+};
+
+/**
+ * Implements a service used by DOM content to request Form Autofill, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ */
+function FormAutofillContentService() {
+}
+
+FormAutofillContentService.prototype = {
+ classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutofillContentService]),
+
+ // nsIFormAutofillContentService
+ requestAutocomplete: function (aForm, aWindow) {
+ new FormHandler(aForm, aWindow).handleRequestAutocomplete()
+ .catch(Cu.reportError);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillContentService]);