diff options
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillContentService.js')
-rw-r--r-- | toolkit/components/formautofill/FormAutofillContentService.js | 272 |
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]); |