mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 15:15:23 +00:00
273 lines
9.7 KiB
JavaScript
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]);
|