gecko-dev/toolkit/components/formautofill/FormAutofillContentService.js

273 lines
9.7 KiB
JavaScript

/* 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]);