rename from browser/actors/FormValidationChild.jsm rename to browser/actors/FormSubmitChild.jsm --- a/browser/actors/FormValidationChild.jsm +++ b/browser/actors/FormSubmitChild.jsm @@ -1,25 +1,25 @@ /* 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/. */ "use strict"; -/** +/* * Handles the validation callback from nsIFormFillController and * the display of the help panel on invalid elements. */ -var EXPORTED_SYMBOLS = ["FormValidationChild"]; +var EXPORTED_SYMBOLS = ["FormSubmitChild"]; const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm"); const {BrowserUtils} = ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm"); const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -class FormValidationChild extends ActorChild { +class FormSubmitChild extends ActorChild { constructor(dispatcher) { super(dispatcher); this._validationMessage = ""; this._element = null; this.mm.addEventListener("pageshow", this); }
--- a/browser/actors/moz.build +++ b/browser/actors/moz.build @@ -28,17 +28,17 @@ with Files("WebRTCChild.jsm"): FINAL_TARGET_FILES.actors += [ 'AboutReaderChild.jsm', 'BlockedSiteChild.jsm', 'BrowserTabChild.jsm', 'ClickHandlerChild.jsm', 'ContentSearchChild.jsm', 'ContextMenuChild.jsm', 'DOMFullscreenChild.jsm', - 'FormValidationChild.jsm', + 'FormSubmitChild.jsm', 'LightweightThemeChild.jsm', 'LightWeightThemeInstallChild.jsm', 'LinkHandlerChild.jsm', 'NetErrorChild.jsm', 'OfflineAppsChild.jsm', 'PageInfoChild.jsm', 'PageStyleChild.jsm', 'PluginChild.jsm',
--- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -36,22 +36,16 @@ addMessageListener("RemoteLogins:fillFor }); function shouldIgnoreLoginManagerEvent(event) { // If we have a null principal then prevent any more password manager code from running and // incorrectly using the document `location`. return event.target.nodePrincipal.isNullPrincipal; } -addEventListener("DOMFormBeforeSubmit", function(event) { - if (shouldIgnoreLoginManagerEvent(event)) { - return; - } - LoginManagerContent.onDOMFormBeforeSubmit(event); -}); addEventListener("DOMFormHasPassword", function(event) { if (shouldIgnoreLoginManagerEvent(event)) { return; } LoginManagerContent.onDOMFormHasPassword(event); let formLike = LoginFormFactory.createFromForm(event.originalTarget); InsecurePasswordUtils.reportInsecurePasswords(formLike); });
--- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -36,16 +36,20 @@ const whitelist = { // Logging related "resource://gre/modules/Log.jsm", // Session store "resource:///modules/sessionstore/ContentSessionStore.jsm", "resource://gre/modules/sessionstore/SessionHistory.jsm", + // Forms and passwords + "resource://formautofill/FormAutofill.jsm", + "resource://formautofill/FormAutofillContent.jsm", + // Browser front-end "resource:///actors/AboutReaderChild.jsm", "resource:///actors/BrowserTabChild.jsm", "resource:///modules/ContentMetaHandler.jsm", "resource:///actors/LinkHandlerChild.jsm", "resource:///actors/PageStyleChild.jsm", "resource:///actors/SearchTelemetryChild.jsm", "resource://gre/modules/ActorChild.jsm", @@ -79,18 +83,20 @@ const whitelist = { // Extensions "resource://gre/modules/addons/Content.js", ]), processScripts: new Set([ "chrome://global/content/process-content.js", "resource:///modules/ContentObservers.js", "data:,ChromeUtils.import('resource://gre/modules/ExtensionProcessScript.jsm')", + "chrome://satchel/content/formSubmitListener.js", "resource://devtools/client/jsonview/converter-observer.js", "resource://gre/modules/WebRequestContent.js", + "data:,new function() {\n ChromeUtils.import(\"resource://formautofill/FormAutofillContent.jsm\");\n }", ]), }; // Items on this list are allowed to be loaded but not // required, as opposed to items in the main whitelist, // which are all required. const intermittently_loaded_whitelist = { modules: new Set([
--- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -118,19 +118,19 @@ let ACTORS = { }, messages: [ "DOMFullscreen:Entered", "DOMFullscreen:CleanUp", ], }, }, - FormValidation: { + FormSubmit: { child: { - module: "resource:///actors/FormValidationChild.jsm", + module: "resource:///actors/FormSubmitChild.jsm", events: { "MozInvalidForm": {}, }, }, }, LightWeightThemeInstall: { child: {
--- a/browser/extensions/formautofill/FormAutofillContent.jsm +++ b/browser/extensions/formautofill/FormAutofillContent.jsm @@ -238,18 +238,16 @@ let ProfileAutocomplete = { FormAutofill.defineLazyLogGetter(this, "ProfileAutocomplete"); this.debug("ensureRegistered"); this._factory = new AutocompleteFactory(); this._factory.register(AutofillProfileAutoCompleteSearch); this._registered = true; Services.obs.addObserver(this, "autocomplete-will-enter-text"); - - this.debug("ensureRegistered. Finished with _registered:", this._registered); }, ensureUnregistered() { if (!this._registered) { return; } this.debug("ensureUnregistered"); @@ -333,116 +331,123 @@ let ProfileAutocomplete = { }; /** * Handles content's interactions for the process. * * NOTE: Declares it by "var" to make it accessible in unit tests. */ var FormAutofillContent = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIFormSubmitObserver]), /** * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects. */ _formsDetails: new WeakMap(), /** * @type {Set} Set of the fields with usable values in any saved profile. */ - get savedFieldNames() { - return Services.cpmm.sharedData.get("FormAutofill:savedFieldNames"); - }, + savedFieldNames: null, /** * @type {Object} The object where to store the active items, e.g. element, * handler, section, and field detail. */ _activeItems: {}, init() { FormAutofill.defineLazyLogGetter(this, "FormAutofillContent"); - this.debug("init"); - // eslint-disable-next-line mozilla/balanced-listeners - Services.cpmm.sharedData.addEventListener("change", this); + Services.cpmm.addMessageListener("FormAutofill:enabledStatus", this); + Services.cpmm.addMessageListener("FormAutofill:savedFieldNames", this); + Services.obs.addObserver(this, "earlyformsubmit"); - let autofillEnabled = Services.cpmm.sharedData.get("FormAutofill:enabled"); + let autofillEnabled = Services.cpmm.initialProcessData.autofillEnabled; // If storage hasn't be initialized yet autofillEnabled is undefined but we need to ensure // autocomplete is registered before the focusin so register it in this case as long as the // pref is true. let shouldEnableAutofill = autofillEnabled === undefined && (FormAutofill.isAutofillAddressesEnabled || FormAutofill.isAutofillCreditCardsEnabled); if (autofillEnabled || shouldEnableAutofill) { ProfileAutocomplete.ensureRegistered(); } + + this.savedFieldNames = + Services.cpmm.initialProcessData.autofillSavedFieldNames; }, /** * Send the profile to parent for doorhanger and storage saving/updating. * * @param {Object} profile Submitted form's address/creditcard guid and record. * @param {Object} domWin Current content window. * @param {int} timeStartedFillingMS Time of form filling started. */ _onFormSubmit(profile, domWin, timeStartedFillingMS) { let mm = this._messageManagerFromWindow(domWin); mm.sendAsyncMessage("FormAutofill:OnFormSubmit", {profile, timeStartedFillingMS}); }, /** - * Handle a form submission and early return when: + * Handle earlyformsubmit event and early return when: * 1. In private browsing mode. * 2. Could not map any autofill handler by form element. * 3. Number of filled fields is less than autofill threshold * - * @param {HTMLElement} formElement Root element which receives submit event. - * @param {Window} domWin Content window only passed for unit tests + * @param {HTMLElement} formElement Root element which receives earlyformsubmit event. + * @param {Object} domWin Content window + * @returns {boolean} Should always return true so form submission isn't canceled. */ - formSubmitted(formElement, domWin = formElement.ownerGlobal) { - this.debug("Handling form submission"); + notify(formElement, domWin) { + try { + this.debug("Notifying form early submission"); - if (!FormAutofill.isAutofillEnabled) { - this.debug("Form Autofill is disabled"); - return; - } + if (!FormAutofill.isAutofillEnabled) { + this.debug("Form Autofill is disabled"); + return true; + } - // The `domWin` truthiness test is used by unit tests to bypass this check. - if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { - this.debug("Ignoring submission in a private window"); - return; - } + if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { + this.debug("Ignoring submission in a private window"); + return true; + } + + let handler = this._formsDetails.get(formElement); + if (!handler) { + this.debug("Form element could not map to an existing handler"); + return true; + } - let handler = this._formsDetails.get(formElement); - if (!handler) { - this.debug("Form element could not map to an existing handler"); - return; - } + let records = handler.createRecords(); + if (!Object.values(records).some(typeRecords => typeRecords.length)) { + return true; + } - let records = handler.createRecords(); - if (!Object.values(records).some(typeRecords => typeRecords.length)) { - return; + this._onFormSubmit(records, domWin, handler.timeStartedFillingMS); + } catch (ex) { + Cu.reportError(ex); } - - this._onFormSubmit(records, domWin, handler.timeStartedFillingMS); + return true; }, - handleEvent(evt) { - switch (evt.type) { - case "change": { - if (!evt.changedKeys.includes("FormAutofill:enabled")) { - return; - } - if (Services.cpmm.sharedData.get("FormAutofill:enabled")) { + receiveMessage({name, data}) { + switch (name) { + case "FormAutofill:enabledStatus": { + if (data) { ProfileAutocomplete.ensureRegistered(); } else { ProfileAutocomplete.ensureUnregistered(); } break; } + case "FormAutofill:savedFieldNames": { + this.savedFieldNames = data; + } } }, /** * Get the form's handler from cache which is created after page identified. * * @param {HTMLInputElement} element Focused input which triggered profile searching * @returns {Array<Object>|null}
--- a/browser/extensions/formautofill/FormAutofillParent.jsm +++ b/browser/extensions/formautofill/FormAutofillParent.jsm @@ -182,30 +182,30 @@ FormAutofillParent.prototype = { } }, /** * Broadcast the status to frames when the form autofill status changes. */ _onStatusChanged() { log.debug("_onStatusChanged: Status changed to", this._active); - Services.ppmm.sharedData.set("FormAutofill:enabled", this._active); - // Sync autofill enabled to make sure the value is up-to-date + Services.ppmm.broadcastAsyncMessage("FormAutofill:enabledStatus", this._active); + // Sync process data autofillEnabled to make sure the value up to date // no matter when the new content process is initialized. - Services.ppmm.sharedData.flush(); + Services.ppmm.initialProcessData.autofillEnabled = this._active; }, /** * Query preference and storage status to determine the overall status of the * form autofill feature. * * @returns {boolean} whether form autofill is active (enabled and has data) */ _computeStatus() { - const savedFieldNames = Services.ppmm.sharedData.get("FormAutofill:savedFieldNames"); + const savedFieldNames = Services.ppmm.initialProcessData.autofillSavedFieldNames; return (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) || Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) && savedFieldNames && savedFieldNames.size > 0; }, /** @@ -370,30 +370,28 @@ FormAutofillParent.prototype = { } target.sendAsyncMessage("FormAutofill:Records", records); }, _updateSavedFieldNames() { log.debug("_updateSavedFieldNames"); - let savedFieldNames; // Don't access the credit cards store unless it is enabled. if (FormAutofill.isAutofillCreditCardsAvailable) { - savedFieldNames = new Set([ - ...this.formAutofillStorage.addresses.getSavedFieldNames(), - ...this.formAutofillStorage.creditCards.getSavedFieldNames(), - ]); + Services.ppmm.initialProcessData.autofillSavedFieldNames = + new Set([...this.formAutofillStorage.addresses.getSavedFieldNames(), + ...this.formAutofillStorage.creditCards.getSavedFieldNames()]); } else { - savedFieldNames = this.formAutofillStorage.addresses.getSavedFieldNames(); + Services.ppmm.initialProcessData.autofillSavedFieldNames = + this.formAutofillStorage.addresses.getSavedFieldNames(); } - Services.ppmm.sharedData.set("FormAutofill:savedFieldNames", savedFieldNames); - Services.ppmm.sharedData.flush(); - + Services.ppmm.broadcastAsyncMessage("FormAutofill:savedFieldNames", + Services.ppmm.initialProcessData.autofillSavedFieldNames); this._updateStatus(); }, async _onAddressSubmit(address, target, timeStartedFillingMS) { let showDoorhanger = null; if (address.guid) { // Avoid updating the fields that users don't modify. let originalAddress = await this.formAutofillStorage.addresses.get(address.guid);
--- a/browser/extensions/formautofill/api.js +++ b/browser/extensions/formautofill/api.js @@ -122,16 +122,21 @@ this.formautofill = class extends Extens } else { Services.prefs.clearUserPref("services.sync.engine.creditcards.available"); } // Listen for the autocomplete popup message to lazily append our stylesheet related to the popup. Services.mm.addMessageListener("FormAutoComplete:MaybeOpenPopup", onMaybeOpenPopup); formAutofillParent.init().catch(Cu.reportError); + /* eslint-disable no-unused-vars */ + Services.ppmm.loadProcessScript("data:,new " + function() { + ChromeUtils.import("resource://formautofill/FormAutofillContent.jsm"); + }, true); + /* eslint-enable no-unused-vars */ Services.mm.loadFrameScript("chrome://formautofill/content/FormAutofillFrameScript.js", true, true); } onShutdown() { resProto.setSubstitution(RESOURCE_HOST, null); this.chromeHandle.destruct(); this.chromeHandle = null;
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js +++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js @@ -1,32 +1,33 @@ /* 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/. */ -/** +/* * Form Autofill frame script. */ "use strict"; /* eslint-env mozilla/frame-script */ var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {FormAutofillContent} = ChromeUtils.import("resource://formautofill/FormAutofillContent.jsm"); ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm"); ChromeUtils.defineModuleGetter(this, "FormAutofill", "resource://formautofill/FormAutofill.jsm"); -ChromeUtils.defineModuleGetter(this, "FormAutofillContent", - "resource://formautofill/FormAutofillContent.jsm"); ChromeUtils.defineModuleGetter(this, "FormAutofillUtils", "resource://formautofill/FormAutofillUtils.jsm"); /** * Handles content's interactions for the frame. + * + * NOTE: Declares it by "var" to make it accessible in unit tests. */ var FormAutofillFrameScript = { _nextHandleElement: null, _alreadyDOMContentLoaded: false, _hasDOMContentLoadedHandler: false, _hasPendingTask: false, _doIdentifyAutofillFields() { @@ -43,44 +44,26 @@ var FormAutofillFrameScript = { // form has been identified, and ready to open popup. sendAsyncMessage("FormAutofill:FieldsIdentified"); FormAutofillContent.updateActiveInput(); }); }, init() { addEventListener("focusin", this); - addEventListener("DOMFormBeforeSubmit", this); addMessageListener("FormAutofill:PreviewProfile", this); addMessageListener("FormAutofill:ClearForm", this); addMessageListener("FormAutoComplete:PopupClosed", this); addMessageListener("FormAutoComplete:PopupOpened", this); }, handleEvent(evt) { if (!evt.isTrusted || !FormAutofill.isAutofillEnabled) { return; } - - switch (evt.type) { - case "focusin": { - this.onFocusIn(evt); - break; - } - case "DOMFormBeforeSubmit": { - this.onDOMFormBeforeSubmit(evt); - break; - } - default: { - throw new Error("Unexpected event type"); - } - } - }, - - onFocusIn(evt) { FormAutofillContent.updateActiveInput(); let element = evt.target; if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) { return; } this._nextHandleElement = element; @@ -94,30 +77,16 @@ var FormAutofillFrameScript = { return; } this._alreadyDOMContentLoaded = true; } this._doIdentifyAutofillFields(); }, - /** - * Handle the DOMFormBeforeSubmit event. - * @param {Event} evt - */ - onDOMFormBeforeSubmit(evt) { - let formElement = evt.target; - - if (!FormAutofill.isAutofillEnabled) { - return; - } - - FormAutofillContent.formSubmitted(formElement); - }, - receiveMessage(message) { if (!FormAutofill.isAutofillEnabled) { return; } const doc = content.document; const {chromeEventHandler} = doc.ownerGlobal.docShell;
--- a/browser/extensions/formautofill/test/unit/test_activeStatus.js +++ b/browser/extensions/formautofill/test/unit/test_activeStatus.js @@ -11,29 +11,29 @@ add_task(async function setup() { }); add_task(async function test_activeStatus_init() { let formAutofillParent = new FormAutofillParent(); sinon.spy(formAutofillParent, "_updateStatus"); // Default status is null before initialization Assert.equal(formAutofillParent._active, null); - Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), undefined); + Assert.equal(Services.ppmm.initialProcessData.autofillEnabled, undefined); await formAutofillParent.init(); // init shouldn't call updateStatus since that requires storage which will // lead to startup time regressions. Assert.equal(formAutofillParent._updateStatus.called, false); - Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), undefined); + Assert.equal(Services.ppmm.initialProcessData.autofillEnabled, undefined); // Initialize profile storage await formAutofillParent.formAutofillStorage.initialize(); // Upon first initializing profile storage, status should be computed. Assert.equal(formAutofillParent._updateStatus.called, true); - Assert.equal(Services.ppmm.sharedData.get("FormAutofill:enabled"), false); + Assert.equal(Services.ppmm.initialProcessData.autofillEnabled, false); formAutofillParent._uninit(); }); add_task(async function test_activeStatus_observe() { let formAutofillParent = new FormAutofillParent(); sinon.stub(formAutofillParent, "_computeStatus"); sinon.spy(formAutofillParent, "_onStatusChanged");
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js +++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js @@ -486,22 +486,22 @@ const TESTCASES = [ untouchedFields: [], }], creditCard: [], }, }, }, ]; -add_task(async function handle_invalid_form() { +add_task(async function handle_earlyformsubmit_event() { info("Starting testcase: Test an invalid form element"); let fakeForm = MOCK_DOC.createElement("form"); sinon.spy(FormAutofillContent, "_onFormSubmit"); - FormAutofillContent.formSubmitted(fakeForm, null); + Assert.equal(FormAutofillContent.notify(fakeForm), true); Assert.equal(FormAutofillContent._onFormSubmit.called, false); FormAutofillContent._onFormSubmit.restore(); }); add_task(async function autofill_disabled() { let form = MOCK_DOC.getElementById("form1"); form.reset(); @@ -520,41 +520,41 @@ add_task(async function autofill_disable FormAutofillContent.identifyAutofillFields(element); sinon.stub(FormAutofillContent, "_onFormSubmit"); // "_onFormSubmit" shouldn't be called if both "addresses" and "creditCards" // are disabled. Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", false); Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false); - FormAutofillContent.formSubmitted(form, null); + FormAutofillContent.notify(form); Assert.equal(FormAutofillContent._onFormSubmit.called, false); FormAutofillContent._onFormSubmit.reset(); // "_onFormSubmit" should be called as usual. Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); - FormAutofillContent.formSubmitted(form, null); + FormAutofillContent.notify(form); Assert.equal(FormAutofillContent._onFormSubmit.called, true); Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].creditCard, []); FormAutofillContent._onFormSubmit.reset(); // "address" should be empty if "addresses" pref is disabled. Services.prefs.setBoolPref("extensions.formautofill.addresses.enabled", false); - FormAutofillContent.formSubmitted(form, null); + FormAutofillContent.notify(form); Assert.equal(FormAutofillContent._onFormSubmit.called, true); Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].creditCard, []); FormAutofillContent._onFormSubmit.reset(); Services.prefs.clearUserPref("extensions.formautofill.addresses.enabled"); // "creditCard" should be empty if "creditCards" pref is disabled. Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false); - FormAutofillContent.formSubmitted(form, null); + FormAutofillContent.notify(form); Assert.deepEqual(FormAutofillContent._onFormSubmit.called, true); Assert.notDeepEqual(FormAutofillContent._onFormSubmit.args[0][0].address, []); Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0].creditCard, []); FormAutofillContent._onFormSubmit.reset(); Services.prefs.clearUserPref("extensions.formautofill.creditCards.enabled"); FormAutofillContent._onFormSubmit.restore(); }); @@ -577,17 +577,17 @@ TESTCASES.forEach(testcase => { } else { input.value = testcase.formValue[key]; } } sinon.stub(FormAutofillContent, "_onFormSubmit"); let element = MOCK_DOC.getElementById(TARGET_ELEMENT_ID); FormAutofillContent.identifyAutofillFields(element); - FormAutofillContent.formSubmitted(form, null); + FormAutofillContent.notify(form); Assert.equal(FormAutofillContent._onFormSubmit.called, testcase.expectedResult.formSubmission, "Check expected onFormSubmit.called"); if (FormAutofillContent._onFormSubmit.called) { Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0], testcase.expectedResult.records); }
--- a/browser/extensions/formautofill/test/unit/test_savedFieldNames.js +++ b/browser/extensions/formautofill/test/unit/test_savedFieldNames.js @@ -1,10 +1,10 @@ /* - * Test for keeping the valid fields information in sharedData. + * Test for keeping the valid fields information in initialProcessData. */ "use strict"; let FormAutofillParent; add_task(async function setup() { ({FormAutofillParent} = ChromeUtils.import("resource://formautofill/FormAutofillParent.jsm", null)); @@ -49,17 +49,17 @@ add_task(async function test_profileSave Object.defineProperty( formAutofillParent.formAutofillStorage.addresses, "_data", {writable: true}); formAutofillParent.formAutofillStorage.addresses._data = []; // The set is empty if there's no profile in the store. formAutofillParent._updateSavedFieldNames(); - Assert.equal(Services.ppmm.sharedData.get("FormAutofill:savedFieldNames").size, 0); + Assert.equal(Services.ppmm.initialProcessData.autofillSavedFieldNames.size, 0); // 2 profiles with 4 valid fields. formAutofillParent.formAutofillStorage.addresses._data = [{ guid: "test-guid-1", organization: "Sesame Street", "street-address": "123 Sesame Street.", tel: "1-345-345-3456", email: "", @@ -76,17 +76,17 @@ add_task(async function test_profileSave timeCreated: 0, timeLastUsed: 0, timeLastModified: 0, timesUsed: 0, }]; formAutofillParent._updateSavedFieldNames(); - let autofillSavedFieldNames = Services.ppmm.sharedData.get("FormAutofill:savedFieldNames"); + let autofillSavedFieldNames = Services.ppmm.initialProcessData.autofillSavedFieldNames; Assert.equal(autofillSavedFieldNames.size, 4); Assert.equal(autofillSavedFieldNames.has("organization"), true); Assert.equal(autofillSavedFieldNames.has("street-address"), true); Assert.equal(autofillSavedFieldNames.has("tel"), true); Assert.equal(autofillSavedFieldNames.has("email"), false); Assert.equal(autofillSavedFieldNames.has("guid"), false); Assert.equal(autofillSavedFieldNames.has("timeCreated"), false); Assert.equal(autofillSavedFieldNames.has("timeLastUsed"), false);
--- a/dom/html/HTMLFormElement.cpp +++ b/dom/html/HTMLFormElement.cpp @@ -87,16 +87,19 @@ static const uint8_t NS_FORM_AUTOCOMPLET static const nsAttrValue::EnumTable kFormAutocompleteTable[] = { {"on", NS_FORM_AUTOCOMPLETE_ON}, {"off", NS_FORM_AUTOCOMPLETE_OFF}, {nullptr, 0}}; // Default autocomplete value is 'on'. static const nsAttrValue::EnumTable* kFormDefaultAutocomplete = &kFormAutocompleteTable[0]; +bool HTMLFormElement::gFirstFormSubmitted = false; +bool HTMLFormElement::gPasswordManagerInitialized = false; + HTMLFormElement::HTMLFormElement( already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) : nsGenericHTMLElement(std::move(aNodeInfo)), mControls(new HTMLFormControlsCollection(this)), mSelectedRadioButtons(2), mRequiredRadioButtonCounts(2), mValueMissingRadioGroups(2), mPendingSubmission(nullptr), @@ -845,36 +848,72 @@ nsresult HTMLFormElement::DoSecureToInse telemetryBucket + 1); } return NS_OK; } nsresult HTMLFormElement::NotifySubmitObservers(nsIURI* aActionURL, bool* aCancelSubmit, bool aEarlyNotify) { + // If this is the first form, bring alive the first form submit + // category observers + if (!gFirstFormSubmitted) { + gFirstFormSubmitted = true; + NS_CreateServicesFromCategory(NS_FIRST_FORMSUBMIT_CATEGORY, nullptr, + NS_FIRST_FORMSUBMIT_CATEGORY); + } + if (!aEarlyNotify) { nsresult rv = DoSecureToInsecureSubmitCheck(aActionURL, aCancelSubmit); if (NS_FAILED(rv)) { return rv; } if (*aCancelSubmit) { return NS_OK; } } - bool defaultAction = true; - nsresult rv = nsContentUtils::DispatchEventOnlyToChrome( - OwnerDoc(), static_cast<nsINode*>(this), - aEarlyNotify ? NS_LITERAL_STRING("DOMFormBeforeSubmit") - : NS_LITERAL_STRING("DOMFormSubmit"), - CanBubble::eYes, Cancelable::eYes, &defaultAction); - *aCancelSubmit = !defaultAction; - if (*aCancelSubmit) { - return NS_OK; + // Notify observers that the form is being submitted. + nsCOMPtr<nsIObserverService> service = + mozilla::services::GetObserverService(); + if (!service) return NS_ERROR_FAILURE; + + nsCOMPtr<nsISimpleEnumerator> theEnum; + nsresult rv = service->EnumerateObservers( + aEarlyNotify ? NS_EARLYFORMSUBMIT_SUBJECT : NS_FORMSUBMIT_SUBJECT, + getter_AddRefs(theEnum)); + NS_ENSURE_SUCCESS(rv, rv); + + if (theEnum) { + nsCOMPtr<nsISupports> inst; + *aCancelSubmit = false; + + // XXXbz what do the submit observers actually want? The window + // of the document this is shown in? Or something else? + // sXBL/XBL2 issue + nsCOMPtr<nsPIDOMWindowOuter> window = OwnerDoc()->GetWindow(); + + bool loop = true; + while (NS_SUCCEEDED(theEnum->HasMoreElements(&loop)) && loop) { + theEnum->GetNext(getter_AddRefs(inst)); + + nsCOMPtr<nsIFormSubmitObserver> formSubmitObserver( + do_QueryInterface(inst)); + if (formSubmitObserver) { + rv = formSubmitObserver->Notify( + this, window ? window->GetCurrentInnerWindow() : nullptr, + aActionURL, aCancelSubmit); + NS_ENSURE_SUCCESS(rv, rv); + } + if (*aCancelSubmit) { + return NS_OK; + } + } } + return rv; } nsresult HTMLFormElement::WalkFormElements( HTMLFormSubmission* aFormSubmission) { // This shouldn't be called recursively, so use a rather large value // for the preallocated buffer. AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls; @@ -1080,18 +1119,26 @@ nsresult HTMLFormElement::AddElement(nsG bool lastElement = AddElementToList(controlList, aChild, this); #ifdef DEBUG AssertDocumentOrder(controlList, this); #endif int32_t type = aChild->ControlType(); - // If it is a password control, inform the password manager. + // + // If it is a password control, and the password manager has not yet been + // initialized, initialize the password manager + // if (type == NS_FORM_INPUT_PASSWORD) { + if (!gPasswordManagerInitialized) { + gPasswordManagerInitialized = true; + NS_CreateServicesFromCategory(NS_PASSWORDMANAGER_CATEGORY, nullptr, + NS_PASSWORDMANAGER_CATEGORY); + } PostPasswordEvent(); } // Default submit element handling if (aChild->IsSubmitControl()) { // Update mDefaultSubmitElement, mFirstSubmitInElements, // mFirstSubmitNotInElements.
--- a/dom/html/HTMLFormElement.h +++ b/dom/html/HTMLFormElement.h @@ -597,16 +597,22 @@ class HTMLFormElement final : public nsG /** Keep track of whether a submission was user-initiated or not */ bool mSubmitInitiatedFromUserInput; /** * Whether the submission of this form has been ever prevented because of * being invalid. */ bool mEverTriedInvalidSubmit; + protected: + /** Detection of first form to notify observers */ + static bool gFirstFormSubmitted; + /** Detection of first password input to initialize the password manager */ + static bool gPasswordManagerInitialized; + private: ~HTMLFormElement(); }; } // namespace dom } // namespace mozilla
--- a/dom/html/nsIFormSubmitObserver.idl +++ b/dom/html/nsIFormSubmitObserver.idl @@ -11,15 +11,21 @@ interface nsIURI; interface nsIArray; webidl HTMLFormElement; webidl Element; [scriptable, uuid(867cb7e7-835d-408b-9788-d2834d284e03)] interface nsIFormSubmitObserver: nsISupports { + void notify(in HTMLFormElement formNode, in mozIDOMWindow window, in nsIURI actionURL, out boolean cancelSubmit); + void notifyInvalidSubmit(in HTMLFormElement formNode, in Array<Element> invalidElements); }; %{C++ +#define NS_FORMSUBMIT_SUBJECT "formsubmit" +#define NS_EARLYFORMSUBMIT_SUBJECT "earlyformsubmit" +#define NS_FIRST_FORMSUBMIT_CATEGORY "firstformsubmit" +#define NS_PASSWORDMANAGER_CATEGORY "passwordmanager" #define NS_INVALIDFORMSUBMIT_SUBJECT "invalidformsubmit" %}
--- a/mobile/android/components/BrowserCLH.js +++ b/mobile/android/components/BrowserCLH.js @@ -221,22 +221,16 @@ BrowserCLH.prototype = { let options = { capture: true, mozSystemGroup: true, }; // NOTE: Much of this logic is duplicated in browser/base/content/content.js // for desktop. - aWindow.addEventListener("DOMFormBeforeSubmit", event => { - if (shouldIgnoreLoginManagerEvent(event)) { - return; - } - this.LoginManagerContent.onDOMFormBeforeSubmit(event); - }); aWindow.addEventListener("DOMFormHasPassword", event => { if (shouldIgnoreLoginManagerEvent(event)) { return; } this.LoginManagerContent.onDOMFormHasPassword(event); }, options); aWindow.addEventListener("DOMInputPasswordAdded", event => {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm @@ -46,19 +46,37 @@ XPCOMUtils.defineLazyGetter(this, "log", Services.cpmm.addMessageListener("clearRecipeCache", () => { LoginRecipesContent._clearRecipeCache(); }); var gLastRightClickTimeStamp = Number.NEGATIVE_INFINITY; var observer = { QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), + // nsIFormSubmitObserver + notify(formElement, aWindow, actionURI) { + log("observer notified for form submission."); + + // We're invoked before the content's |onsubmit| handlers, so we + // can grab form data before it might be modified (see bug 257781). + + try { + let formLike = LoginFormFactory.createFromForm(formElement); + LoginManagerContent._onFormSubmit(formLike); + } catch (e) { + log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message); + Cu.reportError(e); + } + + return true; // Always return true, or form submit will be canceled. + }, // nsIWebProgressListener onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { // Only handle pushState/replaceState here. if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) || !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) { return; } @@ -122,16 +140,17 @@ var observer = { default: { throw new Error("Unexpected event"); } } }, }; +Services.obs.addObserver(observer, "earlyformsubmit"); // This object maps to the "child" process (even in the single-process case). var LoginManagerContent = { __formFillService: null, // FormFillController, for username autocompleting get _formFillService() { if (!this.__formFillService) { this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"]. @@ -336,28 +355,16 @@ var LoginManagerContent = { webProgress.addProgressListener(observer, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | Ci.nsIWebProgress.NOTIFY_LOCATION); } catch (ex) { // Ignore NS_ERROR_FAILURE if the progress listener was already added } }, - onDOMFormBeforeSubmit(event) { - if (!event.isTrusted) { - return; - } - - // We're invoked before the content's |submit| event handlers, so we - // can grab form data before it might be modified (see bug 257781). - log("notified before form submission"); - let formLike = LoginFormFactory.createFromForm(event.target); - LoginManagerContent._onFormSubmit(formLike); - }, - onDOMFormHasPassword(event) { if (!event.isTrusted) { return; } let form = event.target; let formLike = LoginFormFactory.createFromForm(form); log("onDOMFormHasPassword:", form, formLike); @@ -891,17 +898,17 @@ var LoginManagerContent = { for (let formRoot of weakLoginFormRootElements) { if (!formRoot.isConnected) { continue; } if (ChromeUtils.getClassName(formRoot) === "HTMLFormElement") { // For now only perform capture upon navigation for FormLike's without - // a <form> to avoid capture from both a DOMFormBeforeSubmit event and + // a <form> to avoid capture from both an earlyformsubmit and // navigation for the same "form". log("Ignoring navigation for the form root to avoid multiple prompts " + "since it was for a real <form>"); continue; } let formLike = this._formLikeByRootElement.get(formRoot); this._onFormSubmit(formLike); }
--- a/toolkit/components/satchel/FormHistoryStartup.jsm +++ b/toolkit/components/satchel/FormHistoryStartup.jsm @@ -42,17 +42,18 @@ FormHistoryStartup.prototype = { this.inited = true; Services.prefs.addObserver("browser.formfill.", this, true); // triggers needed service cleanup and db shutdown Services.obs.addObserver(this, "idle-daily", true); Services.obs.addObserver(this, "formhistory-expire-now", true); - Services.mm.addMessageListener("FormHistory:FormSubmitEntries", this); + Services.ppmm.loadProcessScript("chrome://satchel/content/formSubmitListener.js", true); + Services.ppmm.addMessageListener("FormHistory:FormSubmitEntries", this); // For each of these messages, we could receive them from content, // or we might receive them from the ppmm if the searchbar is // having its history queried. for (let manager of [Services.mm, Services.ppmm]) { manager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this); manager.addMessageListener("FormHistory:RemoveEntry", this); }
rename from toolkit/components/satchel/FormSubmitChild.jsm rename to toolkit/components/satchel/formSubmitListener.js --- a/toolkit/components/satchel/FormSubmitChild.jsm +++ b/toolkit/components/satchel/formSubmitListener.js @@ -1,152 +1,150 @@ /* 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/. */ -"use strict"; - -var EXPORTED_SYMBOLS = ["FormSubmitChild"]; - -const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm"); -const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +/* eslint-env mozilla/frame-script */ ChromeUtils.defineModuleGetter(this, "CreditCard", "resource://gre/modules/CreditCard.jsm"); ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -class FormSubmitChild extends ActorChild { - constructor(dispatcher) { - super(dispatcher); +(function() { +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); - this.QueryInterface = ChromeUtils.generateQI([ - Ci.nsIObserver, - Ci.nsISupportsWeakReference, - ]); +let satchelFormListener = { + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIFormSubmitObserver, + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]), + debug: true, + enabled: true, + + init() { + Services.obs.addObserver(this, "earlyformsubmit"); + Services.obs.addObserver(this, "xpcom-shutdown"); Services.prefs.addObserver("browser.formfill.", this); this.updatePrefs(); - } - - cleanup() { - super.cleanup(); - Services.prefs.removeObserver("browser.formfill.", this); - } + }, updatePrefs() { this.debug = Services.prefs.getBoolPref("browser.formfill.debug"); this.enabled = Services.prefs.getBoolPref("browser.formfill.enable"); - } + }, log(message) { if (!this.debug) { return; } dump("satchelFormListener: " + message + "\n"); Services.console.logStringMessage("satchelFormListener: " + message); - } + }, /* ---- nsIObserver interface ---- */ observe(subject, topic, data) { if (topic == "nsPref:changed") { this.updatePrefs(); + } else if (topic == "xpcom-shutdown") { + Services.obs.removeObserver(this, "earlyformsubmit"); + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.prefs.removeObserver("browser.formfill.", this); } else { this.log("Oops! Unexpected notification: " + topic); } - } + }, - handleEvent(event) { - switch (event.type) { - case "DOMFormBeforeSubmit": { - this.onDOMFormBeforeSubmit(event); - break; - } - default: { - throw new Error("Unexpected event"); - } - } - } + /* ---- nsIFormSubmitObserver interfaces ---- */ - onDOMFormBeforeSubmit(event) { - let form = event.target; - if (!this.enabled || PrivateBrowsingUtils.isContentWindowPrivate(form.ownerGlobal)) { - return; - } - - this.log("Form submit observer notified."); - - if (form.hasAttribute("autocomplete") && - form.getAttribute("autocomplete").toLowerCase() == "off") { - return; - } - - let entries = []; - for (let input of form.elements) { - if (ChromeUtils.getClassName(input) !== "HTMLInputElement") { - continue; + notify(form, domWin, actionURI, cancelSubmit) { + try { + if (!this.enabled || PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { + return; } - // Only use inputs that hold text values (not including type="password") - if (!input.mozIsTextField(true)) { - continue; - } + this.log("Form submit observer notified."); - // Don't save fields that were previously type=password such as on sites - // that allow the user to toggle password visibility. - if (input.hasBeenTypePassword) { - continue; + if (form.hasAttribute("autocomplete") && + form.getAttribute("autocomplete").toLowerCase() == "off") { + return; } - // Bug 394612: If Login Manager marked this input, don't save it. - // The login manager will deal with remembering it. + let entries = []; + for (let i = 0; i < form.elements.length; i++) { + let input = form.elements[i]; + if (ChromeUtils.getClassName(input) !== "HTMLInputElement") { + continue; + } + + // Only use inputs that hold text values (not including type="password") + if (!input.mozIsTextField(true)) { + continue; + } - // Don't save values when @autocomplete is "off" or has a sensitive field name. - let autocompleteInfo = input.getAutocompleteInfo(); - if (autocompleteInfo && !autocompleteInfo.canAutomaticallyPersist) { - continue; - } + // Don't save fields that were previously type=password such as on sites + // that allow the user to toggle password visibility. + if (input.hasBeenTypePassword) { + continue; + } - let value = input.value.trim(); + // Bug 394612: If Login Manager marked this input, don't save it. + // The login manager will deal with remembering it. + + // Don't save values when @autocomplete is "off" or has a sensitive field name. + let autocompleteInfo = input.getAutocompleteInfo(); + if (autocompleteInfo && !autocompleteInfo.canAutomaticallyPersist) { + continue; + } + + let value = input.value.trim(); - // Don't save empty or unchanged values. - if (!value || value == input.defaultValue.trim()) { - continue; - } + // Don't save empty or unchanged values. + if (!value || value == input.defaultValue.trim()) { + continue; + } + + // Don't save credit card numbers. + if (CreditCard.isValidNumber(value)) { + this.log("skipping saving a credit card number"); + continue; + } + + let name = input.name || input.id; + if (!name) { + continue; + } - // Don't save credit card numbers. - if (CreditCard.isValidNumber(value)) { - this.log("skipping saving a credit card number"); - continue; - } + if (name == "searchbar-history") { + this.log('addEntry for input name "' + name + '" is denied'); + continue; + } - let name = input.name || input.id; - if (!name) { - continue; + // Limit stored data to 200 characters. + if (name.length > 200 || value.length > 200) { + this.log("skipping input that has a name/value too large"); + continue; + } + + // Limit number of fields stored per form. + if (entries.length >= 100) { + this.log("not saving any more entries for this form."); + break; + } + + entries.push({ name, value }); } - if (name == "searchbar-history") { - this.log('addEntry for input name "' + name + '" is denied'); - continue; + if (entries.length) { + this.log("sending entries to parent process for form " + form.id); + sendAsyncMessage("FormHistory:FormSubmitEntries", entries); } - - // Limit stored data to 200 characters. - if (name.length > 200 || value.length > 200) { - this.log("skipping input that has a name/value too large"); - continue; - } + } catch (e) { + this.log("notify failed: " + e); + } + }, +}; - // Limit number of fields stored per form. - if (entries.length >= 100) { - this.log("not saving any more entries for this form."); - break; - } - - entries.push({ name, value }); - } - - if (entries.length) { - this.log("sending entries to parent process for form " + form.id); - this.sendAsyncMessage("FormHistory:FormSubmitEntries", entries); - } - } -} +satchelFormListener.init(); +})();
new file mode 100644 --- /dev/null +++ b/toolkit/components/satchel/jar.mn @@ -0,0 +1,7 @@ +# 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/. + +toolkit.jar: +% content satchel %content/satchel/ + content/satchel/formSubmitListener.js
--- a/toolkit/components/satchel/moz.build +++ b/toolkit/components/satchel/moz.build @@ -35,13 +35,11 @@ EXTRA_JS_MODULES += [ 'InputListAutoComplete.jsm', 'nsFormAutoCompleteResult.jsm', ] XPCOM_MANIFESTS += [ 'components.conf', ] -FINAL_TARGET_FILES.actors += [ - 'FormSubmitChild.jsm', -] +FINAL_LIBRARY = 'xul' -FINAL_LIBRARY = 'xul' +JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html +++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html @@ -26,22 +26,20 @@ function runTests() { SpecialPowers.getFormFillController() .QueryInterface(Ci.nsIAutoCompleteInput); var originalFormFillTimeout = formFillController.timeout; SpecialPowers.attachFormFillControllerTo(window); var target = document.getElementById("input"); // Register a word to the form history. - let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { - let {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm"); - FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); - }); - chromeScript.destroy(); target.focus(); + target.value = "Mozilla"; + synthesizeKey("KEY_Enter"); + target.value = ""; new nsDoTestsForAutoCompleteWithComposition( "Testing on HTML input (asynchronously search)", window, target, formFillController.controller, is, function() { return target.value; }, function() { target.setAttribute("timeout", 0); new nsDoTestsForAutoCompleteWithComposition(
--- a/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html +++ b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html @@ -18,31 +18,26 @@ </div> <pre id="test"> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); async function registerWord(aTarget, aAutoCompleteController) { // Register a word to the form history. - let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { - let {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm"); - FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); - }); aTarget.focus(); aTarget.value = "Mozilla"; - + synthesizeKey("KEY_Enter"); await waitForCondition(() => { if (aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_NONE || aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_COMPLETE_NO_MATCH) { aAutoCompleteController.startSearch("Mozilla"); } return aAutoCompleteController.matchCount > 0; }); - chromeScript.destroy(); aTarget.value = ""; synthesizeKey("KEY_Escape"); } async function runTests() { var formFillController = SpecialPowers.getFormFillController() .QueryInterface(Ci.nsIAutoCompleteInput);
--- a/toolkit/modules/ActorManagerParent.jsm +++ b/toolkit/modules/ActorManagerParent.jsm @@ -181,26 +181,16 @@ let ACTORS = { child: { module: "resource://gre/actors/FinderChild.jsm", messages: [ "Finder:Initialize", ], }, }, - FormSubmit: { - child: { - module: "resource://gre/actors/FormSubmitChild.jsm", - allFrames: true, - events: { - "DOMFormBeforeSubmit": {}, - }, - }, - }, - KeyPressEventModelChecker: { child: { module: "resource://gre/actors/KeyPressEventModelCheckerChild.jsm", events: { "CheckKeyPressEventModel": {capture: true, mozSystemGroup: true}, }, }, },
--- a/toolkit/modules/addons/WebNavigationContent.js +++ b/toolkit/modules/addons/WebNavigationContent.js @@ -77,28 +77,35 @@ var CreatedNavigationTargetListener = { sourceFrameId, createdOuterWindowId, isSourceTab, }); }, }; var FormSubmitListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, + Ci.nsISupportsWeakReference]), init() { this.formSubmitWindows = new WeakSet(); - addEventListener("DOMFormBeforeSubmit", this); + Services.obs.addObserver(FormSubmitListener, "earlyformsubmit"); }, uninit() { - removeEventListener("DOMFormBeforeSubmit", this); + Services.obs.removeObserver(FormSubmitListener, "earlyformsubmit"); this.formSubmitWindows = new WeakSet(); }, - handleEvent({target: form}) { - this.formSubmitWindows.add(form.ownerGlobal); + notify: function(form, window, actionURI) { + try { + this.formSubmitWindows.add(window); + } catch (e) { + Cu.reportError("Error in FormSubmitListener.notify"); + } }, hasAndForget: function(window) { let has = this.formSubmitWindows.has(window); this.formSubmitWindows.delete(window); return has; }, };