diff --git a/toolkit/components/formautofill/FormAutofillContentService.js b/toolkit/components/formautofill/FormAutofillContentService.js index a6d8867984e..7cf8a5ebfa2 100644 --- a/toolkit/components/formautofill/FormAutofillContentService.js +++ b/toolkit/components/formautofill/FormAutofillContentService.js @@ -27,6 +27,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", function FormHandler(aForm, aWindow) { this.form = aForm; this.window = aWindow; + + this.fieldDetails = []; } FormHandler.prototype = { @@ -40,6 +42,21 @@ FormHandler.prototype = { */ 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. */ @@ -49,14 +66,7 @@ FormHandler.prototype = { // string indicates that an unexpected exception occurred. let reason = ""; try { - let data = this.collectFormElements(); - - let ui = yield FormAutofill.integration.createRequestAutocompleteUI(data); - let result = yield ui.show(); - - // At present, we only have cancellation and success cases, since we - // don't do any validation or precondition check. - reason = result.canceled ? "cancel" : "success"; + reason = yield this.promiseRequestAutocomplete(); } catch (ex) { Cu.reportError(ex); } @@ -72,10 +82,39 @@ FormHandler.prototype = { }), /** - * Collects information from the form about fields that can be autofilled, and - * returns an object that can be used to build RequestAutocompleteUI. + * 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. */ - collectFormElements: function () { + promiseRequestAutocomplete: Task.async(function* () { + let data = this.collectFormFields(); + if (!data) { + return "disabled"; + } + + let ui = yield FormAutofill.integration.createRequestAutocompleteUI(data); + let result = yield ui.show(); + 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: [], }; @@ -92,6 +131,22 @@ FormHandler.prototype = { 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); @@ -125,6 +180,38 @@ FormHandler.prototype = { 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. */ diff --git a/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm index c5ee7d698aa..74c4834ba6f 100644 --- a/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm +++ b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm @@ -26,11 +26,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", * Handles the requestAutocomplete user interface. */ this.RequestAutocompleteUI = function (aAutofillData) { - Services.console.logStringMessage("rAc UI request: " + - JSON.stringify(aAutofillData)); + this._autofillData = aAutofillData; } this.RequestAutocompleteUI.prototype = { + _autofillData: null, + show: Task.async(function* () { // Create a new promise and store the function that will resolve it. This // will be called by the UI once the selection has been made. @@ -38,7 +39,10 @@ this.RequestAutocompleteUI.prototype = { let uiPromise = new Promise(resolve => resolveFn = resolve); // Wrap the callback function so that it survives XPCOM. - let args = { resolveFn: resolveFn }; + let args = { + resolveFn: resolveFn, + autofillData: this._autofillData, + }; args.wrappedJSObject = args; // Open the window providing the function to call when it closes. diff --git a/toolkit/components/formautofill/content/requestAutocomplete.js b/toolkit/components/formautofill/content/requestAutocomplete.js index 0b0f431ac5d..928e5dc965f 100644 --- a/toolkit/components/formautofill/content/requestAutocomplete.js +++ b/toolkit/components/formautofill/content/requestAutocomplete.js @@ -20,10 +20,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", const RequestAutocompleteDialog = { resolveFn: null, + autofillData: null, onLoad: function () { Task.spawn(function* () { - this.resolveFn = window.arguments[0].wrappedJSObject.resolveFn; + let args = window.arguments[0].wrappedJSObject; + this.resolveFn = args.resolveFn; + this.autofillData = args.autofillData; window.sizeToContent(); @@ -33,8 +36,46 @@ const RequestAutocompleteDialog = { }, onAccept: function () { + // TODO: Replace with autofill storage module (bug 1018304). + const dummyDB = { + "": { + "name": "Mozzy La", + "street-address": "331 E Evelyn Ave", + "address-level2": "Mountain View", + "address-level1": "CA", + "country": "US", + "postal-code": "94041", + "email": "email@example.org", + } + }; + + let result = { fields: [] }; + for (let section of this.autofillData.sections) { + for (let addressSection of section.addressSections) { + let addressType = addressSection.addressType; + if (!(addressType in dummyDB)) { + continue; + } + + for (let field of addressSection.fields) { + let fieldName = field.fieldName; + if (!(fieldName in dummyDB[addressType])) { + continue; + } + + result.fields.push({ + section: section.name, + addressType: addressType, + contactType: field.contactType, + fieldName: field.fieldName, + value: dummyDB[addressType][fieldName], + }); + } + } + } + window.close(); - this.resolveFn({ email: "email@example.org" }); + this.resolveFn(result); }, onCancel: function () { diff --git a/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js index 66ba5158f20..2a7b58f1216 100644 --- a/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js +++ b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js @@ -13,15 +13,20 @@ */ add_task(function* test_select_profile() { // Request an e-mail address. - let data = { "": { "": { "email": null } } }; - let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(data); + let { uiWindow, promiseResult } = yield FormAutofillTest.showUI( + TestData.requestEmailOnly); // Accept the dialog. let acceptButton = uiWindow.document.getElementById("accept"); EventUtils.synthesizeMouseAtCenter(acceptButton, {}, uiWindow); let result = yield promiseResult; - Assert.equal(result.email, "email@example.org"); + Assert.equal(result.fields.length, 1); + Assert.equal(result.fields[0].section, ""); + Assert.equal(result.fields[0].addressType, ""); + Assert.equal(result.fields[0].contactType, ""); + Assert.equal(result.fields[0].fieldName, "email"); + Assert.equal(result.fields[0].value, "email@example.org"); }); /** @@ -29,8 +34,8 @@ add_task(function* test_select_profile() { */ add_task(function* test_cancel() { // Request an e-mail address. - let data = { "": { "": { "email": null } } }; - let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(data); + let { uiWindow, promiseResult } = yield FormAutofillTest.showUI( + TestData.requestEmailOnly); // Cancel the dialog. let acceptButton = uiWindow.document.getElementById("cancel"); diff --git a/toolkit/components/formautofill/test/head_common.js b/toolkit/components/formautofill/test/head_common.js index a00647b3dff..5aa7d830479 100644 --- a/toolkit/components/formautofill/test/head_common.js +++ b/toolkit/components/formautofill/test/head_common.js @@ -193,7 +193,8 @@ let FormAutofillTest = { // Wait for the initialization event before opening the window. let promiseUIWindow = TestUtils.waitForNotification("formautofill-window-initialized"); - let ui = yield FormAutofill.integration.createRequestAutocompleteUI({}); + let ui = yield FormAutofill.integration.createRequestAutocompleteUI( + aFormAutofillData); let promiseResult = ui.show(); // The window is the subject of the observer notification. @@ -204,6 +205,26 @@ let FormAutofillTest = { }), }; +let TestData = { + /** + * Autofill UI request for the e-mail field only. + */ + get requestEmailOnly() { + return { + sections: [{ + name: "", + addressSections: [{ + addressType: "", + fields: [{ + fieldName: "email", + contactType: "", + }], + }], + }], + }; + }, +}; + /* --- Initialization and termination functions common to all tests --- */ add_task(function* test_common_initialize() {