diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index dc8166eccb77..6fb471088f73 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -126,6 +126,10 @@ var whitelist = [ {file: "resource://app/modules/translation/GoogleTranslator.jsm"}, {file: "resource://app/modules/translation/YandexTranslator.jsm"}, + // Used in Firefox Monitor, which is an extension - we don't check + // files inside the XPI. + {file: "resource://app/modules/EveryWindow.jsm"}, + // Starting from here, files in the whitelist are bugs that need fixing. // Bug 1339424 (wontfix?) {file: "chrome://browser/locale/taskbar.properties", diff --git a/browser/extensions/fxmonitor/moz.build b/browser/extensions/fxmonitor/moz.build index 7b8476e89a76..148852733ec6 100644 --- a/browser/extensions/fxmonitor/moz.build +++ b/browser/extensions/fxmonitor/moz.build @@ -27,7 +27,6 @@ FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged'] += [ ] FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged']['subscripts'] += [ - 'privileged/subscripts/EveryWindow.jsm', 'privileged/subscripts/Globals.jsm' ] diff --git a/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm index 09f8d771d239..c1ef5b43e254 100644 --- a/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm +++ b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm @@ -98,13 +98,10 @@ this.FirefoxMonitor = { this._delayedInited = true; - /* globals Preferences, RemoteSettings, fetch, btoa, XUL_NS */ + /* globals EveryWindow, Preferences, RemoteSettings, fetch, btoa, XUL_NS */ Services.scriptloader.loadSubScript( this.getURL("privileged/subscripts/Globals.jsm")); - /* globals EveryWindow */ - Services.scriptloader.loadSubScript( - this.getURL("privileged/subscripts/EveryWindow.jsm")); // Expire our telemetry on November 1, at which time // we should redo data-review. diff --git a/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm b/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm deleted file mode 100644 index b979bf38250d..000000000000 --- a/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm +++ /dev/null @@ -1,62 +0,0 @@ -/* 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/. */ - -/* globals Services */ - -this.EveryWindow = { - _callbacks: new Map(), - _initialized: false, - - registerCallback: function EW_registerCallback(id, init, uninit) { - if (this._callbacks.has(id)) { - return; - } - - this._callForEveryWindow(init); - this._callbacks.set(id, {id, init, uninit}); - - if (!this._initialized) { - Services.obs.addObserver(this._onOpenWindow.bind(this), - "browser-delayed-startup-finished"); - this._initialized = true; - } - }, - - unregisterCallback: function EW_unregisterCallback(aId, aCallUninit = true) { - if (!this._callbacks.has(aId)) { - return; - } - - if (aCallUninit) { - this._callForEveryWindow(this._callbacks.get(aId).uninit); - } - - this._callbacks.delete(aId); - }, - - _callForEveryWindow(aFunction) { - let windowList = Services.wm.getEnumerator("navigator:browser"); - while (windowList.hasMoreElements()) { - let win = windowList.getNext(); - win.delayedStartupPromise.then(() => { aFunction(win); }); - } - }, - - _onOpenWindow(aWindow) { - for (let c of this._callbacks.values()) { - c.init(aWindow); - } - - aWindow.addEventListener("unload", - this._onWindowClosing.bind(this), - { once: true }); - }, - - _onWindowClosing(aEvent) { - let win = aEvent.target; - for (let c of this._callbacks.values()) { - c.uninit(win, true); - } - }, -}; diff --git a/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm b/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm index 709ae38aaa5d..fea9ce0ae78c 100644 --- a/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm +++ b/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm @@ -10,6 +10,8 @@ ChromeUtils.defineModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); ChromeUtils.defineModuleGetter(this, "RemoteSettings", "resource://services-settings/remote-settings.js"); +ChromeUtils.defineModuleGetter(this, "EveryWindow", + "resource:///modules/EveryWindow.jsm"); const {setTimeout, clearTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm", {}); Cu.importGlobalProperties(["fetch", "btoa"]); diff --git a/browser/modules/EveryWindow.jsm b/browser/modules/EveryWindow.jsm new file mode 100644 index 000000000000..db8e4224d046 --- /dev/null +++ b/browser/modules/EveryWindow.jsm @@ -0,0 +1,109 @@ +/* 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 = ["EveryWindow"]; + +/* + * This module enables consumers to register callbacks on every + * current and future browser window. + * + * Usage: EveryWindow.registerCallback(id, init, uninit); + * EveryWindow.unregisterCallback(id); + * + * id is expected to be a unique value that identifies the + * consumer, to be used for unregistration. If the id is already + * in use, registerCallback returns false without doing anything. + * + * Each callback will receive the window for which it is presently + * being called as the first argument. + * + * init is called on every existing window at the time of registration, + * and on all future windows at browser-delayed-startup-finished. + * + * uninit is called on every existing window if requested at the time + * of unregistration, and at the time of domwindowclosed. + * If the window is closing, a second argument is passed with value `true`. + */ + +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var initialized = false; +var callbacks = new Map(); + +function callForEveryWindow(callback) { + let windowList = Services.wm.getEnumerator("navigator:browser"); + for (let win of windowList) { + win.delayedStartupPromise.then(() => { callback(win); }); + } +} + +this.EveryWindow = { + /** + * Registers init and uninit functions to be called on every window. + * + * @param {string} id A unique identifier for the consumer, to be + * used for unregistration. + * @param {function} init The function to be called on every currently + * existing window and every future window after delayed startup. + * @param {function} uninit The function to be called on every window + * at the time of callback unregistration or after domwindowclosed. + * @returns {boolean} Returns false if the id was taken, else true. + */ + registerCallback: function EW_registerCallback(id, init, uninit) { + if (callbacks.has(id)) { + return false; + } + + if (!initialized) { + let addUnloadListener = (win) => { + function observer(subject, topic, data) { + if (topic == "domwindowclosed" && subject === win) { + Services.ww.unregisterNotification(observer); + for (let c of callbacks.values()) { + c.uninit(win, true); + } + } + } + Services.ww.registerNotification(observer); + }; + + Services.obs.addObserver(win => { + for (let c of callbacks.values()) { + c.init(win); + } + addUnloadListener(win); + }, "browser-delayed-startup-finished"); + + callForEveryWindow(addUnloadListener); + + initialized = true; + } + + callForEveryWindow(init); + callbacks.set(id, {id, init, uninit}); + + return true; + }, + + /** + * Unregisters a previously registered consumer. + * + * @param {string} id The id to unregister. + * @param {boolean} [callUninit=true] Whether to call the registered uninit + * function on every window. + */ + unregisterCallback: function EW_unregisterCallback(id, callUninit = true) { + if (!callbacks.has(id)) { + return; + } + + if (callUninit) { + callForEveryWindow(callbacks.get(id).uninit); + } + + callbacks.delete(id); + }, +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 2472fa4daf7d..284b84ba3789 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -61,6 +61,9 @@ with Files("ContentCrashHandlers.jsm"): with Files("ContentSearch.jsm"): BUG_COMPONENT = ("Firefox", "Search") +with Files("EveryWindow.jsm"): + BUG_COMPONENT = ("Firefox", "General") + with Files("ExtensionsUI.jsm"): BUG_COMPONENT = ("WebExtensions", "General") @@ -136,6 +139,7 @@ EXTRA_JS_MODULES += [ 'ContentObservers.js', 'ContentSearch.jsm', 'Discovery.jsm', + 'EveryWindow.jsm', 'ExtensionsUI.jsm', 'FaviconLoader.jsm', 'FormValidationHandler.jsm', diff --git a/browser/modules/test/browser/browser.ini b/browser/modules/test/browser/browser.ini index 786b5dde0d53..f4fb6ef55aca 100644 --- a/browser/modules/test/browser/browser.ini +++ b/browser/modules/test/browser/browser.ini @@ -14,6 +14,7 @@ support-files = !/browser/components/search/test/browser/head.js !/browser/components/search/test/browser/testEngine.xml testEngine_chromeicon.xml +[browser_EveryWindow.js] [browser_LiveBookmarkMigrator.js] [browser_PageActions.js] [browser_PermissionUI.js] diff --git a/browser/modules/test/browser/browser_EveryWindow.js b/browser/modules/test/browser/browser_EveryWindow.js new file mode 100644 index 000000000000..cfe9f1010c5d --- /dev/null +++ b/browser/modules/test/browser/browser_EveryWindow.js @@ -0,0 +1,129 @@ +/* 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"; + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +const {EveryWindow} = ChromeUtils.import("resource:///modules/EveryWindow.jsm"); + +async function windowInited(aId, aWin) { + // TestUtils.topicObserved returns [subject, data]. We return the + // subject, which in this case is the window. + return (await TestUtils.topicObserved(`${aId}:init`, (win) => { + return aWin ? win == aWin : true; + }))[0]; +} + +function windowUninited(aId, aWin, aClosing) { + return TestUtils.topicObserved(`${aId}:uninit`, (win, closing) => { + if (aWin && aWin != win) { + return false; + } + if (!aWin) { + return true; + } + if (!!aClosing != !!closing) { + return false; + } + return true; + }); +} + +function registerEWCallback(id) { + EveryWindow.registerCallback( + id, + (win) => { + Services.obs.notifyObservers(win, `${id}:init`); + }, + (win, closing) => { + Services.obs.notifyObservers(win, `${id}:uninit`, closing); + }, + ); +} + +function unregisterEWCallback(id, aCallUninit) { + EveryWindow.unregisterCallback(id, aCallUninit); +} + +add_task(async function test_stuff() { + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + let win3 = await BrowserTestUtils.openNewBrowserWindow(); + + let callbackId1 = "EveryWindow:test:1"; + let callbackId2 = "EveryWindow:test:2"; + + let initPromise = Promise.all([windowInited(callbackId1, window), + windowInited(callbackId1, win2), + windowInited(callbackId1, win3), + windowInited(callbackId2, window), + windowInited(callbackId2, win2), + windowInited(callbackId2, win3)]); + + registerEWCallback(callbackId1); + registerEWCallback(callbackId2); + + await initPromise; + ok(true, "Init called for all existing windows for all registered consumers"); + + let uninitPromise = Promise.all([windowUninited(callbackId1, window, false), + windowUninited(callbackId1, win2, false), + windowUninited(callbackId1, win3, false), + windowUninited(callbackId2, window, false), + windowUninited(callbackId2, win2, false), + windowUninited(callbackId2, win3, false)]); + + unregisterEWCallback(callbackId1); + unregisterEWCallback(callbackId2); + await uninitPromise; + ok(true, "Uninit called for all existing windows"); + + initPromise = Promise.all([windowInited(callbackId1, window), + windowInited(callbackId1, win2), + windowInited(callbackId1, win3), + windowInited(callbackId2, window), + windowInited(callbackId2, win2), + windowInited(callbackId2, win3)]); + + registerEWCallback(callbackId1); + registerEWCallback(callbackId2); + + await initPromise; + ok(true, "Init called for all existing windows for all registered consumers"); + + uninitPromise = Promise.all([windowUninited(callbackId1, win2, true), + windowUninited(callbackId2, win2, true)]); + await BrowserTestUtils.closeWindow(win2); + await uninitPromise; + ok(true, "Uninit called with closing=true for win2 for all registered consumers"); + + uninitPromise = Promise.all([windowUninited(callbackId1, win3, true), + windowUninited(callbackId2, win3, true)]); + await BrowserTestUtils.closeWindow(win3); + await uninitPromise; + ok(true, "Uninit called with closing=true for win3 for all registered consumers"); + + initPromise = windowInited(callbackId1); + let initPromise2 = windowInited(callbackId2); + win2 = await BrowserTestUtils.openNewBrowserWindow(); + is(await initPromise, win2, "Init called for new window for callback 1"); + is(await initPromise2, win2, "Init called for new window for callback 2"); + + uninitPromise = Promise.all([windowUninited(callbackId1, win2, true), + windowUninited(callbackId2, win2, true)]); + await BrowserTestUtils.closeWindow(win2); + await uninitPromise; + ok(true, "Uninit called with closing=true for win2 for all registered consumers"); + + uninitPromise = windowUninited(callbackId1, window, false); + unregisterEWCallback(callbackId1); + await uninitPromise; + ok(true, "Uninit called for main window without closing flag for the unregistered consumer"); + + uninitPromise = windowUninited(callbackId2, window, false); + let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500)); + unregisterEWCallback(callbackId2, false); + let result = await Promise.race([uninitPromise, timeoutPromise]); + is(result, undefined, "Uninit not called when unregistering a consumer with aCallUninit=false"); +});