tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 105c0ee1489b02f857cb2fbb39533cd86a24d462
parent 86d5d14266d7e7437a68ddc7138099d605e9b44e
Author: Sandor Molnar <smolnar@mozilla.com>
Date:   Wed,  7 Jan 2026 18:41:12 +0200

Revert "Bug 1856418 - Remove old clear data dialog and privacy.sanitize.useOldClearHistoryDialog preference. r=manuel,necko-reviewers,places-reviewers,urlbar-reviewers,akulyk,valentin,frontend-codestyle-reviewers" for causing bc failures @ sanitizeDialog.css

This reverts commit a549a264cc6671e3524d21679d41260c01351d54.

Diffstat:
Mbrowser/app/profile/firefox.js | 2++
Abrowser/base/content/sanitize.xhtml | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/base/content/sanitizeDialog.js | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mbrowser/base/content/test/sanitize/browser.toml | 2++
Mbrowser/base/content/test/sanitize/browser_cookiePermission_subDomains_v2.js | 1+
Mbrowser/base/content/test/sanitize/browser_sanitize-siteDataExceptAddons.js | 6++++++
Mbrowser/base/content/test/sanitize/browser_sanitize-timespans_v2.js | 4++++
Abrowser/base/content/test/sanitize/browser_sanitizeDialog.js | 815+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/base/content/test/sanitize/browser_sanitizeDialog_v2.js | 3+++
Mbrowser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js | 3+++
Mbrowser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js | 6++++++
Mbrowser/base/jar.mn | 1+
Abrowser/components/places/tests/unit/test_clearHistory_shutdown.js | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/places/tests/unit/xpcshell.toml | 4++++
Abrowser/components/preferences/dialogs/clearSiteData.css | 20++++++++++++++++++++
Abrowser/components/preferences/dialogs/clearSiteData.js | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/preferences/dialogs/clearSiteData.xhtml | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/dialogs/jar.mn | 5+++++
Abrowser/components/preferences/dialogs/sanitize.js | 34++++++++++++++++++++++++++++++++++
Abrowser/components/preferences/dialogs/sanitize.xhtml | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/privacy.js | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mbrowser/components/preferences/tests/browser.toml | 2++
Abrowser/components/preferences/tests/browser_privacy_syncDataClearing.js | 303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/tests/browser_privacy_syncDataClearing_v2.js | 5++++-
Mbrowser/components/preferences/tests/siteData/browser.toml | 2++
Abrowser/components/preferences/tests/siteData/browser_clearSiteData.js | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/tests/siteData/browser_clearSiteData_v2.js | 4++++
Mbrowser/components/urlbar/tests/browser-tips/browser_interventions.js | 16+++++++++-------
Mbrowser/components/urlbar/tests/browser/browser_quickactions.js | 8+++++++-
Mbrowser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js | 8+++++++-
Mbrowser/modules/Sanitizer.sys.mjs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Abrowser/modules/test/unit/test_Sanitizer_interrupted.js | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/modules/test/unit/xpcshell.toml | 4++++
Mmodules/libpref/init/StaticPrefList.yaml | 5+++++
Mnetwerk/cache2/CacheObserver.h | 4++++
Asecurity/manager/ssl/tests/unit/test_sss_sanitizeOnShutdown.js | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msecurity/manager/ssl/tests/unit/xpcshell.toml | 11+++++++++++
Mstylelint-rollouts.config.js | 1+
38 files changed, 2512 insertions(+), 73 deletions(-)

diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -1244,6 +1244,8 @@ pref("privacy.history.custom", false); // 6 - Last 24 hours pref("privacy.sanitize.timeSpan", 1); +pref("privacy.sanitize.useOldClearHistoryDialog", false); + pref("privacy.sanitize.clearOnShutdown.hasMigratedToNewPrefs2", false); pref("privacy.sanitize.clearOnShutdown.hasMigratedToNewPrefs3", false); // flags to track migration of clear history dialog prefs, where cpd stands for diff --git a/browser/base/content/sanitize.xhtml b/browser/base/content/sanitize.xhtml @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. --> +<?csp default-src chrome:; img-src chrome: moz-icon:; style-src chrome: +'unsafe-inline'; ?> + +<!DOCTYPE window> + +<window + id="SanitizeDialog" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="lastSelected screenX screenY" + data-l10n-id="sanitize-dialog-title" + data-l10n-attrs="style" +> + <dialog buttons="accept,cancel"> + <hbox> + <html:h2 id="titleText" /> + </hbox> + + <linkset> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/preferences.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/sanitizeDialog.css" + /> + + <html:link + rel="stylesheet" + href="chrome://browser/content/sanitizeDialog.css" + /> + + <html:link rel="localization" href="browser/sanitize.ftl" /> + </linkset> + + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/sanitizeDialog.js" /> + + <hbox id="SanitizeDurationBox" align="center"> + <label + data-l10n-id="clear-time-duration-prefix" + control="sanitizeDurationChoice" + id="sanitizeDurationLabel" + /> + <menulist + id="sanitizeDurationChoice" + preference="privacy.sanitize.timeSpan" + flex="1" + > + <menupopup id="sanitizeDurationPopup"> + <menuitem + data-l10n-id="clear-time-duration-value-last-hour" + value="1" + /> + <menuitem + data-l10n-id="clear-time-duration-value-last-2-hours" + value="2" + /> + <menuitem + data-l10n-id="clear-time-duration-value-last-4-hours" + value="3" + /> + <menuitem + data-l10n-id="clear-time-duration-value-today" + value="4" + id="sanitizeSinceMidnight" + /> + <menuseparator /> + <menuitem + data-l10n-id="clear-time-duration-value-everything" + value="0" + /> + </menupopup> + </menulist> + <label + id="sanitizeDurationSuffixLabel" + data-l10n-id="clear-time-duration-suffix" + /> + </hbox> + + <vbox id="sanitizeEverythingWarningBox"> + <spacer flex="1" /> + <hbox align="center"> + <image id="sanitizeEverythingWarningIcon" /> + <vbox id="sanitizeEverythingWarningDescBox" flex="1"> + <description id="sanitizeEverythingWarning" /> + <description + id="sanitizeEverythingUndoWarning" + data-l10n-id="sanitize-everything-undo-warning" + ></description> + </vbox> + </hbox> + <spacer flex="1" /> + </vbox> + + <groupbox> + <label> + <html:h2 data-l10n-id="history-section-label" /> + </label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-history-and-downloads" + preference="privacy.cpd.history" + /> + <checkbox + data-l10n-id="item-active-logins" + preference="privacy.cpd.sessions" + /> + <checkbox + data-l10n-id="item-form-search-history" + preference="privacy.cpd.formdata" + /> + </vbox> + <vbox flex="1"> + <checkbox + data-l10n-id="item-cookies" + preference="privacy.cpd.cookies" + /> + <checkbox data-l10n-id="item-cache" preference="privacy.cpd.cache" /> + </vbox> + </hbox> + </groupbox> + <groupbox> + <label> + <html:h2 data-l10n-id="data-section-label" /> + </label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-site-settings" + preference="privacy.cpd.siteSettings" + /> + </vbox> + <vbox flex="1"> + <checkbox + data-l10n-id="item-offline-apps" + preference="privacy.cpd.offlineApps" + /> + </vbox> + </hbox> + </groupbox> + </dialog> +</window> diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js @@ -20,6 +20,13 @@ ChromeUtils.defineESModuleGetters(lazy, { SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", }); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "USE_OLD_DIALOG", + "privacy.sanitize.useOldClearHistoryDialog", + false +); + Preferences.addAll([ { id: "privacy.cpd.history", type: "bool" }, { id: "privacy.cpd.formdata", type: "bool" }, @@ -86,7 +93,7 @@ var gSanitizePromptDialog = { this._inClearOnShutdownNewDialog = false; this._inClearSiteDataNewDialog = false; this._inBrowserWindow = !!arg.inBrowserWindow; - if (arg.mode) { + if (arg.mode && !lazy.USE_OLD_DIALOG) { this._inClearOnShutdownNewDialog = arg.mode == "clearOnShutdown"; this._inClearSiteDataNewDialog = arg.mode == "clearSiteData"; } @@ -103,8 +110,10 @@ var gSanitizePromptDialog = { } } - this._dataSizesUpdated = false; - this.dataSizesFinishedUpdatingPromise = this.getAndUpdateDataSizes(); // this promise is still used in tests + if (!lazy.USE_OLD_DIALOG) { + this._dataSizesUpdated = false; + this.dataSizesFinishedUpdatingPromise = this.getAndUpdateDataSizes(); // this promise is still used in tests + } let OKButton = this._dialog.getButton("accept"); let clearOnShutdownGroupbox = document.getElementById( @@ -129,7 +138,7 @@ var gSanitizePromptDialog = { // If this is the first time the user is opening the new clear on shutdown // dialog, migrate their prefs Sanitizer.maybeMigratePrefs("clearOnShutdown"); - } else { + } else if (!lazy.USE_OLD_DIALOG) { okButtonl10nID = "sanitize-button-ok2"; clearOnShutdownGroupbox.remove(); if (this._inClearSiteDataNewDialog) { @@ -145,22 +154,24 @@ var gSanitizePromptDialog = { } document.l10n.setAttributes(OKButton, okButtonl10nID); - this._sinceMidnightSanitizeDurationOption = document.getElementById( - "sanitizeSinceMidnight" - ); - this._cookiesAndSiteDataCheckbox = - document.getElementById("cookiesAndStorage"); - this._cacheCheckbox = document.getElementById("cache"); - - let midnightTime = Intl.DateTimeFormat(navigator.language, { - hour: "numeric", - minute: "numeric", - }).format(new Date().setHours(0, 0, 0, 0)); - document.l10n.setAttributes( - this._sinceMidnightSanitizeDurationOption, - "clear-time-duration-value-since-midnight", - { midnightTime } - ); + if (!lazy.USE_OLD_DIALOG) { + this._sinceMidnightSanitizeDurationOption = document.getElementById( + "sanitizeSinceMidnight" + ); + this._cookiesAndSiteDataCheckbox = + document.getElementById("cookiesAndStorage"); + this._cacheCheckbox = document.getElementById("cache"); + + let midnightTime = Intl.DateTimeFormat(navigator.language, { + hour: "numeric", + minute: "numeric", + }).format(new Date().setHours(0, 0, 0, 0)); + document.l10n.setAttributes( + this._sinceMidnightSanitizeDurationOption, + "clear-time-duration-value-since-midnight", + { midnightTime } + ); + } document .getElementById("sanitizeDurationChoice") @@ -185,6 +196,12 @@ var gSanitizePromptDialog = { ) { this.prepareWarning(); this.warningBox.hidden = false; + if (lazy.USE_OLD_DIALOG) { + document.l10n.setAttributes( + document.documentElement, + "sanitize-dialog-title-everything" + ); + } let warningDesc = document.getElementById("sanitizeEverythingWarning"); // Ensure we've translated and sized the warning. await document.l10n.translateFragment(warningDesc); @@ -223,8 +240,17 @@ var gSanitizePromptDialog = { window.resizeBy(0, diff); } - // make sure the sizes are updated - await this.updateDataSizesInUI(); + // update title for the old dialog + if (lazy.USE_OLD_DIALOG) { + document.l10n.setAttributes( + document.documentElement, + "sanitize-dialog-title-everything" + ); + } + // make sure the sizes are updated in the new dialog + else { + await this.updateDataSizesInUI(); + } return; } @@ -236,13 +262,15 @@ var gSanitizePromptDialog = { window.resizeBy(0, -diff); warningBox.hidden = true; } - document.l10n.setAttributes( - document.documentElement, - "sanitize-dialog-title2" - ); + let datal1OnId = lazy.USE_OLD_DIALOG + ? "sanitize-dialog-title" + : "sanitize-dialog-title2"; + document.l10n.setAttributes(document.documentElement, datal1OnId); - // Update data sizes to display - await this.updateDataSizesInUI(); + if (!lazy.USE_OLD_DIALOG) { + // We only update data sizes to display on the new dialog + await this.updateDataSizesInUI(); + } }, sanitize(event) { @@ -340,6 +368,10 @@ var gSanitizePromptDialog = { * @returns {Promise} resolves when updating the UI is complete */ async getAndUpdateDataSizes() { + if (lazy.USE_OLD_DIALOG) { + return; + } + // We have to update sites before displaying data sizes // when the dialog is opened in the browser context, since users // can open the dialog in this context without opening about:preferences. @@ -382,6 +414,13 @@ var gSanitizePromptDialog = { updatePrefs() { Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan); + if (lazy.USE_OLD_DIALOG) { + let historyValue = Preferences.get(`privacy.cpd.history`).value; + // Keep the pref for the download history in sync with the history pref. + Preferences.get("privacy.cpd.downloads").value = historyValue; + Services.prefs.setBoolPref("privacy.cpd.downloads", historyValue); + } + // Now manually set the prefs from their corresponding preference // elements. var prefs = this._getItemPrefs(); @@ -488,6 +527,11 @@ var gSanitizePromptDialog = { * @returns {string[]} array of items ["cache", "browsingHistoryAndDownloads"...] */ getItemsToClear() { + // the old dialog uses the preferences to decide what to clear + if (lazy.USE_OLD_DIALOG) { + return null; + } + let items = []; for (let cb of this._allCheckboxes) { if (cb.checked) { diff --git a/browser/base/content/test/sanitize/browser.toml b/browser/base/content/test/sanitize/browser.toml @@ -37,6 +37,8 @@ skip-if = [ ["browser_sanitize-timespans.js"] +["browser_sanitizeDialog.js"] + ["browser_sanitizeDialog_v2.js"] ["browser_sanitizeDialog_v2_dataSizes.js"] diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_subDomains_v2.js b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains_v2.js @@ -12,6 +12,7 @@ add_setup(async function () { ["privacy.clearOnShutdown.downloads", false], ["privacy.clearOnShutdown.siteSettings", false], ["browser.sanitizer.loglevel", "All"], + ["privacy.sanitize.useOldClearHistoryDialog", false], ], }); }); diff --git a/browser/base/content/test/sanitize/browser_sanitize-siteDataExceptAddons.js b/browser/base/content/test/sanitize/browser_sanitize-siteDataExceptAddons.js @@ -24,6 +24,12 @@ async function createAddon() { return extension; } +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); +}); + async function checkAddonPermissionClearingWithPref(prefs, expectedValue) { let extension = await createAddon(); diff --git a/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js b/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js @@ -56,6 +56,10 @@ async function onHistoryReady() { // Should test cookies here, but nsICookieManager/nsICookieService // doesn't let us fake creation times. bug 463127 + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); + let itemsToClear = ["historyAndFormData", "downloads"]; let publicList = await Downloads.getList(Downloads.PUBLIC); diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog.js b/browser/base/content/test/sanitize/browser_sanitizeDialog.js @@ -0,0 +1,815 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests the sanitize dialog (a.k.a. the clear recent history dialog). + * See bug 480169. + * + * The purpose of this test is not to fully flex the sanitize timespan code; + * browser/base/content/test/sanitize/browser_sanitize-timespans.js does that. This + * test checks the UI of the dialog and makes sure it's correctly connected to + * the sanitize timespan code. + * + * Some of this code, especially the history creation parts, was taken from + * browser/base/content/test/sanitize/browser_sanitize-timespans.js. + */ + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + Timer: "resource://gre/modules/Timer.sys.mjs", +}); + +/** + * Ensures that the specified URIs are either cleared or not. + * + * @param aURIs + * Array of page URIs + * @param aShouldBeCleared + * True if each visit to the URI should be cleared, false otherwise + */ +async function promiseHistoryClearedState(aURIs, aShouldBeCleared) { + for (let uri of aURIs) { + let visited = await PlacesUtils.history.hasVisits(uri); + Assert.equal( + visited, + !aShouldBeCleared, + `history visit ${uri.spec} should ${ + aShouldBeCleared ? "no longer" : "still" + } exist` + ); + } +} + +add_setup(async function () { + requestLongerTimeout(3); + await blankSlate(); + registerCleanupFunction(async function () { + await blankSlate(); + await PlacesTestUtils.promiseAsyncUpdates(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", true]], + }); +}); + +/** + * Initializes the dialog to its default state. + */ +add_task(async function default_state() { + let dh = new DialogHelper(); + dh.onload = function () { + // Select "Last Hour" + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Cancels the dialog, makes sure history not cleared. + */ +add_task(async function test_cancel() { + // Add history (within the past hour) + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("https://" + i + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) }); + uris.push(pURI); + } + await PlacesTestUtils.addVisits(places); + + let dh = new DialogHelper(); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("history", false); + this.cancelDialog(); + }; + dh.onunload = async function () { + await promiseHistoryClearedState(uris, false); + await blankSlate(); + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the combined history-downloads checkbox clears both history + * visits and downloads when checked; the dialog respects simple timespan. + */ +add_task(async function test_history_downloads_checked() { + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(downloadIDs, i); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i); + } + + // Add history (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("https://" + 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("https://" + (61 + i) + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i) }); + olderURIs.push(pURI); + } + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + + let dh = new DialogHelper(); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("history", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected" + ); + boolPrefIs( + "cpd.history", + true, + "history pref should be true after accepting dialog with " + + "history checkbox checked" + ); + boolPrefIs( + "cpd.downloads", + true, + "downloads pref should be true after accepting dialog with " + + "history checkbox checked" + ); + + await promiseSanitized; + + // History visits and downloads within one hour should be cleared. + await promiseHistoryClearedState(uris, true); + await ensureDownloadsClearedState(downloadIDs, true); + + // Visits and downloads > 1 hour should still exist. + await promiseHistoryClearedState(olderURIs, false); + await ensureDownloadsClearedState(olderDownloadIDs, false); + + // OK, done, cleanup after ourselves. + await blankSlate(); + await promiseHistoryClearedState(olderURIs, true); + await ensureDownloadsClearedState(olderDownloadIDs, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the combined history-downloads checkbox removes neither + * history visits nor downloads when not checked. + */ +add_task(async function test_history_downloads_unchecked() { + // Add form entries + let formEntries = []; + + for (let i = 0; i < 5; i++) { + formEntries.push(await promiseAddFormEntryWithMinutesAgo(i)); + } + + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(downloadIDs, i); + } + + // Add history, downloads, form entries (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 5; i++) { + pURI = makeURI("https://" + i + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) }); + uris.push(pURI); + } + + await PlacesTestUtils.addVisits(places); + let dh = new DialogHelper(); + dh.onload = function () { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan" + ); + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + + // Remove only form entries, leave history (including downloads). + this.checkPrefCheckbox("history", false); + this.checkPrefCheckbox("formdata", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected" + ); + boolPrefIs( + "cpd.history", + false, + "history pref should be false after accepting dialog with " + + "history checkbox unchecked" + ); + boolPrefIs( + "cpd.downloads", + false, + "downloads pref should be false after accepting dialog with " + + "history checkbox unchecked" + ); + + // Of the three only form entries should be cleared. + await promiseHistoryClearedState(uris, false); + await ensureDownloadsClearedState(downloadIDs, false); + + for (let entry of formEntries) { + let exists = await formNameExists(entry); + ok(!exists, "form entry " + entry + " should no longer exist"); + } + + // OK, done, cleanup after ourselves. + await blankSlate(); + await promiseHistoryClearedState(uris, true); + await ensureDownloadsClearedState(downloadIDs, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the "Everything" duration option works. + */ +add_task(async 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("https://" + aValue + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) }); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + let dh = new DialogHelper(); + dh.onload = function () { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan" + ); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("history", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected" + ); + + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the "Everything" warning is visible on dialog open after + * the previous test. + */ +add_task(async 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("https://" + aValue + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) }); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + let dh = new DialogHelper(); + dh.onload = function () { + is( + this.isWarningPanelVisible(), + true, + "Warning panel should be visible after previously accepting dialog " + + "with clearing everything" + ); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("history", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected" + ); + + await promiseSanitized; + + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * 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(async function test_cannot_clear_history() { + // Add form entries + let formEntries = [await promiseAddFormEntryWithMinutesAgo(10)]; + + let promiseSanitized = promiseSanitizationComplete(); + + // Add history. + let pURI = makeURI("https://" + 10 + "-minutes-ago.com/"); + await PlacesTestUtils.addVisits({ + uri: pURI, + visitDate: visitTimeForMinutesAgo(10), + }); + let uris = [pURI]; + + let dh = new DialogHelper(); + dh.onload = function () { + // Check that the relevant checkboxes are enabled + var cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.formdata']" + ); + ok( + cb.length == 1 && !cb[0].disabled, + "There is formdata, checkbox to clear formdata should be enabled." + ); + + cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.history']" + ); + ok( + cb.length == 1 && !cb[0].disabled, + "There is history, checkbox to clear history should be enabled." + ); + + this.checkAllCheckboxes(); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + + await promiseHistoryClearedState(uris, true); + + let exists = await formNameExists(formEntries[0]); + ok(!exists, "form entry " + formEntries[0] + " should no longer exist"); + }; + dh.open(); + await dh.promiseClosed; +}); + +add_task(async function test_no_formdata_history_to_clear() { + let promiseSanitized = promiseSanitizationComplete(); + let dh = new DialogHelper(); + dh.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" + ); + + var cb = this.win.document.querySelectorAll( + "checkbox[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(); + }; + dh.open(); + await dh.promiseClosed; + await promiseSanitized; +}); + +add_task(async function test_form_entries() { + let formEntry = await promiseAddFormEntryWithMinutesAgo(10); + + let promiseSanitized = promiseSanitizationComplete(); + + let dh = new DialogHelper(); + dh.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( + "checkbox[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(); + }; + dh.onunload = async function () { + await promiseSanitized; + let exists = await formNameExists(formEntry); + ok(!exists, "form entry " + formEntry + " should no longer exist"); + }; + dh.open(); + await dh.promiseClosed; +}); + +// Test for offline apps permission deletion +add_task(async function test_offline_apps_permissions() { + // Prepare stuff, we will work with www.example.com + var URL = "https://www.example.com"; + var URI = makeURI(URL); + var principal = Services.scriptSecurityManager.createContentPrincipal( + URI, + {} + ); + + let promiseSanitized = promiseSanitizationComplete(); + + // Open the dialog + let dh = new DialogHelper(); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("siteSettings", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + + // Check all has been deleted (privileges, data, cache) + is( + Services.perms.testPermissionFromPrincipal(principal, "offline-app"), + 0, + "offline-app permissions removed" + ); + }; + dh.open(); + await dh.promiseClosed; +}); + +var now_mSec = Date.now(); +var now_uSec = now_mSec * 1000; + +/** + * This wraps the dialog and provides some convenience methods for interacting + * with it. + * + * @param browserWin (optional) + * The browser window that the dialog is expected to open in. If not + * supplied, the initial browser window of the test run is used. + */ +function DialogHelper(browserWin = window) { + this._browserWin = browserWin; + this.win = null; + this.promiseClosed = new Promise(resolve => { + this._resolveClosed = resolve; + }); +} + +DialogHelper.prototype = { + /** + * "Presses" the dialog's OK button. + */ + acceptDialog() { + let dialogEl = this.win.document.querySelector("dialog"); + is( + dialogEl.getButton("accept").disabled, + false, + "Dialog's OK button should not be disabled" + ); + dialogEl.acceptDialog(); + }, + + /** + * "Presses" the dialog's Cancel button. + */ + cancelDialog() { + this.win.document.querySelector("dialog").cancelDialog(); + }, + + /** + * (Un)checks a history scope checkbox (browser & download history, + * form history, etc.). + * + * @param aPrefName + * The final portion of the checkbox's privacy.cpd.* preference name + * @param aCheckState + * True if the checkbox should be checked, false otherwise + */ + checkPrefCheckbox(aPrefName, aCheckState) { + var pref = "privacy.cpd." + aPrefName; + var cb = this.win.document.querySelectorAll( + "checkbox[preference='" + pref + "']" + ); + is(cb.length, 1, "found checkbox for " + pref + " preference"); + if (cb[0].checked != aCheckState) { + cb[0].click(); + } + }, + + /** + * Makes sure all the checkboxes are checked. + */ + _checkAllCheckboxesCustom(check) { + var cb = this.win.document.querySelectorAll("checkbox[preference]"); + Assert.greater(cb.length, 1, "found checkboxes for preferences"); + for (var i = 0; i < cb.length; ++i) { + var pref = this.win.Preferences.get(cb[i].getAttribute("preference")); + if (!!pref.value ^ check) { + cb[i].click(); + } + } + }, + + checkAllCheckboxes() { + this._checkAllCheckboxesCustom(true); + }, + + uncheckAllCheckboxes() { + this._checkAllCheckboxesCustom(false); + }, + + /** + * @return The dialog's duration dropdown + */ + getDurationDropdown() { + return this.win.document.getElementById("sanitizeDurationChoice"); + }, + + /** + * @return The clear-everything warning box + */ + getWarningPanel() { + return this.win.document.getElementById("sanitizeEverythingWarningBox"); + }, + + /** + * @return True if the "Everything" warning panel is visible (as opposed to + * the tree) + */ + isWarningPanelVisible() { + return !this.getWarningPanel().hidden; + }, + + /** + * Opens the clear recent history dialog. Before calling this, set + * this.onload to a function to execute onload. It should close the dialog + * when done so that the tests may continue. Set this.onunload to a function + * to execute onunload. this.onunload is optional. If it returns true, the + * caller is expected to call promiseAsyncUpdates at some point; if false is + * returned, promiseAsyncUpdates is called automatically. + */ + async open() { + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + null, + "chrome://browser/content/sanitize.xhtml", + { + isSubDialog: true, + } + ); + + executeSoon(() => { + Sanitizer.showUI(this._browserWin); + }); + + this.win = await dialogPromise; + this.win.addEventListener( + "load", + () => { + // Run onload on next tick so that gSanitizePromptDialog.init can run first. + executeSoon(() => this.onload()); + }, + { once: true } + ); + + this.win.addEventListener( + "unload", + () => { + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + (async () => { + if (this.onunload) { + await this.onunload(); + } + await PlacesTestUtils.promiseAsyncUpdates(); + this._resolveClosed(); + this.win = null; + })(); + }, + { once: true } + ); + }, + + /** + * Selects a duration in the duration dropdown. + * + * @param aDurVal + * One of the Sanitizer.TIMESPAN_* values + */ + selectDuration(aDurVal) { + this.getDurationDropdown().value = aDurVal; + if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) { + is( + this.isWarningPanelVisible(), + true, + "Warning panel should be visible for TIMESPAN_EVERYTHING" + ); + } else { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should not be visible for non-TIMESPAN_EVERYTHING" + ); + } + }, +}; + +/** + * Adds a download to history. + * + * @param aMinutesAgo + * The download will be downloaded this many minutes ago + */ +async function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) { + let publicList = await Downloads.getList(Downloads.PUBLIC); + + let name = "fakefile-" + aMinutesAgo + "-minutes-ago"; + let download = await Downloads.createDownload({ + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: name, + }); + download.startTime = new Date(now_mSec - aMinutesAgo * kMsecPerMin); + download.canceled = true; + publicList.add(download); + + ok( + await downloadExists(name), + "Sanity check: download " + name + " should exist after creating it" + ); + + aExpectedPathList.push(name); +} + +/** + * Adds a form entry to history. + * + * @param aMinutesAgo + * The entry will be added this many minutes ago + */ +function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) { + let name = aMinutesAgo + "-minutes-ago"; + + // Artifically age the entry to the proper vintage. + let timestamp = now_uSec - aMinutesAgo * kUsecPerMin; + + return FormHistory.update({ + op: "add", + fieldname: name, + value: "dummy", + firstUsed: timestamp, + }); +} + +/** + * Checks if a form entry exists. + */ +async function formNameExists(name) { + return !!(await FormHistory.count({ fieldname: name })); +} + +/** + * Ensures that the given pref is the expected value. + * + * @param aPrefName + * The pref's sub-branch under the privacy branch + * @param aExpectedVal + * The pref's expected value + * @param aMsg + * Passed to is() + */ +function boolPrefIs(aPrefName, aExpectedVal, aMsg) { + is(Services.prefs.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Checks to see if the download with the specified path exists. + * + * @param aPath + * The path of the download to check + * @return True if the download exists, false otherwise + */ +async function downloadExists(aPath) { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let listArray = await publicList.getAll(); + return listArray.some(i => i.target.path == aPath); +} + +/** + * Ensures that the specified downloads are either cleared or not. + * + * @param aDownloadIDs + * Array of download database IDs + * @param aShouldBeCleared + * True if each download should be cleared, false otherwise + */ +async function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) { + let niceStr = aShouldBeCleared ? "no longer" : "still"; + for (let id of aDownloadIDs) { + is( + await downloadExists(id), + !aShouldBeCleared, + "download " + id + " should " + niceStr + " exist" + ); + } +} + +/** + * Ensures that the given pref is the expected value. + * + * @param aPrefName + * The pref's sub-branch under the privacy branch + * @param aExpectedVal + * The pref's expected value + * @param aMsg + * Passed to is() + */ +function intPrefIs(aPrefName, aExpectedVal, aMsg) { + is(Services.prefs.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Creates a visit time. + * + * @param aMinutesAgo + * The visit will be visited this many minutes ago + */ +function visitTimeForMinutesAgo(aMinutesAgo) { + return now_uSec - aMinutesAgo * kUsecPerMin; +} diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js @@ -150,6 +150,9 @@ add_setup(async function () { await blankSlate(); await PlacesTestUtils.promiseAsyncUpdates(); }); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); // open preferences to trigger an updateSites() await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js @@ -26,6 +26,9 @@ add_setup(async function () { await PlacesTestUtils.promiseAsyncUpdates(); await SiteDataTestUtils.clear(); }); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); }); /** diff --git a/browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js b/browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js @@ -4,6 +4,12 @@ * 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/. */ +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); +}); + // Test checking "clear cookies and site data when firefox shuts down" does a migration // before making any pref changes (Bug 1894933) add_task(async function testMigrationForDeleteOnClose() { diff --git a/browser/base/jar.mn b/browser/base/jar.mn @@ -81,6 +81,7 @@ browser.jar: content/browser/safeMode.css (content/safeMode.css) content/browser/safeMode.js (content/safeMode.js) content/browser/safeMode.xhtml (content/safeMode.xhtml) + content/browser/sanitize.xhtml (content/sanitize.xhtml) content/browser/sanitize_v2.xhtml (content/sanitize_v2.xhtml) content/browser/sanitizeDialog.js (content/sanitizeDialog.js) content/browser/sanitizeDialog.css (content/sanitizeDialog.css) diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js @@ -0,0 +1,181 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests that requesting clear history at shutdown will really clear history. + */ + +const URIS = [ + "http://a.example1.com/", + "http://b.example1.com/", + "http://b.example2.com/", + "http://c.example3.com/", +]; + +const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/"; + +const { Sanitizer } = ChromeUtils.importESModule( + "resource:///modules/Sanitizer.sys.mjs" +); + +// Send the profile-after-change notification to the form history component to ensure +// that it has been initialized. +var formHistoryStartup = Cc[ + "@mozilla.org/satchel/form-history-startup;1" +].getService(Ci.nsIObserver); +formHistoryStartup.observe(null, "profile-after-change", null); +ChromeUtils.defineESModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", +}); + +var timeInMicroseconds = Date.now() * 1000; + +add_task(async function test_execute() { + info("Avoiding full places initialization importing default bookmarks."); + let { PlacesBrowserStartup } = ChromeUtils.importESModule( + "moz-src:///browser/components/places/PlacesBrowserStartup.sys.mjs" + ); + PlacesBrowserStartup.willImportDefaultBookmarks(); + + Sanitizer.onStartup(); + + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cache", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "offlineApps", + true + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "history", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "downloads", + true + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "sessions", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings", + true + ); + + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true); + + info("Add visits."); + for (let aUrl of URIS) { + await PlacesTestUtils.addVisits({ + uri: uri(aUrl), + visitDate: timeInMicroseconds++, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + info("Add cache."); + await storeCache(FTP_URL, "testData"); + info("Add form history."); + await addFormHistory(); + Assert.equal(await getFormHistoryCount(), 1, "Added form history"); + + info("Simulate and wait shutdown."); + await shutdownPlaces(); + + Assert.equal(await getFormHistoryCount(), 0, "Form history cleared"); + + let stmt = DBConn(true).createStatement( + "SELECT id FROM moz_places WHERE url = :page_url " + ); + + try { + URIS.forEach(function (aUrl) { + stmt.params.page_url = aUrl; + Assert.ok(!stmt.executeStep()); + stmt.reset(); + }); + } finally { + stmt.finalize(); + } + + info("Check cache"); + // Check cache. + await checkCache(FTP_URL); +}); + +function addFormHistory() { + let now = Date.now() * 1000; + return FormHistory.update({ + op: "add", + fieldname: "testfield", + value: "test", + timesUsed: 1, + firstUsed: now, + lastUsed: now, + }); +} + +async function getFormHistoryCount() { + return FormHistory.count({ fieldname: "testfield" }); +} + +function storeCache(aURL, aContent) { + let cache = Services.cache2; + let storage = cache.diskCacheStorage(Services.loadContextInfo.default); + + return new Promise(resolve => { + let storeCacheListener = { + onCacheEntryCheck() { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, status) { + Assert.equal(status, Cr.NS_OK); + + entry.setMetaDataElement("servertype", "0"); + var os = entry.openOutputStream(0, -1); + + var written = os.write(aContent, aContent.length); + if (written != aContent.length) { + do_throw( + "os.write has not written all data!\n" + + " Expected: " + + written + + "\n" + + " Actual: " + + aContent.length + + "\n" + ); + } + os.close(); + resolve(); + }, + }; + + storage.asyncOpenURI( + Services.io.newURI(aURL), + "", + Ci.nsICacheStorage.OPEN_NORMALLY, + storeCacheListener + ); + }); +} + +function checkCache(aURL) { + let cache = Services.cache2; + let storage = cache.diskCacheStorage(Services.loadContextInfo.default); + + return new Promise(resolve => { + let checkCacheListener = { + onCacheEntryAvailable(entry, isnew, status) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + resolve(); + }, + }; + + storage.asyncOpenURI( + Services.io.newURI(aURL), + "", + Ci.nsICacheStorage.OPEN_READONLY, + checkCacheListener + ); + }); +} diff --git a/browser/components/places/tests/unit/xpcshell.toml b/browser/components/places/tests/unit/xpcshell.toml @@ -33,7 +33,11 @@ support-files = [ ["test_browserGlue_restore.js"] +["test_clearHistory_shutdown.js"] +prefs = ["privacy.sanitize.useOldClearHistoryDialog=true"] + ["test_clearHistory_shutdown_v2.js"] +prefs = ["privacy.sanitize.useOldClearHistoryDialog=false"] ["test_interactions_blocklist.js"] diff --git a/browser/components/preferences/dialogs/clearSiteData.css b/browser/components/preferences/dialogs/clearSiteData.css @@ -0,0 +1,20 @@ +/* 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/. */ + +.options-container { + background-color: var(--background-color-box); + border: 1px solid var(--border-color); + border-radius: 2px; + color: var(--text-color); + padding: 0.5em; +} + +.option { + padding-bottom: 8px; +} + +.option-description { + color: var(--text-color-deemphasized); + margin-top: -0.5em !important; +} diff --git a/browser/components/preferences/dialogs/clearSiteData.js b/browser/components/preferences/dialogs/clearSiteData.js @@ -0,0 +1,100 @@ +/* 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/. */ + +const { SiteDataManager } = ChromeUtils.importESModule( + "resource:///modules/SiteDataManager.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", +}); + +var gClearSiteDataDialog = { + _clearSiteDataCheckbox: null, + _clearCacheCheckbox: null, + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this._dialog = document.querySelector("dialog"); + this._clearSiteDataCheckbox = document.getElementById("clearSiteData"); + this._clearCacheCheckbox = document.getElementById("clearCache"); + + // We'll block init() on this because the result values may impact + // subdialog sizing. + await Promise.all([ + SiteDataManager.getTotalUsage().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearSiteDataCheckbox, + "clear-site-data-cookies-with-data", + { amount, unit } + ); + }), + SiteDataManager.getCacheSize().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearCacheCheckbox, + "clear-site-data-cache-with-data", + { amount, unit } + ); + }), + ]); + await document.l10n.translateElements([ + this._clearCacheCheckbox, + this._clearSiteDataCheckbox, + ]); + + document.addEventListener("dialogaccept", event => this.onClear(event)); + + this._clearSiteDataCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + this._clearCacheCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + + document + .getElementById("key_close") + .addEventListener("command", () => window.close()); + }, + + onCheckboxCommand() { + this._dialog.toggleAttribute( + "buttondisabledaccept", + !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked) + ); + }, + + onClear(event) { + let clearSiteData = this._clearSiteDataCheckbox.checked; + let clearCache = this._clearCacheCheckbox.checked; + + if (clearSiteData) { + // Ask for confirmation before clearing site data + if (!SiteDataManager.promptSiteDataRemoval(window)) { + clearSiteData = false; + // Prevent closing the dialog when the data removal wasn't allowed. + event.preventDefault(); + } + } + + if (clearSiteData) { + SiteDataManager.removeSiteData(); + } + if (clearCache) { + SiteDataManager.removeCache(); + + // If we're not clearing site data, we need to tell the + // SiteDataManager to signal that it's updating. + if (!clearSiteData) { + SiteDataManager.updateSites(); + } + } + }, +}; + +window.addEventListener("load", () => gClearSiteDataDialog.onLoad()); diff --git a/browser/components/preferences/dialogs/clearSiteData.xhtml b/browser/components/preferences/dialogs/clearSiteData.xhtml @@ -0,0 +1,78 @@ +<?xml version="1.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/. --> + +<?csp default-src chrome:; style-src chrome: 'unsafe-inline'; ?> + +<window + id="ClearSiteDataDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="clear-site-data-window2" + data-l10n-attrs="title, style" + persist="width height" +> + <dialog + buttons="accept,cancel" + data-l10n-id="clear-site-data-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/preferences.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/content/preferences/dialogs/clearSiteData.css" + /> + + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/clearSiteData.ftl" + /> + </linkset> + <script src="chrome://browser/content/preferences/dialogs/clearSiteData.js" /> + + <keyset> + <key + id="key_close" + data-l10n-id="clear-site-data-close-key" + modifiers="accel" + /> + </keyset> + + <vbox class="contentPane"> + <description control="url" data-l10n-id="clear-site-data-description" /> + <separator class="thin" /> + <vbox class="options-container"> + <vbox class="option"> + <checkbox + data-l10n-id="clear-site-data-cookies-empty" + id="clearSiteData" + checked="true" + /> + <description + class="option-description indent" + data-l10n-id="clear-site-data-cookies-info" + /> + </vbox> + <vbox class="option"> + <checkbox + data-l10n-id="clear-site-data-cache-empty" + id="clearCache" + checked="true" + /> + <description + class="option-description indent" + data-l10n-id="clear-site-data-cache-info" + /> + </vbox> + </vbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn @@ -7,6 +7,9 @@ browser.jar: content/browser/preferences/dialogs/applicationManager.js content/browser/preferences/dialogs/browserLanguages.xhtml content/browser/preferences/dialogs/browserLanguages.js + content/browser/preferences/dialogs/clearSiteData.css + content/browser/preferences/dialogs/clearSiteData.js + content/browser/preferences/dialogs/clearSiteData.xhtml content/browser/preferences/dialogs/colors.css content/browser/preferences/dialogs/colors.xhtml content/browser/preferences/dialogs/colors.js @@ -26,6 +29,8 @@ browser.jar: content/browser/preferences/dialogs/containers.xhtml content/browser/preferences/dialogs/containers.js content/browser/preferences/dialogs/permissions.js + content/browser/preferences/dialogs/sanitize.xhtml + content/browser/preferences/dialogs/sanitize.js content/browser/preferences/dialogs/selectBookmark.xhtml content/browser/preferences/dialogs/selectBookmark.js content/browser/preferences/dialogs/siteDataSettings.xhtml diff --git a/browser/components/preferences/dialogs/sanitize.js b/browser/components/preferences/dialogs/sanitize.js @@ -0,0 +1,34 @@ +/* -*- 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/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "privacy.clearOnShutdown.history", type: "bool" }, + { id: "privacy.clearOnShutdown.formdata", type: "bool" }, + { id: "privacy.clearOnShutdown.downloads", type: "bool" }, + { id: "privacy.clearOnShutdown.cookies", type: "bool" }, + { id: "privacy.clearOnShutdown.cache", type: "bool" }, + { id: "privacy.clearOnShutdown.offlineApps", type: "bool" }, + { id: "privacy.clearOnShutdown.sessions", type: "bool" }, + { id: "privacy.clearOnShutdown.siteSettings", type: "bool" }, +]); + +var gSanitizeDialog = Object.freeze({ + init() { + this.onClearHistoryChanged(); + + Preferences.get("privacy.clearOnShutdown.history").on( + "change", + this.onClearHistoryChanged.bind(this) + ); + }, + + onClearHistoryChanged() { + let downloadsPref = Preferences.get("privacy.clearOnShutdown.downloads"); + let historyPref = Preferences.get("privacy.clearOnShutdown.history"); + downloadsPref.value = historyPref.value; + }, +}); diff --git a/browser/components/preferences/dialogs/sanitize.xhtml b/browser/components/preferences/dialogs/sanitize.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. --> + +<!DOCTYPE window> + +<window + id="SanitizeDialog" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="lastSelected" + data-l10n-id="sanitize-prefs2" + data-l10n-attrs="style" + onload="gSanitizeDialog.init();" +> + <dialog buttons="accept,cancel"> + <linkset> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/preferences.css" + /> + + <html:link rel="localization" href="browser/sanitize.ftl" /> + <html:link rel="localization" href="branding/brand.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + + <keyset> + <key + data-l10n-id="window-close" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <script src="chrome://browser/content/preferences/dialogs/sanitize.js" /> + + <description data-l10n-id="clear-data-settings-label"></description> + + <groupbox> + <label><html:h2 data-l10n-id="history-section-label" /></label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-history-and-downloads" + preference="privacy.clearOnShutdown.history" + /> + <checkbox + data-l10n-id="item-active-logins" + preference="privacy.clearOnShutdown.sessions" + /> + <checkbox + data-l10n-id="item-form-search-history" + preference="privacy.clearOnShutdown.formdata" + /> + </vbox> + <vbox> + <checkbox + data-l10n-id="item-cookies" + preference="privacy.clearOnShutdown.cookies" + /> + <checkbox + data-l10n-id="item-cache" + preference="privacy.clearOnShutdown.cache" + /> + </vbox> + </hbox> + </groupbox> + <groupbox> + <label><html:h2 data-l10n-id="data-section-label" /></label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-site-settings" + preference="privacy.clearOnShutdown.siteSettings" + /> + </vbox> + <vbox flex="1"> + <checkbox + data-l10n-id="item-offline-apps" + preference="privacy.clearOnShutdown.offlineApps" + /> + </vbox> + </hbox> + </groupbox> + </dialog> +</window> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -76,6 +76,13 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "useOldClearHistoryDialog", + "privacy.sanitize.useOldClearHistoryDialog", + false +); + ChromeUtils.defineESModuleGetters(this, { AppUpdater: "resource://gre/modules/AppUpdater.sys.mjs", DoHConfigController: "moz-src:///toolkit/components/doh/DoHConfig.sys.mjs", @@ -1864,8 +1871,16 @@ Preferences.addSetting( }; }, onUserClick() { + let uri; + if (useOldClearHistoryDialog) { + uri = + "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"; + } else { + uri = "chrome://browser/content/sanitize_v2.xhtml"; + } + gSubDialog.open( - "chrome://browser/content/sanitize_v2.xhtml", + uri, { features: "resizable=no", }, @@ -1939,6 +1954,16 @@ Preferences.addSetting({ }); function isCookiesAndStorageClearingOnShutdown() { + // We have to branch between the old clear on shutdown prefs and new prefs after the clear history revamp (Bug 1853996) + // Once the old dialog is deprecated, we can remove these branches. + if (useOldClearHistoryDialog) { + return ( + Preferences.get("privacy.sanitize.sanitizeOnShutdown").value && + Preferences.get("privacy.clearOnShutdown.cookies").value && + Preferences.get("privacy.clearOnShutdown.cache").value && + Preferences.get("privacy.clearOnShutdown.offlineApps").value + ); + } return ( Preferences.get("privacy.sanitize.sanitizeOnShutdown").value && Preferences.get("privacy.clearOnShutdown_v2.cookiesAndStorage").value && @@ -1950,22 +1975,32 @@ function isCookiesAndStorageClearingOnShutdown() { * Unsets cleaning prefs that do not belong to DeleteOnClose */ function resetCleaningPrefs() { - return SANITIZE_ON_SHUTDOWN_PREFS_ONLY_V2.forEach( + let sanitizeOnShutdownPrefsArray = useOldClearHistoryDialog + ? SANITIZE_ON_SHUTDOWN_PREFS_ONLY + : SANITIZE_ON_SHUTDOWN_PREFS_ONLY_V2; + + return sanitizeOnShutdownPrefsArray.forEach( pref => (Preferences.get(pref).value = false) ); } Preferences.addSetting({ id: "clearOnCloseCookies", - pref: "privacy.clearOnShutdown_v2.cookiesAndStorage", + pref: useOldClearHistoryDialog + ? "privacy.clearOnShutdown.cookies" + : "privacy.clearOnShutdown_v2.cookiesAndStorage", }); Preferences.addSetting({ id: "clearOnCloseCache", - pref: "privacy.clearOnShutdown_v2.cache", + pref: useOldClearHistoryDialog + ? "privacy.clearOnShutdown.cache" + : "privacy.clearOnShutdown_v2.cache", }); Preferences.addSetting({ id: "clearOnCloseStorage", - pref: "privacy.clearOnShutdown_v2.cookiesAndStorage", + pref: useOldClearHistoryDialog + ? "privacy.clearOnShutdown.offlineApps" + : "privacy.clearOnShutdown_v2.cookiesAndStorage", }); Preferences.addSetting({ id: "sanitizeOnShutdown", @@ -2253,8 +2288,12 @@ Preferences.addSetting({ return !alwaysClear.value || alwaysClear.disabled; }, onUserClick() { + let dialogFile = useOldClearHistoryDialog + ? "chrome://browser/content/preferences/dialogs/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; + gSubDialog.open( - "chrome://browser/content/sanitize_v2.xhtml", + dialogFile, { features: "resizable=no", }, @@ -4587,7 +4626,9 @@ var gPrivacyPane = { * Displays the Clear Private Data settings dialog. */ showClearPrivateDataSettings() { - let dialogFile = "chrome://browser/content/sanitize_v2.xhtml"; + let dialogFile = useOldClearHistoryDialog + ? "chrome://browser/content/preferences/dialogs/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; gSubDialog.open( dialogFile, @@ -4612,7 +4653,10 @@ var gPrivacyPane = { ts.value = 0; } - let dialogFile = "chrome://browser/content/sanitize_v2.xhtml"; + // Bug 1856418 We intend to remove the old dialog box + let dialogFile = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; gSubDialog.open(dialogFile, { features: "resizable=no", @@ -4631,7 +4675,9 @@ var gPrivacyPane = { Checks if the user set cleaning prefs that do not belong to DeleteOnClose */ _isCustomCleaningPrefPresent() { - let sanitizeOnShutdownPrefsArray = SANITIZE_ON_SHUTDOWN_PREFS_ONLY_V2; + let sanitizeOnShutdownPrefsArray = useOldClearHistoryDialog + ? SANITIZE_ON_SHUTDOWN_PREFS_ONLY + : SANITIZE_ON_SHUTDOWN_PREFS_ONLY_V2; return sanitizeOnShutdownPrefsArray.some( pref => Preferences.get(pref).value diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml @@ -243,6 +243,8 @@ skip-if = [ ["browser_privacy_status_card.js"] +["browser_privacy_syncDataClearing.js"] + ["browser_privacy_syncDataClearing_v2.js"] ["browser_privacy_uploadEnabled.js"] diff --git a/browser/components/preferences/tests/browser_privacy_syncDataClearing.js b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js @@ -0,0 +1,303 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * With no custom cleaning categories set and sanitizeOnShutdown disabled, + * the checkboxes "alwaysClear" and "deleteOnClose" should share the same state. + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", true]], + }); +}); + +add_task(async function test_syncWithoutCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + deleteOnCloseBox.click(); + + // Wait for UI to update. + await new Promise(resolve => requestAnimationFrame(resolve)); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnClose sets alwaysClear in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + deleteOnCloseBox.click(); + + // Wait for UI to update. + await new Promise(resolve => requestAnimationFrame(resolve)); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnclose sets alwaysClear in the same state, deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.prefs.clearUserPref("privacy.clearOnShutdown.downloads"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.offlineApps"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cache"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cookies"); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); +}); + +/* + * With custom cleaning category already set and SanitizeOnShutdown enabled, + * deselecting "deleteOnClose" should not change the state of "alwaysClear". + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_task(async function test_syncWithCustomPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.history", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(alwaysClearBox.checked, "AlwaysClear initial state is selected"); + + deleteOnCloseBox.click(); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + + deleteOnCloseBox.click(); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + !deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear is not synced with deleteOnClose, only deleteOnClose is deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * Setting/resetting cleaning prefs for cookies, cache, offline apps + * and selecting/deselecting the "alwaysClear" Box, also selects/deselects + * the "deleteOnClose" box. + */ + +add_task(async function test_syncWithCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + // Make sure custom is selected so the depending checkboxes are visible. + ["privacy.history.custom", true], + ], + }); + + ok(alwaysClearBox.checked, "AlwaysClear is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + + alwaysClearBox.click(); + + // Wait for UI to update. + await new Promise(resolve => requestAnimationFrame(resolve)); + + ok(!alwaysClearBox.checked, "AlwaysClear is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * On loading the page, the ClearOnClose box should be set according to the pref selection + */ +add_task(async function test_initialState() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, selected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", true], + ["privacy.clearOnShutdown.history", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + !deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // When private browsing mode autostart is selected, the deleteOnClose Box is selected always + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", false], + ["browser.privatebrowsing.autostart", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the private Browsing autostart pref, selected" + ); + + // Reset history mode + await SpecialPowers.popPrefEnv(); + gBrowser.contentWindow.Preferences.getSetting("historyMode").value = + "remember"; + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_privacy_syncDataClearing_v2.js b/browser/components/preferences/tests/browser_privacy_syncDataClearing_v2.js @@ -8,7 +8,10 @@ */ add_setup(async function () { await SpecialPowers.pushPrefEnv({ - set: [["privacy.clearOnShutdown.cookies", true]], + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.sanitize.useOldClearHistoryDialog", false], + ], }); }); diff --git a/browser/components/preferences/tests/siteData/browser.toml b/browser/components/preferences/tests/siteData/browser.toml @@ -8,6 +8,8 @@ support-files = [ "offline/manifest.appcache", ] +["browser_clearSiteData.js"] + ["browser_clearSiteData_v2.js"] ["browser_siteData.js"] diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js @@ -0,0 +1,234 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +async function testClearData(clearSiteData, clearCache) { + PermissionTestUtils.add( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + + // Open a test site which saves into appcache. + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Fill indexedDB with test data. + // Don't wait for the page to load, to register the content event handler as quickly as possible. + // If this test goes intermittent, we might have to tell the page to wait longer before + // firing the event. + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Register some service workers. + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Test the initial states. + let cacheUsage = await SiteDataManager.getCacheSize(); + let quotaUsage = await SiteDataTestUtils.getQuotaUsage( + TEST_QUOTA_USAGE_ORIGIN + ); + let totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(cacheUsage, 0, "The cache usage should not be 0"); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + + let initialSizeLabelValue = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let siteDataSizeItem = content.document.getElementById("siteDataSize"); + // The <strong> element contains the data size. + let usage = siteDataSizeItem.querySelector("strong"); + return usage.textContent; + } + ); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); + + let url = "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"; + let dialogOpened = promiseLoadSubDialog(url); + clearSiteDataButton.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + clearSiteDataButton.buttonEl, + {}, + doc.ownerGlobal + ); + let dialogWin = await dialogOpened; + + // Convert the usage numbers in the same way the UI does it to assert + // that they're displayed in the dialog. + let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage); + // For cache we just assert that the right unit (KB, probably) is displayed, + // since we've had cache intermittently changing under our feet. + let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); + + let cookiesCheckboxId = "clearSiteData"; + let cacheCheckboxId = "clearCache"; + let clearSiteDataCheckbox = + dialogWin.document.getElementById(cookiesCheckboxId); + let clearCacheCheckbox = dialogWin.document.getElementById(cacheCheckboxId); + // The usage details are filled asynchronously, so we assert that they're present by + // waiting for them to be filled in. + await Promise.all([ + TestUtils.waitForCondition( + () => + clearSiteDataCheckbox.label && + clearSiteDataCheckbox.label.includes(convertedTotalUsage), + "Should show the quota usage" + ), + TestUtils.waitForCondition( + () => + clearCacheCheckbox.label && + clearCacheCheckbox.label.includes(convertedCacheUnit), + "Should show the cache usage" + ), + ]); + + // Check the boxes according to our test input. + clearSiteDataCheckbox.checked = clearSiteData; + clearCacheCheckbox.checked = clearCache; + + // Some additional promises/assertions to wait for + // when deleting site data. + let acceptPromise; + let updatePromise; + let cookiesClearedPromise; + if (clearSiteData) { + acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + updatePromise = promiseSiteDataManagerSitesUpdated(); + cookiesClearedPromise = promiseCookiesCleared(); + } + + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + + let clearButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + if (!clearSiteData && !clearCache) { + // Simulate user input on one of the checkboxes to trigger the event listener for + // disabling the clearButton. + clearCacheCheckbox.doCommand(); + // Check that the clearButton gets disabled by unchecking both options. + await TestUtils.waitForCondition( + () => clearButton.disabled, + "Clear button should be disabled" + ); + let cancelButton = dialogWin.document + .querySelector("dialog") + .getButton("cancel"); + // Cancel, since we can't delete anything. + cancelButton.click(); + } else { + // Delete stuff! + clearButton.click(); + } + + // For site data we display an extra warning dialog, make sure + // to accept it. + if (clearSiteData) { + await acceptPromise; + } + + await dialogClosed; + + if (clearCache) { + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getCacheSize(); + return usage == 0; + }, "The cache usage should be removed"); + } else { + Assert.greater( + await SiteDataManager.getCacheSize(), + 0, + "The cache usage should not be 0" + ); + } + + if (clearSiteData) { + await updatePromise; + await cookiesClearedPromise; + await promiseServiceWorkersCleared(); + + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getTotalUsage(); + return usage == 0; + }, "The total usage should be removed"); + } else { + quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN); + totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + } + + if (clearCache || clearSiteData) { + // Check that the size label in about:preferences updates after we cleared data. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ initialSizeLabelValue }], + async function (opts) { + let siteDataSizeItem = content.document.getElementById("siteDataSize"); + let usage = siteDataSizeItem.querySelector("strong"); + let siteDataSizeText = usage.textContent; + await ContentTaskUtils.waitForCondition(() => { + return siteDataSizeText != opts.initialSizeLabelValue; + }, "Site data size label should have updated."); + } + ); + } + + let permission = PermissionTestUtils.getPermissionObject( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage" + ); + is( + clearSiteData ? permission : permission.capability, + clearSiteData ? null : Services.perms.ALLOW_ACTION, + "Should have the correct permission state." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SiteDataManager.removeAll(); +} + +add_setup(function () { + SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", true]], + }); +}); + +// Test opening the "Clear All Data" dialog and cancelling. +add_task(async function () { + await testClearData(false, false); +}); + +// Test opening the "Clear All Data" dialog and removing all site data. +add_task(async function () { + await testClearData(true, false); +}); + +// Test opening the "Clear All Data" dialog and removing all cache. +add_task(async function () { + await testClearData(false, true); +}); + +// Test opening the "Clear All Data" dialog and removing everything. +add_task(async function () { + await testClearData(true, true); +}); diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js b/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js @@ -195,6 +195,10 @@ async function testClearData(clearSiteData, clearCache) { } add_setup(function () { + SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); + // The tests in this file all test specific interactions with the new clear // history dialog and can't be split up. requestLongerTimeout(2); diff --git a/browser/components/urlbar/tests/browser-tips/browser_interventions.js b/browser/components/urlbar/tests/browser-tips/browser_interventions.js @@ -41,19 +41,21 @@ add_task(async function refresh() { add_task(async function clear() { // Pick the tip, which should open the refresh dialog. Click its cancel // button. + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; await checkIntervention({ searchString: SEARCH_STRINGS.CLEAR, tip: UrlbarProviderInterventions.TIP_TYPE.CLEAR, title: "Clear your cache, cookies, history and more.", button: "Choose What to Clear…", awaitCallback() { - return BrowserTestUtils.promiseAlertDialog( - "cancel", - "chrome://browser/content/sanitize_v2.xhtml", - { - isSubDialog: true, - } - ); + return BrowserTestUtils.promiseAlertDialog("cancel", dialogURL, { + isSubDialog: true, + }); }, }); }); diff --git a/browser/components/urlbar/tests/browser/browser_quickactions.js b/browser/components/urlbar/tests/browser/browser_quickactions.js @@ -232,9 +232,15 @@ add_task(async function test_refresh() { }); add_task(async function test_clear() { + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; await doAlertDialogTest({ input: "clear", - dialogContentURI: "chrome://browser/content/sanitize_v2.xhtml", + dialogContentURI: dialogURL, }); }); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js @@ -91,10 +91,16 @@ add_task(async function selected_result_tip() { }); add_task(async function selected_result_intervention_clear() { + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; await doInterventionTest( SEARCH_STRINGS.CLEAR, "intervention_clear", - "chrome://browser/content/sanitize_v2.xhtml", + dialogURL, [ { selected_result: "intervention_clear", diff --git a/browser/modules/Sanitizer.sys.mjs b/browser/modules/Sanitizer.sys.mjs @@ -3,6 +3,7 @@ * 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/. */ +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; @@ -15,6 +16,13 @@ ChromeUtils.defineESModuleGetters(lazy, { PrincipalsCollector: "resource://gre/modules/PrincipalsCollector.sys.mjs", }); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "useOldClearHistoryDialog", + "privacy.sanitize.useOldClearHistoryDialog", + false +); + var logConsole; function log(...msgs) { if (!logConsole) { @@ -50,7 +58,15 @@ export var Sanitizer = { * Pref branches to fetch sanitization options from. */ PREF_CPD_BRANCH: "privacy.cpd.", - PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown_v2.", + /* + * We need to choose between two branches for shutdown since there are separate prefs for the new + * clear history dialog + */ + get PREF_SHUTDOWN_BRANCH() { + return lazy.useOldClearHistoryDialog + ? "privacy.clearOnShutdown." + : "privacy.clearOnShutdown_v2."; + }, /** * The fallback timestamp used when no argument is given to @@ -126,7 +142,9 @@ export var Sanitizer = { parentWindow = null; } - let dialogFile = "sanitize_v2.xhtml"; + let dialogFile = lazy.useOldClearHistoryDialog + ? "sanitize.xhtml" + : "sanitize_v2.xhtml"; if (parentWindow?.gDialogBox) { parentWindow.gDialogBox.open(`chrome://browser/content/${dialogFile}`, { @@ -1002,31 +1020,66 @@ async function sanitizeInternal(items, aItemsToClear, options) { async function sanitizeOnShutdown(progress) { log("Sanitizing on shutdown"); - // Perform a migration if this is the first time sanitizeOnShutdown is - // running for the user with the new dialog - Sanitizer.maybeMigratePrefs("clearOnShutdown"); - - progress.sanitizationPrefs = { - privacy_sanitize_sanitizeOnShutdown: Services.prefs.getBoolPref( - "privacy.sanitize.sanitizeOnShutdown" - ), - privacy_clearOnShutdown_v2_cookiesAndStorage: Services.prefs.getBoolPref( - "privacy.clearOnShutdown_v2.cookiesAndStorage" - ), - privacy_clearOnShutdown_v2_browsingHistoryAndDownloads: - Services.prefs.getBoolPref( - "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads" + if (lazy.useOldClearHistoryDialog) { + progress.sanitizationPrefs = { + privacy_sanitize_sanitizeOnShutdown: Services.prefs.getBoolPref( + "privacy.sanitize.sanitizeOnShutdown" ), - privacy_clearOnShutdown_v2_cache: Services.prefs.getBoolPref( - "privacy.clearOnShutdown_v2.cache" - ), - privacy_clearOnShutdown_v2_formdata: Services.prefs.getBoolPref( - "privacy.clearOnShutdown_v2.formdata" - ), - privacy_clearOnShutdown_v2_siteSettings: Services.prefs.getBoolPref( - "privacy.clearOnShutdown_v2.siteSettings" - ), - }; + privacy_clearOnShutdown_cookies: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.cookies" + ), + privacy_clearOnShutdown_history: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.history" + ), + privacy_clearOnShutdown_formdata: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.formdata" + ), + privacy_clearOnShutdown_downloads: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.downloads" + ), + privacy_clearOnShutdown_cache: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.cache" + ), + privacy_clearOnShutdown_sessions: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.sessions" + ), + privacy_clearOnShutdown_offlineApps: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.offlineApps" + ), + privacy_clearOnShutdown_siteSettings: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.siteSettings" + ), + privacy_clearOnShutdown_openWindows: Services.prefs.getBoolPref( + "privacy.clearOnShutdown.openWindows" + ), + }; + } else { + // Perform a migration if this is the first time sanitizeOnShutdown is + // running for the user with the new dialog + Sanitizer.maybeMigratePrefs("clearOnShutdown"); + + progress.sanitizationPrefs = { + privacy_sanitize_sanitizeOnShutdown: Services.prefs.getBoolPref( + "privacy.sanitize.sanitizeOnShutdown" + ), + privacy_clearOnShutdown_v2_cookiesAndStorage: Services.prefs.getBoolPref( + "privacy.clearOnShutdown_v2.cookiesAndStorage" + ), + privacy_clearOnShutdown_v2_browsingHistoryAndDownloads: + Services.prefs.getBoolPref( + "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads" + ), + privacy_clearOnShutdown_v2_cache: Services.prefs.getBoolPref( + "privacy.clearOnShutdown_v2.cache" + ), + privacy_clearOnShutdown_v2_formdata: Services.prefs.getBoolPref( + "privacy.clearOnShutdown_v2.formdata" + ), + privacy_clearOnShutdown_v2_siteSettings: Services.prefs.getBoolPref( + "privacy.clearOnShutdown_v2.siteSettings" + ), + }; + } let needsSyncSavePrefs = false; if (Sanitizer.shouldSanitizeOnShutdown) { diff --git a/browser/modules/test/unit/test_Sanitizer_interrupted.js b/browser/modules/test/unit/test_Sanitizer_interrupted.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +do_get_profile(); + +// Test that interrupted sanitizations are properly tracked. + +add_task(async function () { + const { Sanitizer } = ChromeUtils.importESModule( + "resource:///modules/Sanitizer.sys.mjs" + ); + + Services.prefs.setBoolPref(Sanitizer.PREF_NEWTAB_SEGREGATION, false); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN); + Services.prefs.clearUserPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata"); + Services.prefs.clearUserPref(Sanitizer.PREF_NEWTAB_SEGREGATION); + }); + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", true); + + await Sanitizer.onStartup(); + Assert.ok(Sanitizer.shouldSanitizeOnShutdown, "Should sanitize on shutdown"); + + let pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 1, + "Should have 1 pending sanitization" + ); + Assert.equal( + pendingSanitizations[0].id, + "shutdown", + "Should be the shutdown sanitization" + ); + Assert.ok( + pendingSanitizations[0].itemsToClear.includes("formdata"), + "Pref has been setup" + ); + Assert.ok( + !pendingSanitizations[0].options.isShutdown, + "Shutdown option is not present" + ); + + // Check the preference listeners. + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false); + pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 0, + "Should not have pending sanitizations" + ); + Assert.ok( + !Sanitizer.shouldSanitizeOnShutdown, + "Should not sanitize on shutdown" + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true); + pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 1, + "Should have 1 pending sanitization" + ); + Assert.equal( + pendingSanitizations[0].id, + "shutdown", + "Should be the shutdown sanitization" + ); + + Assert.ok( + pendingSanitizations[0].itemsToClear.includes("formdata"), + "Pending sanitizations should include formdata" + ); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", + false + ); + pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 1, + "Should have 1 pending sanitization" + ); + Assert.ok( + !pendingSanitizations[0].itemsToClear.includes("formdata"), + "Pending sanitizations should have been updated" + ); + + // Check a sanitization properly rebuilds the pref. + await Sanitizer.sanitize(["formdata"]); + pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 1, + "Should have 1 pending sanitization" + ); + Assert.equal( + pendingSanitizations[0].id, + "shutdown", + "Should be the shutdown sanitization" + ); + + // Startup should run the pending one and setup a new shutdown sanitization. + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", + false + ); + await Sanitizer.onStartup(); + pendingSanitizations = JSON.parse( + Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]") + ); + Assert.equal( + pendingSanitizations.length, + 1, + "Should have 1 pending sanitization" + ); + Assert.equal( + pendingSanitizations[0].id, + "shutdown", + "Should be the shutdown sanitization" + ); + Assert.ok( + !pendingSanitizations[0].itemsToClear.includes("formdata"), + "Pref has been setup" + ); +}); diff --git a/browser/modules/test/unit/xpcshell.toml b/browser/modules/test/unit/xpcshell.toml @@ -48,7 +48,11 @@ run-if = [ "os == 'win'", # Test of a Windows-specific feature ] +["test_Sanitizer_interrupted.js"] +prefs = ["privacy.sanitize.useOldClearHistoryDialog=true"] + ["test_Sanitizer_interrupted_v2.js"] +prefs = ["privacy.sanitize.useOldClearHistoryDialog=false"] ["test_SiteDataManager.js"] diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -16771,6 +16771,11 @@ value: 900 mirror: always +- name: privacy.sanitize.useOldClearHistoryDialog + type: RelaxedAtomicBool + value: true + mirror: always + - name: privacy.sanitize.sanitizeOnShutdown type: RelaxedAtomicBool value: false diff --git a/netwerk/cache2/CacheObserver.h b/netwerk/cache2/CacheObserver.h @@ -75,6 +75,10 @@ class CacheObserver : public nsIObserver, public nsSupportsWeakReference { if (!StaticPrefs::privacy_sanitize_sanitizeOnShutdown()) { return false; } + if (StaticPrefs::privacy_sanitize_useOldClearHistoryDialog()) { + return StaticPrefs::privacy_clearOnShutdown_cache(); + } + // use the new cache clearing pref for the new clear history dialog return StaticPrefs::privacy_clearOnShutdown_v2_cache(); } static void ParentDirOverride(nsIFile** aDir); diff --git a/security/manager/ssl/tests/unit/test_sss_sanitizeOnShutdown.js b/security/manager/ssl/tests/unit/test_sss_sanitizeOnShutdown.js @@ -0,0 +1,59 @@ +/* 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"; + +// The purpose of this test is to ensure that Firefox sanitizes site security +// service data on shutdown if configured to do so. + +ChromeUtils.defineESModuleGetters(this, { + Sanitizer: "resource:///modules/Sanitizer.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +Sanitizer.onStartup(); + +// This helps us away from test timed out. If service worker manager(swm) hasn't +// been initilaized before profile-change-teardown, this test would fail due to +// the shutdown blocker added by swm. Normally, swm should be initialized before +// that and the similar crash signatures are fixed. So, assume this cannot +// happen in the real world and initilaize swm here as a workaround. +Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +add_task(async function run_test() { + do_get_profile(); + let SSService = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + let header = "max-age=50000"; + SSService.processHeader(Services.io.newURI("https://example.com"), header); + await TestUtils.waitForCondition(() => { + let stateFileContents = get_data_storage_contents(SSS_STATE_FILE_NAME); + return stateFileContents + ? stateFileContents.includes("example.com") + : false; + }); + + // Configure Firefox to clear this data on shutdown. + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings", + true + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true); + + // Simulate shutdown. + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNTEARDOWN + ); + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN + ); + + await TestUtils.waitForCondition(() => { + let stateFile = do_get_profile(); + stateFile.append(SSS_STATE_FILE_NAME); + return !stateFile.exists(); + }); +}); diff --git a/security/manager/ssl/tests/unit/xpcshell.toml b/security/manager/ssl/tests/unit/xpcshell.toml @@ -353,8 +353,19 @@ run-if = [ ["test_sss_resetState.js"] +["test_sss_sanitizeOnShutdown.js"] +requesttimeoutfactor = 3 # Slow on condprof +prefs = ["privacy.sanitize.useOldClearHistoryDialog=true"] +firefox-appdir = "browser" +skip-if = [ + "appname == 'thunderbird'", # browser/modules/Sanitizer.sys.mjs used by the test isn't available in Thunderbird. + "os == 'android'", # Sanitization works differently on Android - this doesn't apply. + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && opt", # Bug 1839376 +] + ["test_sss_sanitizeOnShutdown_v2.js"] requesttimeoutfactor = 2 # Slow on condprof +prefs = ["privacy.sanitize.useOldClearHistoryDialog=false"] firefox-appdir = "browser" skip-if = [ "appname == 'thunderbird'", # browser/modules/Sanitizer.sys.mjs used by the test isn't available in Thunderbird. diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js @@ -427,6 +427,7 @@ module.exports = [ "browser/components/ipprotection/content/ipprotection-status-card.css", "browser/components/messagepreview/messagepreview.css", "browser/components/places/metadataViewer/interactionsViewer.css", + "browser/components/preferences/dialogs/clearSiteData.css", "browser/components/preferences/dialogs/sitePermissions.css", "browser/components/preferences/widgets/nav-notice/nav-notice.css", "browser/components/preferences/widgets/security-privacy/security-privacy-card/security-privacy-card.css",