From 7c5b398ffd31276c59489a9e16a39ecf1e37ea5d Mon Sep 17 00:00:00 2001 From: David Rajchenbach-Teller Date: Mon, 27 Jul 2015 19:18:19 +0200 Subject: [PATCH] Bug 1089695 - Async sanitize.js. r=mak --HG-- extra : transplant_source : %B8r%AA%97n%C9%5B%EA%A64%89P%D1%0E%8B%60%97%1B%BF%D3 --- browser/base/content/browser.js | 52 -- browser/base/content/sanitize.js | 532 ++++++----- browser/base/content/sanitizeDialog.js | 26 +- .../browser_sanitize-passwordDisabledHosts.js | 12 +- .../browser_sanitize-sitepermissions.js | 6 +- .../test/general/browser_sanitizeDialog.js | 831 +++++++++--------- browser/base/jar.mn | 2 +- browser/components/nsBrowserGlue.js | 5 +- .../tests/unit/test_clearHistory_shutdown.js | 2 + browser/modules/Sanitizer.jsm | 22 + browser/modules/moz.build | 1 + 11 files changed, 756 insertions(+), 735 deletions(-) create mode 100644 browser/modules/Sanitizer.jsm diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index ff35ed857d64..978b63149b43 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1283,9 +1283,6 @@ var gBrowserInit = { gBrowser.selectedBrowser.focus(); } - // Set up Sanitize Item - this._initializeSanitizer(); - // Enable/Disable auto-hide tabbar gBrowser.tabContainer.updateVisibility(); @@ -1670,9 +1667,6 @@ var gBrowserInit = { // initialise the offline listener BrowserOffline.init(); - // Set up Sanitize Item - this._initializeSanitizer(); - // initialize the private browsing UI gPrivateBrowsingUI.init(); @@ -1697,52 +1691,6 @@ var gBrowserInit = { BrowserOffline.uninit(); }, #endif - - _initializeSanitizer: function() { - const kDidSanitizeDomain = "privacy.sanitize.didShutdownSanitize"; - if (gPrefService.prefHasUserValue(kDidSanitizeDomain)) { - gPrefService.clearUserPref(kDidSanitizeDomain); - // We need to persist this preference change, since we want to - // check it at next app start even if the browser exits abruptly - gPrefService.savePrefFile(null); - } - - /** - * Migrate Firefox 3.0 privacy.item prefs under one of these conditions: - * - * a) User has customized any privacy.item prefs - * b) privacy.sanitize.sanitizeOnShutdown is set - */ - if (!gPrefService.getBoolPref("privacy.sanitize.migrateFx3Prefs")) { - let itemBranch = gPrefService.getBranch("privacy.item."); - let itemArray = itemBranch.getChildList(""); - - // See if any privacy.item prefs are set - let doMigrate = itemArray.some(function (name) itemBranch.prefHasUserValue(name)); - // Or if sanitizeOnShutdown is set - if (!doMigrate) - doMigrate = gPrefService.getBoolPref("privacy.sanitize.sanitizeOnShutdown"); - - if (doMigrate) { - let cpdBranch = gPrefService.getBranch("privacy.cpd."); - let clearOnShutdownBranch = gPrefService.getBranch("privacy.clearOnShutdown."); - for (let name of itemArray) { - try { - // don't migrate password or offlineApps clearing in the CRH dialog since - // there's no UI for those anymore. They default to false. bug 497656 - if (name != "passwords" && name != "offlineApps") - cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name)); - clearOnShutdownBranch.setBoolPref(name, itemBranch.getBoolPref(name)); - } - catch(e) { - Cu.reportError("Exception thrown during privacy pref migration: " + e); - } - } - } - - gPrefService.setBoolPref("privacy.sanitize.migrateFx3Prefs", true); - } - }, } diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js index ba10cafa492c..a0c0d29e8b45 100644 --- a/browser/base/content/sanitize.js +++ b/browser/base/content/sanitize.js @@ -1,10 +1,12 @@ -# -*- indent-tabs-mode: nil; js-indent-level: 4 -*- -# 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/. +// -*- indent-tabs-mode: nil; js-indent-level: 4 -*- +/* 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/. */ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", @@ -19,8 +21,21 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", "resource:///modules/DownloadsCommon.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/devtools/Console.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", + "resource://gre/modules/Timer.jsm"); -function Sanitizer() {} +/** + * A number of iterations after which to yield time back + * to the system. + */ +const YIELD_PERIOD = 10; + +function Sanitizer() { +} Sanitizer.prototype = { // warning to the caller: this one may raise an exception (e.g. bug #265028) clearItem: function (aItemName) @@ -29,6 +44,14 @@ Sanitizer.prototype = { this.items[aItemName].clear(); }, + promiseCanClearItem: function (aItemName, aArg) { + return new Promise(resolve => { + return this.canClearItem(aItemName, + (_, canClear) => resolve(canClear), + aArg) + }); + }, + canClearItem: function (aItemName, aCallback, aArg) { let canClear = this.items[aItemName].canClear; @@ -57,12 +80,43 @@ Sanitizer.prototype = { * If the consumer specifies the (optional) array parameter, only those * items get cleared (irrespective of the preference settings) */ - sanitize: function (aItemsToClear) - { - var deferred = Promise.defer(); - var seenError = false; + sanitize: Task.async(function*(aItemsToClear = null) { + let progress = {}; + let promise = this._sanitize(aItemsToClear, progress); + + // + // Depending on preferences, the sanitizer may perform asynchronous + // work before it starts cleaning up the Places database (e.g. closing + // windows). We need to make sure that the connection to that database + // hasn't been closed by the time we use it. + // + let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsPIPlacesDatabase) + .shutdownClient + .jsclient; + + shutdownClient.addBlocker("sanitize.js: Sanitize", + promise, + { + fetchState: () => { + return { progress }; + } + } + ); + try { + yield promise; + } finally { + Services.obs.notifyObservers(null, "sanitizer-sanitization-complete", ""); + } + }), + + _sanitize: Task.async(function*(aItemsToClear, progress = {}) { + let seenError = false; + let itemsToClear; if (Array.isArray(aItemsToClear)) { - var itemsToClear = [...aItemsToClear]; + // Shallow copy the array, as we are going to modify + // it in place later. + itemsToClear = [...aItemsToClear]; } else { let branch = Services.prefs.getBranch(this.prefDomain); itemsToClear = Object.keys(this.items).filter(itemName => { @@ -74,104 +128,71 @@ Sanitizer.prototype = { }); } + // Store the list of items to clear, in case we are killed before we + // get a chance to complete. + Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS, JSON.stringify(itemsToClear)); + + // Store the list of items to clear, for debugging/forensics purposes + for (let k of itemsToClear) { + progress[k] = "ready"; + } + // Ensure open windows get cleared first, if they're in our list, so that they don't stick // around in the recently closed windows list, and so we can cancel the whole thing // if the user selects to keep a window open from a beforeunload prompt. let openWindowsIndex = itemsToClear.indexOf("openWindows"); if (openWindowsIndex != -1) { itemsToClear.splice(openWindowsIndex, 1); - let item = this.items.openWindows; - - let ok = item.clear(() => { - try { - let clearedPromise = this.sanitize(itemsToClear); - clearedPromise.then(deferred.resolve, deferred.reject); - } catch(e) { - let error = "Sanitizer threw after closing windows: " + e; - Cu.reportError(error); - deferred.reject(error); - } - }); - // When cancelled, reject immediately - if (!ok) { - deferred.reject("Sanitizer canceled closing windows"); - } - - return deferred.promise; + yield this.items.openWindows.clear(); + progress.openWindows = "cleared"; } - let cookiesIndex = itemsToClear.indexOf("cookies"); - if (cookiesIndex != -1) { - itemsToClear.splice(cookiesIndex, 1); - let item = this.items.cookies; - item.range = this.range; - let ok = item.clear(() => { - try { - if (!itemsToClear.length) { - // we're done - deferred.resolve(); - return; - } - let clearedPromise = this.sanitize(itemsToClear); - clearedPromise.then(deferred.resolve, deferred.reject); - } catch(e) { - let error = "Sanitizer threw after clearing cookies: " + e; - Cu.reportError(error); - deferred.reject(error); - } - }); - // When cancelled, reject immediately - if (!ok) { - deferred.reject("Sanitizer canceled clearing cookies"); - } - - return deferred.promise; - } - - TelemetryStopwatch.start("FX_SANITIZE_TOTAL"); - // Cache the range of times to clear - if (this.ignoreTimespan) - var range = null; // If we ignore timespan, clear everything - else + let range = null; + // If we ignore timespan, clear everything, + // otherwise, pick a range. + if (!this.ignoreTimespan) { range = this.range || Sanitizer.getClearRange(); + } - let itemCount = Object.keys(itemsToClear).length; - let onItemComplete = function() { - if (!--itemCount) { - TelemetryStopwatch.finish("FX_SANITIZE_TOTAL"); - seenError ? deferred.reject() : deferred.resolve(); - } - }; for (let itemName of itemsToClear) { let item = this.items[itemName]; + if (!("clear" in item)) { + progress[itemName] = "`clear` not in item"; + continue; + } item.range = range; - if ("clear" in item) { - let clearCallback = (itemName, aCanClear) => { - // Some of these clear() may raise exceptions (see bug #265028) - // to sanitize as much as possible, we catch and store them, - // rather than fail fast. - // Callers should check returned errors and give user feedback - // about items that could not be sanitized - let item = this.items[itemName]; - try { - if (aCanClear) - item.clear(); - } catch(er) { - seenError = true; - Components.utils.reportError("Error sanitizing " + itemName + - ": " + er + "\n"); - } - onItemComplete(); - }; - this.canClearItem(itemName, clearCallback); - } else { - onItemComplete(); + let canClear = yield this.promiseCanClearItem(itemName); + if (!canClear) { + progress[itemName] = "cannot clear item"; + continue; + } + // Some of these clear() may raise exceptions (see bug #265028) + // to sanitize as much as possible, we catch and store them, + // rather than fail fast. + // Callers should check returned errors and give user feedback + // about items that could not be sanitized + let refObj = {}; + try { + TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj); + yield item.clear(); + progress[itemName] = "cleared"; + } catch(er) { + progress[itemName] = "failed"; + seenError = true; + console.error("Error sanitizing " + itemName, er); + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj); } } - return deferred.promise; - }, + // Sanitization is complete. + Preferences.reset(Sanitizer.PREF_SANITIZE_IN_PROGRESS); + progress = {}; + if (seenError) { + throw new Error("Error sanitizing"); + } + }), // Time span only makes sense in certain cases. Consumers who want // to only clear some private data can opt in by setting this to false, @@ -185,7 +206,8 @@ Sanitizer.prototype = { cache: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_CACHE"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj); var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]. getService(Ci.nsICacheStorageService); @@ -201,7 +223,7 @@ Sanitizer.prototype = { imageCache.clearCache(false); // true=chrome, false=content } catch(er) {} - TelemetryStopwatch.finish("FX_SANITIZE_CACHE"); + TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj); }, get canClear() @@ -211,10 +233,12 @@ Sanitizer.prototype = { }, cookies: { - clear: function (aCallback) + clear: Task.async(function* () { - TelemetryStopwatch.start("FX_SANITIZE_COOKIES"); - TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2"); + let yieldCounter = 0; + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj); + TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj); var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"] .getService(Ci.nsICookieManager); @@ -224,17 +248,22 @@ Sanitizer.prototype = { while (cookiesEnum.hasMoreElements()) { var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2); - if (cookie.creationTime > this.range[0]) + if (cookie.creationTime > this.range[0]) { // This cookie was created after our cutoff, clear it cookieMgr.remove(cookie.host, cookie.name, cookie.path, false); + + if (++yieldCounter % YIELD_PERIOD == 0) { + yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long + } + } } } else { // Remove everything cookieMgr.removeAll(); + yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long } - - TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2"); + TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj); // Clear deviceIds. Done asynchronously (returns before complete). let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"] @@ -242,17 +271,13 @@ Sanitizer.prototype = { mediaMgr.sanitizeDeviceIds(this.range && this.range[0]); // Clear plugin data. - TelemetryStopwatch.start("FX_SANITIZE_PLUGINS"); - this.clearPluginCookies().then( - function() { - TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS"); - TelemetryStopwatch.finish("FX_SANITIZE_COOKIES"); - aCallback(); - }); - return true; - }, + TelemetryStopwatch.start("FX_SANITIZE_PLUGINS", refObj); + yield this.promiseClearPluginCookies(); + TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj); + TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj); + }), - clearPluginCookies: function() { + promiseClearPluginCookies: Task.async(function*() { const phInterface = Ci.nsIPluginHost; const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL; let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface); @@ -264,33 +289,23 @@ Sanitizer.prototype = { let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000) : -1; if (!this.range || age >= 0) { let tags = ph.getPluginTags(); - function iterate(tag) { - let promise = new Promise(resolve => { - try { - let onClear = function(rv) { - // If the plugin doesn't support clearing by age, clear everything. - if (rv == Components.results. NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { - ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, function() { - resolve(); - }); - } else { - resolve(); - } - }; - ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, onClear); - } catch (ex) { - resolve(); - } - }); - return promise; - } - let promises = []; for (let tag of tags) { - promises.push(iterate(tag)); + try { + let rv = yield new Promise(resolve => + ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve) + ); + // If the plugin doesn't support clearing by age, clear everything. + if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { + yield new Promise(resolve => + ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve) + ); + } + } catch (ex) { + // Ignore errors from plug-ins + } } - return Promise.all(promises); } - }, + }), get canClear() { @@ -301,10 +316,11 @@ Sanitizer.prototype = { offlineApps: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj); Components.utils.import("resource:///modules/offlineAppCache.jsm"); OfflineAppCacheHelper.clear(); - TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS"); + TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj); }, get canClear() @@ -314,31 +330,37 @@ Sanitizer.prototype = { }, history: { - clear: function () + clear: Task.async(function* () { - TelemetryStopwatch.start("FX_SANITIZE_HISTORY"); - - if (this.range) - PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]); - else - PlacesUtils.history.removeAllPages(); - + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj); try { - var os = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - let clearStartingTime = this.range ? String(this.range[0]) : ""; - os.notifyObservers(null, "browser:purge-session-history", clearStartingTime); + if (this.range) { + yield PlacesUtils.history.removeVisitsByFilter({ + beginDate: new Date(this.range[0] / 1000), + endDate: new Date(this.range[1] / 1000) + }); + } else { + // Remove everything. + yield PlacesUtils.history.clear(); + } + + try { + let clearStartingTime = this.range ? String(this.range[0]) : ""; + Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime); + } catch (e) { } + + try { + let predictor = Components.classes["@mozilla.org/network/predictor;1"] + .getService(Components.interfaces.nsINetworkPredictor); + predictor.reset(); + } catch (e) { + console.error("Error while resetting the predictor", e); + } + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj); } - catch (e) { } - - try { - var predictor = Components.classes["@mozilla.org/network/predictor;1"] - .getService(Components.interfaces.nsINetworkPredictor); - predictor.reset(); - } catch (e) { } - - TelemetryStopwatch.finish("FX_SANITIZE_HISTORY"); - }, + }), get canClear() { @@ -351,7 +373,8 @@ Sanitizer.prototype = { formdata: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_FORMDATA"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj); // Clear undo history of all searchBars var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] @@ -378,7 +401,7 @@ Sanitizer.prototype = { } FormHistory.update(change); - TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA"); + TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj); }, canClear : function(aCallback, aArg) @@ -425,7 +448,8 @@ Sanitizer.prototype = { downloads: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj); Task.spawn(function () { let filterByTime = null; if (this.range) { @@ -439,9 +463,9 @@ Sanitizer.prototype = { // Clear all completed/cancelled downloads let list = yield Downloads.getList(Downloads.ALL); list.removeFinished(filterByTime); - TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS"); + TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj); }.bind(this)).then(null, error => { - TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS"); + TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj); Components.utils.reportError(error); }); }, @@ -456,7 +480,8 @@ Sanitizer.prototype = { sessions: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_SESSIONS"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj); // clear all auth tokens var sdr = Components.classes["@mozilla.org/security/sdr;1"] @@ -468,7 +493,7 @@ Sanitizer.prototype = { .getService(Components.interfaces.nsIObserverService); os.notifyObservers(null, "net:clear-active-logins", null); - TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS"); + TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj); }, get canClear() @@ -480,7 +505,8 @@ Sanitizer.prototype = { siteSettings: { clear: function () { - TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj); // Clear site-specific permissions like "Allow this site to open popups" // we ignore the "end" range and hope it is now() - none of the @@ -530,7 +556,7 @@ Sanitizer.prototype = { dump("Web Push may not be available.\n"); } - TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS"); + TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj); }, get canClear() @@ -565,15 +591,10 @@ Sanitizer.prototype = { win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow(); } }, - clear: function(aCallback) - { + clear: Task.async(function*() { // NB: this closes all *browser* windows, not other windows like the library, about window, // browser console, etc. - if (!aCallback) { - throw "Sanitizer's openWindows clear() requires a callback."; - } - // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload // dialogs let existingWindow = Services.appShell.hiddenDOMWindow; @@ -588,7 +609,7 @@ Sanitizer.prototype = { // If someone says "no" to a beforeunload prompt, we abort here: if (!this._canCloseWindow(someWin)) { this._resetAllWindowClosures(windowList); - return false; + throw new Error("Sanitize could not close windows: cancelled by user"); } // ...however, beforeunload prompts spin the event loop, and so the code here won't get @@ -597,13 +618,14 @@ Sanitizer.prototype = { // 'forget', and the timespans will be all wrong by now anyway: if (existingWindow.performance.now() > (startDate + 60 * 1000)) { this._resetAllWindowClosures(windowList); - return false; + throw new Error("Sanitize could not close windows: timeout"); } } // If/once we get here, we should actually be able to close all windows. - TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS"); + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj); // First create a new window. We do this first so that on non-mac, we don't // accidentally close the app by closing all the windows. @@ -613,39 +635,58 @@ Sanitizer.prototype = { let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank", features, defaultArgs); - // Window creation and destruction is asynchronous. We need to wait - // until all existing windows are fully closed, and the new window is - // fully open, before continuing. Otherwise the rest of the sanitizer - // could run too early (and miss new cookies being set when a page - // closes) and/or run too late (and not have a fully-formed window yet - // in existence). See bug 1088137. - let newWindowOpened = false; - function onWindowOpened(subject, topic, data) { - if (subject != newWindow) - return; - - Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished"); - newWindowOpened = true; - // If we're the last thing to happen, invoke callback. - if (numWindowsClosing == 0) { - TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS"); - aCallback(); - } - } - - let numWindowsClosing = windowList.length; - function onWindowClosed() { - numWindowsClosing--; - if (numWindowsClosing == 0) { - Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed"); - // If we're the last thing to happen, invoke callback. - if (newWindowOpened) { - TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS"); - aCallback(); + if (AppConstants.platform == "macosx") { + let onFullScreen = function(e) { + newWindow.removeEventListener("fullscreen", onFullScreen); + let docEl = newWindow.document.documentElement; + let sizemode = docEl.getAttribute("sizemode"); + if (!newWindow.fullScreen && sizemode == "fullscreen") { + docEl.setAttribute("sizemode", "normal"); + e.preventDefault(); + e.stopPropagation(); + return false; } } + newWindow.addEventListener("fullscreen", onFullScreen); } + let promiseReady = new Promise(resolve => { + // Window creation and destruction is asynchronous. We need to wait + // until all existing windows are fully closed, and the new window is + // fully open, before continuing. Otherwise the rest of the sanitizer + // could run too early (and miss new cookies being set when a page + // closes) and/or run too late (and not have a fully-formed window yet + // in existence). See bug 1088137. + let newWindowOpened = false; + function onWindowOpened(subject, topic, data) { + if (subject != newWindow) + return; + + Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished"); + if (AppConstants.platform == "macosx") { + newWindow.removeEventListener("fullscreen", onFullScreen); + } + newWindowOpened = true; + // If we're the last thing to happen, invoke callback. + if (numWindowsClosing == 0) { + TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj); + resolve(); + } + } + + let numWindowsClosing = windowList.length; + function onWindowClosed() { + numWindowsClosing--; + if (numWindowsClosing == 0) { + Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed"); + // If we're the last thing to happen, invoke callback. + if (newWindowOpened) { + TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj); + resolve(); + } + } + } + }); Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false); Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false); @@ -654,8 +695,8 @@ Sanitizer.prototype = { windowList.pop().close(); } newWindow.focus(); - return true; - }, + yield promiseReady; + }), get canClear() { @@ -668,9 +709,10 @@ Sanitizer.prototype = { // "Static" members -Sanitizer.prefDomain = "privacy.sanitize."; -Sanitizer.prefShutdown = "sanitizeOnShutdown"; -Sanitizer.prefDidShutdown = "didShutdownSanitize"; +Sanitizer.PREF_DOMAIN = "privacy.sanitize."; +Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown"; +Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress"; +Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize"; // Time span constants corresponding to values of the privacy.sanitize.timeSpan // pref. Used to determine how much history to clear, for various items @@ -729,7 +771,7 @@ Sanitizer.__defineGetter__("prefs", function() return Sanitizer._prefs ? Sanitizer._prefs : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService) - .getBranch(Sanitizer.prefDomain); + .getBranch(Sanitizer.PREF_DOMAIN); }); // Shows sanitization UI @@ -737,11 +779,10 @@ Sanitizer.showUI = function(aParentWindow) { var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher); -#ifdef XP_MACOSX - ww.openWindow(null, // make this an app-modal window on Mac -#else - ww.openWindow(aParentWindow, -#endif + let win = AppConstants.platform == "macosx" ? + null: // make this an app-modal window on Mac + aParentWindow; + ww.openWindow(win, "chrome://browser/content/sanitize.xul", "Sanitize", "chrome,titlebar,dialog,centerscreen,modal", @@ -757,24 +798,15 @@ Sanitizer.sanitize = function(aParentWindow) Sanitizer.showUI(aParentWindow); }; -Sanitizer.onStartup = function() -{ - // we check for unclean exit with pending sanitization - Sanitizer._checkAndSanitize(); -}; +Sanitizer.onStartup = Task.async(function*() { + // Make sure that we are triggered during shutdown, at the right time. + let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsPIPlacesDatabase) + .shutdownClient + .jsclient; -Sanitizer.onShutdown = function() -{ - // we check if sanitization is needed and perform it - Sanitizer._checkAndSanitize(); -}; - -// this is called on startup and shutdown, to perform pending sanitizations -Sanitizer._checkAndSanitize = function() -{ - const prefs = Sanitizer.prefs; - if (prefs.getBoolPref(Sanitizer.prefShutdown) && - !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) { + shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown", + () => Sanitizer.onShutdown()); // One time migration to remove support for the clear saved passwords on exit feature. if (!Services.prefs.getBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit")) { @@ -787,13 +819,31 @@ Sanitizer._checkAndSanitize = function() } Services.prefs.clearUserPref(deprecatedPref); Services.prefs.setBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit", true); - } - - // this is a shutdown or a startup after an unclean exit - var s = new Sanitizer(); - s.prefDomain = "privacy.clearOnShutdown."; - s.sanitize().then(function() { - prefs.setBoolPref(Sanitizer.prefDidShutdown, true); - }); } -}; + + // Handle incomplete sanitizations + if (Preferences.has(Sanitizer.PREF_SANITIZE_IN_PROGRESS)) { + // Firefox crashed during sanitization. + let s = new Sanitizer(); + let json = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS); + let itemsToClear = JSON.parse(json); + yield s.sanitize(itemsToClear); + } + if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) { + // Firefox crashed before having a chance to sanitize during shutdown. + // (note that if Firefox crashed during shutdown sanitization, we + // will hit both `if` so we will run a second double-sanitization). + yield Sanitizer.onShutdown(); + } +}); + +Sanitizer.onShutdown = Task.async(function*() { + if (!Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) { + return; + } + // Need to sanitize upon shutdown + let s = new Sanitizer(); + s.prefDomain = "privacy.clearOnShutdown."; + yield s.sanitize(); + Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true); +}); diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js index 538b179cab66..77ff794864e2 100644 --- a/browser/base/content/sanitizeDialog.js +++ b/browser/base/content/sanitizeDialog.js @@ -5,6 +5,9 @@ const Cc = Components.classes; const Ci = Components.interfaces; +const Cu = Components.utils; + +let {Sanitizer} = Cu.import("resource:///modules/Sanitizer.jsm", {}); var gSanitizePromptDialog = { @@ -43,17 +46,20 @@ var gSanitizePromptDialog = { var s = new Sanitizer(); s.prefDomain = "privacy.cpd."; + let tasks = []; let sanitizeItemList = document.querySelectorAll("#itemList > [preference]"); for (let i = 0; i < sanitizeItemList.length; i++) { let prefItem = sanitizeItemList[i]; let name = s.getNameFromPreference(prefItem.getAttribute("preference")); - s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) { - if (!aCanClear) { - aPrefItem.preference = null; - aPrefItem.checked = false; - aPrefItem.disabled = true; + let promise = s.promiseCanClearItem(name).then(canClear => { + if (canClear) { + return; } - }, prefItem); + prefItem.preference = null; + prefItem.checked = false; + prefItem.disabled = true; + }); + tasks.push(promise); } document.documentElement.getButton("accept").label = @@ -67,6 +73,10 @@ var gSanitizePromptDialog = { } else this.warningBox.hidden = true; + + Promise.all(tasks).then(() => { + Services.obs.notifyObservers(null, "sanitize-dialog-setup-complete", ""); + }); }, selectByTimespan: function () @@ -119,6 +129,7 @@ var gSanitizePromptDialog = { acceptButton.setAttribute("label", this.bundleBrowser.getString("sanitizeButtonClearing")); docElt.getButton("cancel").disabled = true; + try { s.sanitize().then(null, Components.utils.reportError) .then(() => window.close()) @@ -127,7 +138,6 @@ var gSanitizePromptDialog = { Components.utils.reportError("Exception during sanitize: " + er); return true; // We *do* want to close immediately on error. } - return false; }, /** @@ -505,7 +515,7 @@ var gSanitizePromptDialog = { } try { - s.sanitize(); + s.sanitize(); // We ignore the resulting Promise } catch (er) { Components.utils.reportError("Exception during sanitize: " + er); } diff --git a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js index 06cf2467e798..31dd30420f73 100644 --- a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js +++ b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js @@ -6,13 +6,11 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) .loadSubScript("chrome://browser/content/sanitize.js", tempScope); let Sanitizer = tempScope.Sanitizer; -function test() { - +add_task(function*() { var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); // Add a disabled host pwmgr.setLoginSavingEnabled("http://example.com", false); - // Sanity check is(pwmgr.getLoginSavingEnabled("http://example.com"), false, "example.com should be disabled for password saving since we haven't cleared that yet."); @@ -31,11 +29,11 @@ function test() { itemPrefs.setBoolPref("passwords", false); itemPrefs.setBoolPref("sessions", false); itemPrefs.setBoolPref("siteSettings", true); - + // Clear it - s.sanitize(); - + yield s.sanitize(); + // Make sure it's gone is(pwmgr.getLoginSavingEnabled("http://example.com"), true, "example.com should be enabled for password saving again now that we've cleared."); -} +}); diff --git a/browser/base/content/test/general/browser_sanitize-sitepermissions.js b/browser/base/content/test/general/browser_sanitize-sitepermissions.js index 0db2af14d231..05802252b1fa 100644 --- a/browser/base/content/test/general/browser_sanitize-sitepermissions.js +++ b/browser/base/content/test/general/browser_sanitize-sitepermissions.js @@ -15,7 +15,7 @@ function countPermissions() { return result; } -function test() { +add_task(function* test() { // sanitize before we start so we have a good baseline. // Set up the sanitizer to just clear siteSettings let s = new Sanitizer(); @@ -45,8 +45,8 @@ function test() { ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one"); // Clear it - s.sanitize(); + yield s.sanitize(); // Make sure it's gone is(numAtStart, countPermissions(), "Permission manager should have the same count it started with"); -} +}); diff --git a/browser/base/content/test/general/browser_sanitizeDialog.js b/browser/base/content/test/general/browser_sanitizeDialog.js index fa867128a4e4..b533d7b78298 100644 --- a/browser/base/content/test/general/browser_sanitizeDialog.js +++ b/browser/base/content/test/general/browser_sanitizeDialog.js @@ -24,6 +24,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Timer", + "resource://gre/modules/Timer.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); let tempScope = {}; Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) @@ -33,105 +37,108 @@ let Sanitizer = tempScope.Sanitizer; const kMsecPerMin = 60 * 1000; const kUsecPerMin = 60 * 1000000; -let formEntries, downloadIDs, olderDownloadIDs; +add_task(function* init() { + requestLongerTimeout(2); + blankSlate(); + registerCleanupFunction(() => { + blankSlate(); + return PlacesTestUtils.promiseAsyncUpdates(); + }); +}); -// Add tests here. Each is a function that's called by doNextTest(). -var gAllTests = [ +/** + * Initializes the dialog to its default state. + */ +add_task(function* default_state() { + let wh = new WindowHelper(); + wh.onload = function () { + // Select "Last Hour" + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + // Hide details + if (!this.getItemList().collapsed) + this.toggleDetails(); + this.acceptDialog(); + }; + wh.open(); + return wh.promiseClosed; +}); - /** - * Initializes the dialog to its default state. - */ - function () { +/** + * Cancels the dialog, makes sure history not cleared. + */ +add_task(function* test_cancel() { + // Add history (within the past hour) + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + + return new Promise(resolve => { + PlacesTestUtils.addVisits(places).then(() => { let wh = new WindowHelper(); wh.onload = function () { - // Select "Last Hour" this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("history", false); + this.checkDetails(false); + + // Show details + this.toggleDetails(); + this.checkDetails(true); + // Hide details - if (!this.getItemList().collapsed) - this.toggleDetails(); - this.acceptDialog(); + this.toggleDetails(); + this.checkDetails(false); + this.cancelDialog(); }; + wh.onunload = function* () { + yield promiseHistoryClearedState(uris, false); + yield blankSlate(); + yield promiseHistoryClearedState(uris, true); + }; + wh.promiseClosed.then(resolve); wh.open(); - }, + })}); +}); - /** - * Cancels the dialog, makes sure history not cleared. - */ - function () { - // Add history (within the past hour) - let uris = []; - let places = []; - let pURI; - for (let i = 0; i < 30; i++) { - pURI = makeURI("http://" + i + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); - uris.push(pURI); - } +/** + * Ensures that the combined history-downloads checkbox clears both history + * visits and downloads when checked; the dialog respects simple timespan. + */ +add_task(function* test_history_downloads_checked() { + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + yield addDownloadWithMinutesAgo(downloadIDs, i); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i); + } - PlacesTestUtils.addVisits(places).then(() => { - let wh = new WindowHelper(); - wh.onload = function () { - this.selectDuration(Sanitizer.TIMESPAN_HOUR); - this.checkPrefCheckbox("history", false); - this.checkDetails(false); - - // Show details - this.toggleDetails(); - this.checkDetails(true); - - // Hide details - this.toggleDetails(); - this.checkDetails(false); - this.cancelDialog(); - }; - wh.onunload = function () { - yield promiseHistoryClearedState(uris, false); - yield blankSlate(); - yield promiseHistoryClearedState(uris, true); - }; - wh.open(); - }); - }, - - function () { - // Add downloads (within the past hour). - Task.spawn(function () { - downloadIDs = []; - for (let i = 0; i < 5; i++) { - yield addDownloadWithMinutesAgo(downloadIDs, i); - } - // Add downloads (over an hour ago). - olderDownloadIDs = []; - for (let i = 0; i < 5; i++) { - yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i); - } - - doNextTest(); - }).then(null, Components.utils.reportError); - }, - - /** - * Ensures that the combined history-downloads checkbox clears both history - * visits and downloads when checked; the dialog respects simple timespan. - */ - function () { - // Add history (within the past hour). - let uris = []; - let places = []; - let pURI; - for (let i = 0; i < 30; i++) { - pURI = makeURI("http://" + i + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); - uris.push(pURI); - } - // Add history (over an hour ago). - let olderURIs = []; - for (let i = 0; i < 5; i++) { - pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)}); - olderURIs.push(pURI); - } + // Add history (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + // Add history (over an hour ago). + let olderURIs = []; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)}); + olderURIs.push(pURI); + } + let promiseSanitized = promiseSanitizationComplete(); + return new Promise(resolve => { PlacesTestUtils.addVisits(places).then(() => { let totalHistoryVisits = uris.length + olderURIs.length; @@ -141,7 +148,7 @@ var gAllTests = [ this.checkPrefCheckbox("history", true); this.acceptDialog(); }; - wh.onunload = function () { + wh.onunload = function* () { intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR, "timeSpan pref should be hour after accepting dialog with " + "hour selected"); @@ -152,6 +159,8 @@ var gAllTests = [ "downloads pref should be true after accepting dialog with " + "history checkbox checked"); + yield promiseSanitized; + // History visits and downloads within one hour should be cleared. yield promiseHistoryClearedState(uris, true); yield ensureDownloadsClearedState(downloadIDs, true); @@ -165,54 +174,42 @@ var gAllTests = [ yield promiseHistoryClearedState(olderURIs, true); yield ensureDownloadsClearedState(olderDownloadIDs, true); }; + wh.promiseClosed.then(resolve); wh.open(); }); - }, + }); +}); - /** - * Add form history entries for the next test. - */ - function () { - formEntries = []; +/** + * Ensures that the combined history-downloads checkbox removes neither + * history visits nor downloads when not checked. + */ +add_task(function* test_history_downloads_unchecked() { + // Add form entries + let formEntries = []; - let iter = function() { - for (let i = 0; i < 5; i++) { - formEntries.push(addFormEntryWithMinutesAgo(iter, i)); - yield undefined; - } - doNextTest(); - }(); + for (let i = 0; i < 5; i++) { + formEntries.push((yield promiseAddFormEntryWithMinutesAgo(i))); + } - iter.next(); - }, - function () { - // Add downloads (within the past hour). - Task.spawn(function () { - downloadIDs = []; - for (let i = 0; i < 5; i++) { - yield addDownloadWithMinutesAgo(downloadIDs, i); - } + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + yield addDownloadWithMinutesAgo(downloadIDs, i); + } - doNextTest(); - }).then(null, Components.utils.reportError); - }, - - /** - * Ensures that the combined history-downloads checkbox removes neither - * history visits nor downloads when not checked. - */ - function () { - // Add history, downloads, form entries (within the past hour). - let uris = []; - let places = []; - let pURI; - for (let i = 0; i < 5; i++) { - pURI = makeURI("http://" + i + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); - uris.push(pURI); - } + // Add history, downloads, form entries (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + return new Promise(resolve => { PlacesTestUtils.addVisits(places).then(() => { let wh = new WindowHelper(); wh.onload = function () { @@ -226,7 +223,7 @@ var gAllTests = [ this.checkPrefCheckbox("formdata", true); this.acceptDialog(); }; - wh.onunload = function () { + wh.onunload = function* () { intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR, "timeSpan pref should be hour after accepting dialog with " + "hour selected"); @@ -251,25 +248,31 @@ var gAllTests = [ yield promiseHistoryClearedState(uris, true); yield ensureDownloadsClearedState(downloadIDs, true); }; + wh.promiseClosed.then(resolve); wh.open(); }); - }, + }); +}); - /** - * Ensures that the "Everything" duration option works. - */ - function () { - // Add history. - let uris = []; - let places = []; - let pURI; - // within past hour, within past two hours, within past four hours and - // outside past four hours - [10, 70, 130, 250].forEach(function(aValue) { - pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); - uris.push(pURI); - }); +/** + * Ensures that the "Everything" duration option works. + */ +add_task(function* test_everything() { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function(aValue) { + pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + return new Promise(resolve => { PlacesTestUtils.addVisits(places).then(() => { let wh = new WindowHelper(); wh.onload = function () { @@ -291,32 +294,39 @@ var gAllTests = [ this.acceptDialog(); }; wh.onunload = function () { + yield promiseSanitized; intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING, "timeSpan pref should be everything after accepting dialog " + "with everything selected"); yield promiseHistoryClearedState(uris, true); }; + wh.promiseClosed.then(resolve); wh.open(); }); - }, + }); +}); - /** - * Ensures that the "Everything" warning is visible on dialog open after - * the previous test. - */ - function () { - // Add history. - let uris = []; - let places = []; - let pURI; - // within past hour, within past two hours, within past four hours and - // outside past four hours - [10, 70, 130, 250].forEach(function(aValue) { - pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); - places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); - uris.push(pURI); - }); +/** + * Ensures that the "Everything" warning is visible on dialog open after + * the previous test. + */ +add_task(function* test_everything_warning() { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function(aValue) { + pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + return new Promise(resolve => { PlacesTestUtils.addVisits(places).then(() => { let wh = new WindowHelper(); wh.onload = function () { @@ -332,33 +342,30 @@ var gAllTests = [ "timeSpan pref should be everything after accepting dialog " + "with everything selected"); + yield promiseSanitized; + yield promiseHistoryClearedState(uris, true); }; + wh.promiseClosed.then(resolve); wh.open(); }); - }, + }); +}); - /** - * Add form history entry for the next test. - */ - function () { - let iter = function() { - formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ]; - yield undefined; - doNextTest(); - }(); +/** + * The next three tests checks that when a certain history item cannot be + * cleared then the checkbox should be both disabled and unchecked. + * In addition, we ensure that this behavior does not modify the preferences. + */ +add_task(function* test_cannot_clear_history() { + // Add form entries + let formEntries = [ (yield promiseAddFormEntryWithMinutesAgo(10)) ]; - iter.next(); - }, + let promiseSanitized = promiseSanitizationComplete(); - /** - * The next three tests checks that when a certain history item cannot be - * cleared then the checkbox should be both disabled and unchecked. - * In addition, we ensure that this behavior does not modify the preferences. - */ - function () { - // Add history. - let pURI = makeURI("http://" + 10 + "-minutes-ago.com/"); + // Add history. + let pURI = makeURI("http://" + 10 + "-minutes-ago.com/"); + return new Promise(resolve => { PlacesTestUtils.addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}).then(() => { let uris = [ pURI ]; @@ -379,90 +386,88 @@ var gAllTests = [ this.acceptDialog(); }; wh.onunload = function () { + yield promiseSanitized; + yield promiseHistoryClearedState(uris, true); let exists = yield formNameExists(formEntries[0]); is(exists, false, "form entry " + formEntries[0] + " should no longer exist"); }; + wh.promiseClosed.then(resolve); wh.open(); }); - }, - function () { - let wh = new WindowHelper(); - wh.onload = function() { - boolPrefIs("cpd.history", true, - "history pref should be true after accepting dialog with " + - "history checkbox checked"); - boolPrefIs("cpd.formdata", true, - "formdata pref should be true after accepting dialog with " + - "formdata checkbox checked"); + }); +}); + +add_task(function* test_no_formdata_history_to_clear() { + let promiseSanitized = promiseSanitizationComplete(); + let wh = new WindowHelper(); + wh.onload = function() { + boolPrefIs("cpd.history", true, + "history pref should be true after accepting dialog with " + + "history checkbox checked"); + boolPrefIs("cpd.formdata", true, + "formdata pref should be true after accepting dialog with " + + "formdata checkbox checked"); + + // Even though the formdata pref is true, because there is no history + // left to clear, the checkbox will be disabled. + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.formdata']"); + ok(cb.length == 1 && cb[0].disabled && !cb[0].checked, + "There is no formdata history, checkbox should be disabled and be " + + "cleared to reduce user confusion (bug 497664)."); + + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.history']"); + ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, + "There is no history, but history checkbox should always be enabled " + + "and will be checked from previous preference."); + + this.acceptDialog(); + } + wh.open(); + yield wh.promiseClosed; + yield promiseSanitized; +}); + +add_task(function* test_form_entries() { + let formEntry = (yield promiseAddFormEntryWithMinutesAgo(10)); + + let promiseSanitized = promiseSanitizationComplete(); + + let wh = new WindowHelper(); + wh.onload = function() { + boolPrefIs("cpd.formdata", true, + "formdata pref should persist previous value after accepting " + + "dialog where you could not clear formdata."); + + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.formdata']"); + + info("There exists formEntries so the checkbox should be in sync with the pref."); + is(cb.length, 1, "There is only one checkbox for form data"); + ok(!cb[0].disabled, "The checkbox is enabled"); + ok(cb[0].checked, "The checkbox is checked"); + + this.acceptDialog(); + }; + wh.onunload = function () { + yield promiseSanitized; + let exists = yield formNameExists(formEntry); + is(exists, false, "form entry " + formEntry + " should no longer exist"); + }; + wh.open(); + return wh.promiseClosed; +}); - // Even though the formdata pref is true, because there is no history - // left to clear, the checkbox will be disabled. - var cb = this.win.document.querySelectorAll( - "#itemList > [preference='privacy.cpd.formdata']"); - - // Wait until the checkbox is disabled. This is done asynchronously - // from Sanitizer.init() as FormHistory.count() is a purely async API. - promiseWaitForCondition(() => cb[0].disabled).then(() => { - ok(cb.length == 1 && cb[0].disabled && !cb[0].checked, - "There is no formdata history, checkbox should be disabled and be " + - "cleared to reduce user confusion (bug 497664)."); - - cb = this.win.document.querySelectorAll( - "#itemList > [preference='privacy.cpd.history']"); - ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, - "There is no history, but history checkbox should always be enabled " + - "and will be checked from previous preference."); - - this.acceptDialog(); - }); - } - wh.open(); - }, - - /** - * Add form history entry for the next test. - */ - function () { - let iter = function() { - formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ]; - yield undefined; - doNextTest(); - }(); - - iter.next(); - }, - - function () { - let wh = new WindowHelper(); - wh.onload = function() { - boolPrefIs("cpd.formdata", true, - "formdata pref should persist previous value after accepting " + - "dialog where you could not clear formdata."); - - var cb = this.win.document.querySelectorAll( - "#itemList > [preference='privacy.cpd.formdata']"); - ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, - "There exists formEntries so the checkbox should be in sync with " + - "the pref."); - - this.acceptDialog(); - }; - wh.onunload = function () { - let exists = yield formNameExists(formEntries[0]); - is(exists, false, "form entry " + formEntries[0] + " should no longer exist"); - }; - wh.open(); - }, - - - /** - * These next six tests together ensure that toggling details persists - * across dialog openings. - */ - function () { +/** + * Ensure that toggling details persists + * across dialog openings. + */ +add_task(function* test_toggling_details_persists() { + { let wh = new WindowHelper(); wh.onload = function () { // Check all items and select "Everything" @@ -475,8 +480,9 @@ var gAllTests = [ this.acceptDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should remain closed because all items are checked. @@ -487,8 +493,9 @@ var gAllTests = [ this.acceptDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should be open because not all items are checked. @@ -500,8 +507,9 @@ var gAllTests = [ this.acceptDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should be open because not all items are checked. @@ -513,8 +521,9 @@ var gAllTests = [ this.cancelDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should be open because not all items are checked. @@ -528,8 +537,9 @@ var gAllTests = [ this.acceptDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should not be open because "Last Hour" is selected @@ -538,8 +548,9 @@ var gAllTests = [ this.cancelDialog(); }; wh.open(); - }, - function () { + yield wh.promiseClosed; + } + { let wh = new WindowHelper(); wh.onload = function () { // Details should have remained closed @@ -551,115 +562,118 @@ var gAllTests = [ this.cancelDialog(); }; wh.open(); - }, - function () { - // Test for offline cache deletion + yield wh.promiseClosed; + } +}); - // Prepare stuff, we will work with www.example.com - var URL = "http://www.example.com"; +// Test for offline cache deletion +add_task(function* test_offline_cache() { + // Prepare stuff, we will work with www.example.com + var URL = "http://www.example.com"; - var ios = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - var URI = ios.newURI(URL, null, null); + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var URI = ios.newURI(URL, null, null); - var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] - .getService(Ci.nsIScriptSecurityManager); - var principal = sm.createCodebasePrincipal(URI, {}); + var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var principal = sm.getNoAppCodebasePrincipal(URI); - // Give www.example.com privileges to store offline data - var pm = Cc["@mozilla.org/permissionmanager;1"] - .getService(Ci.nsIPermissionManager); - pm.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION); - pm.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + // Give www.example.com privileges to store offline data + var pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + pm.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION); + pm.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); - // Store something to the offline cache - var appcacheserv = Cc["@mozilla.org/network/application-cache-service;1"] - .getService(Ci.nsIApplicationCacheService); - var appcachegroupid = appcacheserv.buildGroupID(makeURI(URL + "/manifest"), LoadContextInfo.default); - var appcache = appcacheserv.createApplicationCache(appcachegroupid); + // Store something to the offline cache + var appcacheserv = Cc["@mozilla.org/network/application-cache-service;1"] + .getService(Ci.nsIApplicationCacheService); + var appcachegroupid = appcacheserv.buildGroupID(makeURI(URL + "/manifest"), LoadContextInfo.default); + var appcache = appcacheserv.createApplicationCache(appcachegroupid); - var cacheserv = Cc["@mozilla.org/netwerk/cache-storage-service;1"] - .getService(Ci.nsICacheStorageService); - var storage = cacheserv.appCacheStorage(LoadContextInfo.default, appcache); + var cacheserv = Cc["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Ci.nsICacheStorageService); + var storage = cacheserv.appCacheStorage(LoadContextInfo.default, appcache); - // Open the dialog - let wh = new WindowHelper(); - wh.onload = function () { - this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); - // Show details - this.toggleDetails(); - // Clear only offlineApps - this.uncheckAllCheckboxes(); - this.checkPrefCheckbox("offlineApps", true); - this.acceptDialog(); - }; - wh.onunload = function () { - // Check if the cache has been deleted - var size = -1; - var visitor = { - onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory) - { - size = aConsumption; - } - }; - storage.asyncVisitStorage(visitor, false); - // Offline cache visit happens synchronously, since it's forwarded to the old code - is(size, 0, "offline application cache entries evicted"); - }; - - var cacheListener = { - onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, - onCacheEntryAvailable: function (entry, isnew, appcache, status) { - is(status, Cr.NS_OK); - var stream = entry.openOutputStream(0); - var content = "content"; - stream.write(content, content.length); - stream.close(); - entry.close(); - wh.open(); + // Open the dialog + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Show details + this.toggleDetails(); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("offlineApps", true); + this.acceptDialog(); + }; + wh.onunload = function () { + // Check if the cache has been deleted + var size = -1; + var visitor = { + onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory) + { + size = aConsumption; } }; + storage.asyncVisitStorage(visitor, false); + // Offline cache visit happens synchronously, since it's forwarded to the old code + is(size, 0, "offline application cache entries evicted"); + }; - storage.asyncOpenURI(makeURI(URL), "", Ci.nsICacheStorage.OPEN_TRUNCATE, cacheListener); - }, - function () { - // Test for offline apps permission deletion + var cacheListener = { + onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, + onCacheEntryAvailable: function (entry, isnew, appcache, status) { + is(status, Cr.NS_OK); + var stream = entry.openOutputStream(0); + var content = "content"; + stream.write(content, content.length); + stream.close(); + entry.close(); + wh.open(); + } + }; - // Prepare stuff, we will work with www.example.com - var URL = "http://www.example.com"; + storage.asyncOpenURI(makeURI(URL), "", Ci.nsICacheStorage.OPEN_TRUNCATE, cacheListener); + return wh.promiseClosed; +}); - var ios = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - var URI = ios.newURI(URL, null, null); +// Test for offline apps permission deletion +add_task(function* test_offline_apps_permissions() { + // Prepare stuff, we will work with www.example.com + var URL = "http://www.example.com"; - var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] - .getService(Ci.nsIScriptSecurityManager); - var principal = sm.createCodebasePrincipal(URI, {}); + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var URI = ios.newURI(URL, null, null); - // Open the dialog - let wh = new WindowHelper(); - wh.onload = function () { - this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); - // Show details - this.toggleDetails(); - // Clear only offlineApps - this.uncheckAllCheckboxes(); - this.checkPrefCheckbox("siteSettings", true); - this.acceptDialog(); - }; - wh.onunload = function () { - // Check all has been deleted (privileges, data, cache) - var pm = Cc["@mozilla.org/permissionmanager;1"] - .getService(Ci.nsIPermissionManager); - is(pm.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed"); - }; - wh.open(); - } -]; + var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var principal = sm.createCodebasePrincipal(URI, {}); -// Index in gAllTests of the test currently being run. Incremented for each -// test run. See doNextTest(). -var gCurrTest = 0; + let promiseSanitized = promiseSanitizationComplete(); + + // Open the dialog + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Show details + this.toggleDetails(); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("siteSettings", true); + this.acceptDialog(); + }; + wh.onunload = function () { + yield promiseSanitized; + + // Check all has been deleted (privileges, data, cache) + var pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + is(pm.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed"); + }; + wh.open(); + return wh.promiseClosed; +}); let now_mSec = Date.now(); let now_uSec = now_mSec * 1000; @@ -675,6 +689,7 @@ let now_uSec = now_mSec * 1000; */ function WindowHelper(aWin) { this.win = aWin; + this.promiseClosed = new Promise(resolve => {this._resolveClosed = resolve}); } WindowHelper.prototype = { @@ -819,6 +834,8 @@ WindowHelper.prototype = { var loaded = false; let win = aSubject.QueryInterface(Ci.nsIDOMWindow); + let promiseDialogReady = promiseSanitizationDialogReady(); + win.addEventListener("load", function onload(event) { win.removeEventListener("load", onload, false); @@ -828,17 +845,10 @@ WindowHelper.prototype = { wh.win = win; loaded = true; - executeSoon(function () { - // Some exceptions that reach here don't reach the test harness, but - // ok()/is() do... - try { - wh.onload(); - } - catch (exc) { - win.close(); - ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); - finish(); - } + Task.spawn(function*() { + yield promiseDialogReady; + yield new Promise(resolve => setTimeout(resolve, 0)); + yield wh.onload(); }); }, false); @@ -855,23 +865,14 @@ WindowHelper.prototype = { win.removeEventListener("unload", onunload, false); wh.win = win; - executeSoon(function () { - // Some exceptions that reach here don't reach the test harness, but - // ok()/is() do... - try { - if (wh.onunload) { - Task.spawn(wh.onunload).then(function() { - waitForAsyncUpdates(doNextTest); - }).then(null, Components.utils.reportError); - } else { - waitForAsyncUpdates(doNextTest); - } - } - catch (exc) { - win.close(); - ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); - finish(); + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + Task.spawn(function*() { + if (wh.onunload) { + yield wh.onunload(); } + yield PlacesTestUtils.promiseAsyncUpdates(); + wh._resolveClosed(); }); }, false); } @@ -909,6 +910,14 @@ WindowHelper.prototype = { } }; +function promiseSanitizationDialogReady() { + return promiseTopicObserved("sanitize-dialog-setup-complete"); +} + +function promiseSanitizationComplete() { + return promiseTopicObserved("sanitizer-sanitization-complete"); +} + /** * Adds a download to history. * @@ -940,19 +949,23 @@ function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) { * @param aMinutesAgo * The entry will be added this many minutes ago */ -function addFormEntryWithMinutesAgo(then, aMinutesAgo) { +function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) { let name = aMinutesAgo + "-minutes-ago"; // Artifically age the entry to the proper vintage. let timestamp = now_uSec - (aMinutesAgo * kUsecPerMin); - FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp }, + return new Promise((resolve, reject) => + FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp }, { handleError: function (error) { do_throw("Error occurred updating form history: " + error); + reject(); }, - handleCompletion: function (reason) { then.next(); } - }); - return name; + handleCompletion: function (reason) { + resolve(name); + } + }) + ) } /** @@ -1039,22 +1052,6 @@ function downloadExists(aPath) }); } -/** - * Runs the next test in the gAllTests array. If all tests have been run, - * finishes the entire suite. - */ -function doNextTest() { - if (gAllTests.length <= gCurrTest) { - blankSlate(); - waitForAsyncUpdates(finish); - } - else { - let ct = gCurrTest; - gCurrTest++; - gAllTests[ct](); - } -} - /** * Ensures that the specified downloads are either cleared or not. * @@ -1094,13 +1091,3 @@ function intPrefIs(aPrefName, aExpectedVal, aMsg) { function visitTimeForMinutesAgo(aMinutesAgo) { return now_uSec - aMinutesAgo * kUsecPerMin; } - -/////////////////////////////////////////////////////////////////////////////// - -function test() { - requestLongerTimeout(2); - waitForExplicitFinish(); - blankSlate(); - // Kick off all the tests in the gAllTests array. - waitForAsyncUpdates(doNextTest); -} diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 7d5b86001ff0..12324d888a76 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -138,7 +138,7 @@ browser.jar: content/browser/safeMode.css (content/safeMode.css) content/browser/safeMode.js (content/safeMode.js) content/browser/safeMode.xul (content/safeMode.xul) -* content/browser/sanitize.js (content/sanitize.js) + content/browser/sanitize.js (content/sanitize.js) * content/browser/sanitize.xul (content/sanitize.xul) * content/browser/sanitizeDialog.js (content/sanitizeDialog.js) content/browser/sanitizeDialog.css (content/sanitizeDialog.css) diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index fad7f988720c..33fb7212dd54 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -507,6 +507,9 @@ BrowserGlue.prototype = { case "autocomplete-did-enter-text": this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput)); break; + case "test-initialize-sanitizer": + this._sanitizer.onStartup(); + break; } }, @@ -1159,6 +1162,7 @@ BrowserGlue.prototype = { Cu.import("resource://gre/modules/RokuApp.jsm"); return new RokuApp(aService); }, + mirror: true, types: ["video/mp4"], extensions: ["mp4"] }; @@ -1760,7 +1764,6 @@ BrowserGlue.prototype = { * - export bookmarks as HTML, if so configured. */ _onPlacesShutdown: function BG__onPlacesShutdown() { - this._sanitizer.onShutdown(); PageThumbs.uninit(); if (this._bookmarksBackupIdleTime) { diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js index 83dfa57e5d77..36a9ab7fc6e5 100644 --- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js +++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js @@ -49,6 +49,8 @@ add_task(function* test_execute() { let glue = Cc["@mozilla.org/browser/browserglue;1"]. getService(Ci.nsIObserver); glue.observe(null, "initial-migration-will-import-default-bookmarks", null); + glue.observe(null, "test-initialize-sanitizer", null); + Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true); Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true); diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm new file mode 100644 index 000000000000..cf9ea474a8ab --- /dev/null +++ b/browser/modules/Sanitizer.jsm @@ -0,0 +1,22 @@ +/* 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"; + +// +// A shared module for sanitize.js +// +// Until bug 1167238 lands, this serves only as a way to ensure that +// sanitize is loaded from its own compartment, rather than from that +// of the sanitize dialog. +// + +this.EXPORTED_SYMBOLS = ["Sanitizer"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +let scope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", scope); + +this.Sanitizer = scope.Sanitizer; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index eadb836f5fed..0f6ff61520a3 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -38,6 +38,7 @@ EXTRA_JS_MODULES += [ 'ReaderParent.jsm', 'RecentWindow.jsm', 'RemotePrompt.jsm', + 'Sanitizer.jsm', 'SelfSupportBackend.jsm', 'SitePermissions.jsm', 'Social.jsm',