From 617efd9bd8938115d921521ac32938ef5c8add60 Mon Sep 17 00:00:00 2001 From: Paul Zuehlcke Date: Mon, 18 Nov 2024 11:40:02 +0000 Subject: [PATCH] Bug 1926591 - PreferencesCleaner support for OriginAttributesPattern. r=mak Differential Revision: https://phabricator.services.mozilla.com/D221783 --- .../base/nsIContentPrefService2.idl | 41 +- .../cleardata/ClearDataService.sys.mjs | 79 ++- .../tests/unit/test_content_preferences.js | 371 ++++++++++++ .../cleardata/tests/unit/xpcshell.toml | 2 + .../contentprefs/ContentPrefService2.sys.mjs | 558 +++++++++++------- .../tests/unit_cps2/test_remove.js | 78 ++- .../tests/unit_cps2/test_removeAllDomains.js | 3 +- .../unit_cps2/test_removeAllDomainsSince.js | 60 +- .../tests/unit_cps2/test_removeByDomain.js | 65 +- .../tests/unit_cps2/test_removeByName.js | 103 +++- .../mozapps/downloads/DownloadLastDir.sys.mjs | 24 +- 11 files changed, 1128 insertions(+), 256 deletions(-) create mode 100644 toolkit/components/cleardata/tests/unit/test_content_preferences.js diff --git a/dom/interfaces/base/nsIContentPrefService2.idl b/dom/interfaces/base/nsIContentPrefService2.idl index 41a93c6de6a9..a42a77396fda 100644 --- a/dom/interfaces/base/nsIContentPrefService2.idl +++ b/dom/interfaces/base/nsIContentPrefService2.idl @@ -87,6 +87,11 @@ interface nsIContentPrefObserver : nsISupports * window or channel whose content pertains to the preferences being modified or * retrieved. * + * The remove methods, used to clear prefs, also accept an optional + * nsILoadContext. If you pass null, both private and normal browsing data will + * be removed. Passing a normal browsing context will remove only normal + * browsing data, and passing a private browsing context will remove only + * private browsing data. * * Callbacks * @@ -255,7 +260,9 @@ interface nsIContentPrefService2 : nsISupports * * @param domain The preference's domain. * @param name The preference's name. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeByDomainAndName(in AString domain, @@ -269,7 +276,9 @@ interface nsIContentPrefService2 : nsISupports * * @param domain The preferences' domain. * @param name The preferences' name. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeBySubdomainAndName(in AString domain, @@ -281,7 +290,9 @@ interface nsIContentPrefService2 : nsISupports * Removes the preference with no domain and the given name. * * @param name The preference's name. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeGlobal(in AString name, @@ -292,7 +303,9 @@ interface nsIContentPrefService2 : nsISupports * Removes all preferences with the given domain. * * @param domain The preferences' domain. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeByDomain(in AString domain, @@ -304,7 +317,9 @@ interface nsIContentPrefService2 : nsISupports * of the given domain. * * @param domain The preferences' domain. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeBySubdomain(in AString domain, @@ -316,7 +331,9 @@ interface nsIContentPrefService2 : nsISupports * global preferences with the given name. * * @param name The preferences' name. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeByName(in AString name, @@ -327,7 +344,9 @@ interface nsIContentPrefService2 : nsISupports * Removes all non-global preferences -- in other words, all preferences that * have a domain. * - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeAllDomains(in nsILoadContext context, @@ -337,7 +356,9 @@ interface nsIContentPrefService2 : nsISupports * Removes all non-global preferences created after and including |since|. * * @param since Timestamp in milliseconds. - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeAllDomainsSince(in unsigned long long since, @@ -348,7 +369,9 @@ interface nsIContentPrefService2 : nsISupports * Removes all global preferences -- in other words, all preferences that have * no domain. * - * @param context The private-browsing context, if any. + * @param [context] Optional context to pass to indicate whether normal or + * private-browsing data should be removed. Passing null + * removes both private and normal browsing data. * @param callback handleCompletion is called when the operation completes. */ void removeAllGlobals(in nsILoadContext context, diff --git a/toolkit/components/cleardata/ClearDataService.sys.mjs b/toolkit/components/cleardata/ClearDataService.sys.mjs index 66589ab75647..4e4d7d8c9e00 100644 --- a/toolkit/components/cleardata/ClearDataService.sys.mjs +++ b/toolkit/components/cleardata/ClearDataService.sys.mjs @@ -1546,13 +1546,26 @@ const PermissionsCleaner = { }; const PreferencesCleaner = { - deleteByHost(aHost) { + deleteByHost(aHost, aOriginAttributes = {}) { + aOriginAttributes = + ChromeUtils.fillNonDefaultOriginAttributes(aOriginAttributes); + + let loadContext; + if ( + aOriginAttributes.privateBrowsingId == + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID + ) { + loadContext = Cu.createLoadContext(); + } else { + loadContext = Cu.createPrivateLoadContext(); + } + // Also clears subdomains of aHost. return new Promise((aResolve, aReject) => { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); - cps2.removeBySubdomain(aHost, null, { + cps2.removeBySubdomain(aHost, loadContext, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); @@ -1560,7 +1573,6 @@ const PreferencesCleaner = { aResolve(); } }, - handleError() {}, }); }); }, @@ -1569,23 +1581,74 @@ const PreferencesCleaner = { return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes); }, - deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { - // TODO: aOriginAttributesPattern - return this.deleteByHost(aSchemelessSite, {}); + async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { + // If aOriginAttributesPattern does not specify private or normal browsing + // clear both. + let loadContext = null; + + // If the pattern filters by normal or private browsing mode only clear that mode. + if (aOriginAttributesPattern.privateBrowsingId != null) { + // The default private browsing ID is 0 which is non private browsing mode + // / normal mode. + let isPrivateBrowsing = + aOriginAttributesPattern.privateBrowsingId != + Ci.nsIScriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID; + loadContext = isPrivateBrowsing + ? Cu.createPrivateLoadContext() + : Cu.createLoadContext(); + } + + let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + await new Promise((aResolve, aReject) => { + cps2.removeBySubdomain(aSchemelessSite, loadContext, { + handleCompletion: aReason => { + if (aReason === cps2.COMPLETE_ERROR) { + aReject(); + } else { + aResolve(); + } + }, + }); + }); }, async deleteByRange(aFrom) { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); - cps2.removeAllDomainsSince(aFrom / 1000, null); + + await new Promise((aResolve, aReject) => { + cps2.removeAllDomainsSince(aFrom / 1000, null, { + handleCompletion: aReason => { + if (aReason === cps2.COMPLETE_ERROR) { + aReject(); + } else { + aResolve(); + } + }, + }); + }); }, async deleteAll() { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); - cps2.removeAllDomains(null); + + await new Promise((aResolve, aReject) => { + cps2.removeAllDomains(null, { + handleCompletion: aReason => { + if (aReason === cps2.COMPLETE_ERROR) { + aReject(); + } else { + aResolve(); + } + }, + }); + }); }, }; diff --git a/toolkit/components/cleardata/tests/unit/test_content_preferences.js b/toolkit/components/cleardata/tests/unit/test_content_preferences.js new file mode 100644 index 000000000000..38cdd3ba84e8 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_content_preferences.js @@ -0,0 +1,371 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 +); + +// Async wrappers around content pref service setter / getter. Would be nice if +// the service provided that itself. + +async function setContentPref(domain, name, value, isPBM) { + return new Promise((resolve, reject) => { + let loadContext = isPBM + ? Cu.createPrivateLoadContext() + : Cu.createLoadContext(); + cps2.set(domain, name, value, loadContext, { + handleCompletion: aReason => { + if (aReason === cps2.COMPLETE_ERROR) { + reject(new Error("Failed to set content pref")); + } else { + resolve(); + } + }, + }); + }); +} + +async function getContentPref(domain, name, isPBM) { + return new Promise((resolve, reject) => { + let loadContext = isPBM + ? Cu.createPrivateLoadContext() + : Cu.createLoadContext(); + let result; + cps2.getByDomainAndName(domain, name, loadContext, { + handleResult({ value }) { + result = value; + }, + handleCompletion(aReason) { + if (aReason === cps2.COMPLETE_ERROR) { + reject(new Error("Failed to get content pref")); + } else { + resolve(result); + } + }, + }); + }); +} + +add_task(async function test_deleteByHost() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "bar", "bar", true); + await setContentPref("foo.example.org", "bar", "bar", false); + await setContentPref("foo.example.org", "bar", "bar", true); + await setContentPref("bar.foo.example.org", "subsub", "subsub", false); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "bar", true), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", true), "bar"); + Assert.equal( + await getContentPref("bar.foo.example.org", "subsub", false), + "subsub" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "foo.example.org", + true, + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info( + "Verify content prefs matching host 'foo.example.org' have been cleared" + ); + Assert.equal( + await getContentPref("example.com", "foo", false), + "foo", + "Unrelated domain entry should still exist." + ); + Assert.equal( + await getContentPref("example.org", "bar", false), + "bar", + "Base domain entry should still exist." + ); + Assert.equal( + await getContentPref("example.org", "bar", true), + "bar", + "Base domain PBM entry should still exist." + ); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + undefined, + "Exact domain match should have been cleared in normal browsing." + ); + Assert.equal( + await getContentPref("foo.example.org", "bar", true), + "bar", + "Exact domain match should not have been cleared in private browsing." + ); + Assert.equal( + await getContentPref("bar.foo.example.org", "subsub", false), + undefined, + "Subdomain should have been cleared" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteByPrincipal() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "bar", "bar", true); + await setContentPref("foo.example.org", "bar", "bar", false); + await setContentPref("foo.example.org", "bar", "bar", true); + await setContentPref("bar.foo.example.org", "subsub", "subsub", false); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "bar", true), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", true), "bar"); + Assert.equal( + await getContentPref("bar.foo.example.org", "subsub", false), + "subsub" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://foo.example.org" + ), + true, + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info( + "Verify content prefs matching principal 'https://foo.example.org' have been cleared" + ); + Assert.equal( + await getContentPref("example.com", "foo", false), + "foo", + "Unrelated domain entry should still exist." + ); + Assert.equal( + await getContentPref("example.org", "bar", false), + "bar", + "Base domain entry should still exist." + ); + Assert.equal( + await getContentPref("example.org", "bar", true), + "bar", + "Base domain PBM entry should still exist." + ); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + undefined, + "Exact domain match should have been cleared in normal browsing." + ); + Assert.equal( + await getContentPref("foo.example.org", "bar", true), + "bar", + "Exact domain match should NOT have been cleared in private browsing." + ); + // TODO: PreferencesCleaner does not clear by exact principal but includes + // subdomains of the given principal. + Assert.equal( + await getContentPref("bar.foo.example.org", "subsub", false), + undefined, + "TODO: Subdomain entry should still exist." + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteBySite() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "bar", "bar", true); + await setContentPref("foo.example.org", "bar", "bar", false); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "bar", true), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromSite( + "example.org", + {}, + true, + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info( + "Verify content prefs for 'example.org' have been cleared, including PBM." + ); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), undefined); + Assert.equal(await getContentPref("example.org", "bar", true), undefined); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + undefined + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteBySite_pattern() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "barPBM", "barPBM", true); + await setContentPref("foo.example.org", "bar", "bar", false); + await setContentPref("foo.example.org", "subPBM", "subPBM", true); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "barPBM", true), "barPBM"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + Assert.equal( + await getContentPref("foo.example.org", "subPBM", true), + "subPBM" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromSite( + "example.org", + { privateBrowsingId: 1 }, + true, + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info( + "Verify content prefs for 'example.org' have been cleared, but only for PBM." + ); + Assert.equal( + await getContentPref("example.com", "foo", false), + "foo", + "Unrelated domain should have not been cleared." + ); + Assert.equal( + await getContentPref("example.org", "bar", false), + "bar", + "Base domain entry should NOT have been cleared for normal browsing." + ); + Assert.equal( + await getContentPref("example.org", "barPBM", true), + undefined, + "Base domain entry should have been cleared for private browsing." + ); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + "bar", + "Subdomain entry should not have been cleared for normal browsing." + ); + Assert.equal( + await getContentPref("foo.example.org", "subPBM", true), + undefined, + "Subdomain entry should have been cleared for private browsing." + ); + + await SiteDataTestUtils.clear(); +}); + +// TODO: implement a proper range clearing test. We're currently lacking the +// capability to set content prefs with a specific creation timestamp. This +// tests only clearing everything if the entire time range is passed. +add_task(async function test_deleteByRange() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "bar", "bar", true); + await setContentPref("foo.example.org", "bar", "bar", false); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "bar", true), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + + info("Delete entire time range."); + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + 0, + Date.now() * 1000, + true, + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info("Verify all content prefs have been cleared"); + Assert.equal(await getContentPref("example.com", "foo", false), undefined); + Assert.equal(await getContentPref("example.org", "bar", false), undefined); + Assert.equal(await getContentPref("example.org", "bar", true), undefined); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + undefined + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteAll() { + info("Set content prefs"); + await setContentPref("example.com", "foo", "foo", false); + await setContentPref("example.org", "bar", "bar", false); + await setContentPref("example.org", "bar", "bar", true); + await setContentPref("foo.example.org", "bar", "bar", false); + + info("Verify content prefs have been set"); + Assert.equal(await getContentPref("example.com", "foo", false), "foo"); + Assert.equal(await getContentPref("example.org", "bar", false), "bar"); + Assert.equal(await getContentPref("example.org", "bar", true), "bar"); + Assert.equal(await getContentPref("foo.example.org", "bar", false), "bar"); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + info("Verify all content prefs have been cleared"); + Assert.equal(await getContentPref("example.com", "foo", false), undefined); + Assert.equal(await getContentPref("example.org", "bar", false), undefined); + Assert.equal(await getContentPref("example.org", "bar", true), undefined); + Assert.equal( + await getContentPref("foo.example.org", "bar", false), + undefined + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cleardata/tests/unit/xpcshell.toml b/toolkit/components/cleardata/tests/unit/xpcshell.toml index 00bad69b7054..fd02a9fe9bb1 100644 --- a/toolkit/components/cleardata/tests/unit/xpcshell.toml +++ b/toolkit/components/cleardata/tests/unit/xpcshell.toml @@ -19,6 +19,8 @@ skip-if = ["condprof"] # Bug 1893058 ["test_clear_data_service_flags.js"] +["test_content_preferences.js"] + ["test_cookie_banner_handling.js"] ["test_cookies.js"] diff --git a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs index 069229c9c512..3ee17d2ab3ff 100644 --- a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs +++ b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs @@ -529,16 +529,18 @@ ContentPrefService2.prototype = { this._cache.remove(sgroup, name); } + let isPrivate = context && context.usePrivateBrowsing; let stmts = []; - // First get the matching prefs. - stmts.push(this._commonGetStmt(group, name, includeSubdomains)); + if (context == null || !isPrivate) { + // First get the matching prefs. + stmts.push(this._commonGetStmt(group, name, includeSubdomains)); - // Delete the matching prefs. - let stmt = this._stmtWithGroupClause( - group, - includeSubdomains, - ` + // Delete the matching prefs. + let stmt = this._stmtWithGroupClause( + group, + includeSubdomains, + ` DELETE FROM prefs WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND CASE typeof(:group) @@ -546,45 +548,69 @@ ContentPrefService2.prototype = { ELSE prefs.groupID IN (${GROUP_CLAUSE}) END ` - ); - stmt.params.name = name; - stmts.push(stmt); + ); + stmt.params.name = name; + stmts.push(stmt); - stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + } - let prefs = new ContentPrefStore(); + let prefsNonPrivateBrowsing = new ContentPrefStore(); - let isPrivate = context && context.usePrivateBrowsing; - this._execStmts(stmts, { - onRow: row => { - let grp = row.getResultByName("grp"); - prefs.set(grp, name, undefined); - this._cache.set(grp, name, undefined); - }, - onDone: (reason, ok) => { - if (ok) { - this._cache.set(group, name, undefined); - if (isPrivate) { - for (let [sgroup] of this._pbStore.match( - group, - name, - includeSubdomains - )) { - prefs.set(sgroup, name, undefined); - this._pbStore.remove(sgroup, name); - } - } + // Only execute if we have statements to run. stmts is empty for clearing + // only private browsing data. + let queryPromise = Promise.resolve([ + Ci.nsIContentPrefService2.COMPLETE_OK, + false, + ]); + + if (stmts.length) { + queryPromise = new Promise(resolve => { + this._execStmts(stmts, { + onRow: row => { + let grp = row.getResultByName("grp"); + prefsNonPrivateBrowsing.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: (reason, ok) => { + this._cache.set(group, name, undefined); + resolve([reason, ok]); + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }); + } + + let prefsPrivateBrowsing = new ContentPrefStore(); + + if (isPrivate || context == null) { + for (let [sgroup] of this._pbStore.match( + group, + name, + includeSubdomains + )) { + prefsPrivateBrowsing.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); + } + } + + queryPromise.then(([reason, hasNonPrivateEntries]) => { + // Notify caller of completion. + cbHandleCompletion(callback, reason); + + // Notify for non PBM changes. + if (hasNonPrivateEntries) { + for (let [sgroup] of prefsNonPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, name, false); } - cbHandleCompletion(callback, reason); - if (ok) { - for (let [sgroup, ,] of prefs) { - this._notifyPrefRemoved(sgroup, name, isPrivate); - } - } - }, - onError: nsresult => { - cbHandleError(callback, nsresult); - }, + } + + // Notify for PBM changes. + for (let [sgroup] of prefsPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, name, true); + } }); }, @@ -634,93 +660,126 @@ ContentPrefService2.prototype = { this._cache.removeGroup(sgroup); } + let isPrivate = context?.usePrivateBrowsing; let stmts = []; - // First get the matching prefs, then delete groups and prefs that reference - // deleted groups. - if (group) { - stmts.push( - this._stmtWithGroupClause( - group, - includeSubdomains, - ` - SELECT groups.name AS grp, settings.name AS name - FROM prefs - JOIN settings ON settings.id = prefs.settingID - JOIN groups ON groups.id = prefs.groupID - WHERE prefs.groupID IN (${GROUP_CLAUSE}) - ` - ) - ); - stmts.push( - this._stmtWithGroupClause( - group, - includeSubdomains, - `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})` - ) - ); + // For null we clear normal and private browsing data. false only clears + // normal browsing data. true only clears private browsing data. + if (context == null || !isPrivate) { + // First get the matching prefs, then delete groups and prefs that reference + // deleted groups. + if (group) { + stmts.push( + this._stmtWithGroupClause( + group, + includeSubdomains, + ` + SELECT groups.name AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE prefs.groupID IN (${GROUP_CLAUSE}) + ` + ) + ); + stmts.push( + this._stmtWithGroupClause( + group, + includeSubdomains, + `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})` + ) + ); + stmts.push( + this._stmt(` + DELETE FROM prefs + WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups) + `) + ); + } else { + stmts.push( + this._stmt(` + SELECT NULL AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE prefs.groupID IS NULL + `) + ); + stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL")); + } + + // Finally delete settings that are no longer referenced. stmts.push( this._stmt(` - DELETE FROM prefs - WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups) + DELETE FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) `) ); - } else { - stmts.push( - this._stmt(` - SELECT NULL AS grp, settings.name AS name - FROM prefs - JOIN settings ON settings.id = prefs.settingID - WHERE prefs.groupID IS NULL - `) - ); - stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL")); } - // Finally delete settings that are no longer referenced. - stmts.push( - this._stmt(` - DELETE FROM settings - WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) - `) - ); + let prefsNonPrivateBrowsing = new ContentPrefStore(); - let prefs = new ContentPrefStore(); + // Only execute if we have statements to run. stmts is empty for clearing + // only private browsing data. + let queryPromise = Promise.resolve([ + Ci.nsIContentPrefService2.COMPLETE_OK, + false, + ]); + if (stmts.length) { + queryPromise = new Promise(resolve => { + this._execStmts(stmts, { + onRow: row => { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefsNonPrivateBrowsing.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: (reason, ok) => { + resolve([reason, ok]); + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }); + } - let isPrivate = context && context.usePrivateBrowsing; - this._execStmts(stmts, { - onRow: row => { - let grp = row.getResultByName("grp"); - let name = row.getResultByName("name"); - prefs.set(grp, name, undefined); - this._cache.set(grp, name, undefined); - }, - onDone: (reason, ok) => { - if (ok && isPrivate) { - for (let [sgroup, sname] of this._pbStore) { - if ( - !group || - (!includeSubdomains && group == sgroup) || - (includeSubdomains && - sgroup && - this._pbStore.groupsMatchIncludingSubdomains(group, sgroup)) - ) { - prefs.set(sgroup, sname, undefined); - this._pbStore.remove(sgroup, sname); - } - } + let prefsPrivateBrowsing = new ContentPrefStore(); + + if (isPrivate || context == null) { + for (let [sgroup, sname] of this._pbStore) { + if ( + !group || + (!includeSubdomains && group == sgroup) || + (includeSubdomains && + sgroup && + this._pbStore.groupsMatchIncludingSubdomains(group, sgroup)) + ) { + prefsPrivateBrowsing.set(sgroup, sname, undefined); + this._pbStore.remove(sgroup, sname); } + } + } + + queryPromise + .then(([reason, hasNonPrivateEntries]) => { + // Notify caller of completion. cbHandleCompletion(callback, reason); - if (ok) { - for (let [sgroup, sname] of prefs) { - this._notifyPrefRemoved(sgroup, sname, isPrivate); + + // Notify for non PBM changes. + if (hasNonPrivateEntries) { + for (let [sgroup, sname] of prefsNonPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, sname, false); } } - }, - onError: nsresult => { + + // Notify for PBM changes. + for (let [sgroup, sname] of prefsPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, sname, true); + } + }) + .catch(nsresult => { cbHandleError(callback, nsresult); - }, - }); + }); }, _removeAllDomainsSince: function CPS2__removeAllDomainsSince( @@ -737,60 +796,95 @@ ContentPrefService2.prototype = { // Invalidate all the group cache because we don't know which groups will be removed. this._cache.removeAllGroups(); + let isPrivate = context?.usePrivateBrowsing; let stmts = []; - // Get prefs that are about to be removed to notify about their removal. - let stmt = this._stmt(` - SELECT groups.name AS grp, settings.name AS name - FROM prefs - JOIN settings ON settings.id = prefs.settingID - JOIN groups ON groups.id = prefs.groupID - WHERE timestamp >= :since - `); - stmt.params.since = since; - stmts.push(stmt); + // For null we clear normal and private browsing data. false only clears + // normal browsing data. true only clears private browsing data. + if (context == null || !isPrivate) { + // Get prefs that are about to be removed to notify about their removal. + let stmt = this._stmt(` + SELECT groups.name AS grp, settings.name AS name + FROM prefs + JOIN settings ON settings.id = prefs.settingID + JOIN groups ON groups.id = prefs.groupID + WHERE timestamp >= :since + `); + stmt.params.since = since; + stmts.push(stmt); - // Do the actual remove. - stmt = this._stmt(` - DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since - `); - stmt.params.since = since; - stmts.push(stmt); + // Do the actual remove. + stmt = this._stmt(` + DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since + `); + stmt.params.since = since; + stmts.push(stmt); - // Cleanup no longer used values. - stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + // Cleanup no longer used values. + stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); + } - let prefs = new ContentPrefStore(); - let isPrivate = context && context.usePrivateBrowsing; - this._execStmts(stmts, { - onRow: row => { - let grp = row.getResultByName("grp"); - let name = row.getResultByName("name"); - prefs.set(grp, name, undefined); - this._cache.set(grp, name, undefined); - }, - onDone: (reason, ok) => { - // This nukes all the groups in _pbStore since we don't have their timestamp - // information. - if (ok && isPrivate) { - for (let [sgroup, sname] of this._pbStore) { - if (sgroup) { - prefs.set(sgroup, sname, undefined); - } - } - this._pbStore.removeAllGroups(); + let prefsNonPrivateBrowsing = new ContentPrefStore(); + + // Only execute if we have statements to run. stmts is empty for clearing + // only private browsing data. + let queryPromise = Promise.resolve([ + Ci.nsIContentPrefService2.COMPLETE_OK, + false, + ]); + + if (stmts.length) { + queryPromise = new Promise(resolve => { + this._execStmts(stmts, { + onRow: row => { + let grp = row.getResultByName("grp"); + let name = row.getResultByName("name"); + prefsNonPrivateBrowsing.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: (reason, ok) => { + resolve([reason, ok]); + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }); + } + + let prefsPrivateBrowsing = new ContentPrefStore(); + + // This nukes all the groups in _pbStore since we don't have their timestamp + // information. + if (isPrivate || context == null) { + for (let [sgroup, sname] of this._pbStore) { + if (sgroup) { + prefsPrivateBrowsing.set(sgroup, sname, undefined); } + } + this._pbStore.removeAllGroups(); + } + + queryPromise + .then(([reason, hasNonPrivateEntries]) => { + // Notify caller of completion. cbHandleCompletion(callback, reason); - if (ok) { - for (let [sgroup, sname] of prefs) { - this._notifyPrefRemoved(sgroup, sname, isPrivate); + + // Notify for non PBM changes. + if (hasNonPrivateEntries) { + for (let [sgroup, sname] of prefsNonPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, sname, false); } } - }, - onError: nsresult => { + + // Notify for PBM changes. + for (let [sgroup, sname] of prefsPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, sname, true); + } + }) + .catch(nsresult => { cbHandleError(callback, nsresult); - }, - }); + }); }, removeAllDomainsSince: function CPS2_removeAllDomainsSince( @@ -817,76 +911,110 @@ ContentPrefService2.prototype = { } } + let isPrivate = context?.usePrivateBrowsing; let stmts = []; - // First get the matching prefs. Include null if any of those prefs are - // global. - let stmt = this._stmt(` - SELECT groups.name AS grp - FROM prefs - JOIN settings ON settings.id = prefs.settingID - JOIN groups ON groups.id = prefs.groupID - WHERE settings.name = :name - UNION - SELECT NULL AS grp - WHERE EXISTS ( - SELECT prefs.id + // For null we clear normal and private browsing data. false only clears + // normal browsing data. true only clears private browsing data. + if (context == null || !isPrivate) { + // First get the matching prefs. Include null if any of those prefs are + // global. + let stmt = this._stmt(` + SELECT groups.name AS grp FROM prefs JOIN settings ON settings.id = prefs.settingID - WHERE settings.name = :name AND prefs.groupID IS NULL - ) - `); - stmt.params.name = name; - stmts.push(stmt); + JOIN groups ON groups.id = prefs.groupID + WHERE settings.name = :name + UNION + SELECT NULL AS grp + WHERE EXISTS ( + SELECT prefs.id + FROM prefs + JOIN settings ON settings.id = prefs.settingID + WHERE settings.name = :name AND prefs.groupID IS NULL + ) + `); + stmt.params.name = name; + stmts.push(stmt); - // Delete the target settings. - stmt = this._stmt("DELETE FROM settings WHERE name = :name"); - stmt.params.name = name; - stmts.push(stmt); + // Delete the target settings. + stmt = this._stmt("DELETE FROM settings WHERE name = :name"); + stmt.params.name = name; + stmts.push(stmt); - // Delete prefs and groups that are no longer used. - stmts.push( - this._stmt( - "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" - ) - ); - stmts.push( - this._stmt(` - DELETE FROM groups WHERE id NOT IN ( - SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL - ) - `) - ); + // Delete prefs and groups that are no longer used. + stmts.push( + this._stmt( + "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" + ) + ); + stmts.push( + this._stmt(` + DELETE FROM groups WHERE id NOT IN ( + SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL + ) + `) + ); + } - let prefs = new ContentPrefStore(); - let isPrivate = context && context.usePrivateBrowsing; + let prefsNonPrivateBrowsing = new ContentPrefStore(); - this._execStmts(stmts, { - onRow: row => { - let grp = row.getResultByName("grp"); - prefs.set(grp, name, undefined); - this._cache.set(grp, name, undefined); - }, - onDone: (reason, ok) => { - if (ok && isPrivate) { - for (let [sgroup, sname] of this._pbStore) { - if (sname === name) { - prefs.set(sgroup, name, undefined); - this._pbStore.remove(sgroup, name); - } - } + // Only execute if we have statements to run. stmts is empty for clearing + // only private browsing data. + let queryPromise = Promise.resolve([ + Ci.nsIContentPrefService2.COMPLETE_OK, + false, + ]); + + if (stmts.length) { + queryPromise = new Promise(resolve => { + this._execStmts(stmts, { + onRow: row => { + let grp = row.getResultByName("grp"); + prefsNonPrivateBrowsing.set(grp, name, undefined); + this._cache.set(grp, name, undefined); + }, + onDone: (reason, ok) => { + resolve([reason, ok]); + }, + onError: nsresult => { + cbHandleError(callback, nsresult); + }, + }); + }); + } + + let prefsPrivateBrowsing = new ContentPrefStore(); + + if (isPrivate || context == null) { + for (let [sgroup, sname] of this._pbStore) { + if (sname === name) { + prefsPrivateBrowsing.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); } + } + } + + queryPromise + .then(([reason, hasNonPrivateEntries]) => { + // Notify caller of completion. cbHandleCompletion(callback, reason); - if (ok) { - for (let [sgroup, ,] of prefs) { - this._notifyPrefRemoved(sgroup, name, isPrivate); + + // Notify for non PBM changes. + if (hasNonPrivateEntries) { + for (let [sgroup, ,] of prefsNonPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, name, false); } } - }, - onError: nsresult => { + + // Notify for PBM changes. + for (let [sgroup, ,] of prefsPrivateBrowsing) { + this._notifyPrefRemoved(sgroup, name, true); + } + }) + .catch(nsresult => { cbHandleError(callback, nsresult); - }, - }); + }); }, /** diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js index 0c9e79790302..4f058771d02a 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js @@ -175,16 +175,18 @@ add_task(async function privateBrowsing() { await set("a.com", "foo", 8, context); await setGlobal("foo", 9, context); await new Promise(resolve => - cps.removeByDomainAndName("a.com", "foo", context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve)) ); await new Promise(resolve => - cps.removeGlobal("foo", context, makeCallback(resolve)) + cps.removeGlobal("foo", null, makeCallback(resolve)) ); await new Promise(resolve => - cps.removeGlobal("qux", context, makeCallback(resolve)) + cps.removeGlobal("qux", null, makeCallback(resolve)) ); await new Promise(resolve => - cps.removeByDomainAndName("b.com", "foo", context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeByDomainAndName("b.com", "foo", null, makeCallback(resolve)) ); await dbOK([ ["a.com", "bar", 2], @@ -209,6 +211,74 @@ add_task(async function privateBrowsing() { await reset(); }); +/** + * Tests that when clearing data for normal browsing, private browsing is not + * affected and vice versa. + */ +add_task(async function privateBrowsingIsolatedRemoval() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setGlobal("qux", 5); + await set("b.com", "foo", 6); + await set("b.com", "bar", 7); + + await set("a.com", "foo", 8, privateLoadContext); + await set("b.com", "foo", 9, privateLoadContext); + await setGlobal("foo", 10, privateLoadContext); + + info("For a.com only clear the normal browsing entry."); + await new Promise(resolve => + cps.removeByDomainAndName( + "a.com", + "foo", + loadContext, + makeCallback(resolve) + ) + ); + + info("For a.com, 'foo' only the PBM entry should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], 6); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + await getOK(["b.com", "foo", privateLoadContext], 9); + await getGlobalOK(["foo", privateLoadContext], 10); + + info("For b.com only clear PBM entry."); + await new Promise(resolve => + cps.removeByDomainAndName( + "b.com", + "foo", + privateLoadContext, + makeCallback(resolve) + ) + ); + + info("For b.com, 'foo' only the non PBM entry should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], 6); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + // This still returns an entry because even if a PBM load context is passed we + // will fall back to non PBM entries with the same key. + await getOK(["b.com", "foo", privateLoadContext], 6); + await getGlobalOK(["foo", privateLoadContext], 10); + + await reset(); +}); + add_task(async function erroneous() { do_check_throws(() => cps.removeByDomainAndName(null, "foo", null)); do_check_throws(() => cps.removeByDomainAndName("", "foo", null)); diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js index 7615757f971c..66569c2169d8 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js @@ -51,7 +51,8 @@ add_task(async function privateBrowsing() { await set("a.com", "foo", 6, context); await setGlobal("foo", 7, context); await new Promise(resolve => - cps.removeAllDomains(context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeAllDomains(null, makeCallback(resolve)) ); await dbOK([ [null, "foo", 3], diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js index a78d35054ba1..74da2b6e33dd 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js @@ -73,7 +73,8 @@ add_task(async function privateBrowsing() { await set("a.com", "foo", 6, context); await setGlobal("foo", 7, context); await new Promise(resolve => - cps.removeAllDomainsSince(0, context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeAllDomainsSince(0, null, makeCallback(resolve)) ); await dbOK([ [null, "foo", 3], @@ -93,6 +94,63 @@ add_task(async function privateBrowsing() { await reset(); }); +/** + * Tests that when clearing data for normal browsing, private browsing is not + * affected and vice versa. + */ +add_task(async function privateBrowsingIsolatedRemoval() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setGlobal("qux", 5); + await set("b.com", "foo", 6); + await set("b.com", "bar", 7); + + await set("a.com", "foo", 8, privateLoadContext); + await set("b.com", "foo", 9, privateLoadContext); + await setGlobal("foo", 10, privateLoadContext); + + info("Clear all PBM data."); + await new Promise(resolve => + cps.removeAllDomainsSince(0, privateLoadContext, makeCallback(resolve)) + ); + + await getOK(["a.com", "foo", loadContext], 1); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], 6); + await getOK(["b.com", "bar", loadContext], 7); + + // This still returns an entry because even if a PBM load context is passed we + // will fall back to non PBM entries with the same key. + await getOK(["a.com", "foo", privateLoadContext], 1); + await getOK(["b.com", "foo", privateLoadContext], 6); + await getGlobalOK(["foo", privateLoadContext], 10); + + info("Clear all non PBM data."); + await new Promise(resolve => + cps.removeAllDomainsSince(0, loadContext, makeCallback(resolve)) + ); + + info("Should have cleared all domain keyed entries"); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], undefined); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], undefined); + await getOK(["b.com", "bar", loadContext], undefined); + + await getOK(["a.com", "foo", privateLoadContext], undefined); + await getOK(["b.com", "foo", privateLoadContext], undefined); + await getGlobalOK(["foo", privateLoadContext], 10); + + await reset(); +}); + add_task(async function erroneous() { do_check_throws(() => cps.removeAllDomainsSince(null, "bogus")); await reset(); diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js index b68d589dbfb3..99c142966ca1 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js @@ -149,12 +149,14 @@ add_task(async function privateBrowsing() { await set("b.com", "foo", 7, context); await setGlobal("foo", 8, context); await new Promise(resolve => - cps.removeByDomain("a.com", context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeByDomain("a.com", null, makeCallback(resolve)) ); await getOK(["b.com", "foo", context], 7); await getGlobalOK(["foo", context], 8); await new Promise(resolve => - cps.removeAllGlobals(context, makeCallback(resolve)) + // Passing context=null clears both normal and private browsing data. + cps.removeAllGlobals(null, makeCallback(resolve)) ); await dbOK([["b.com", "foo", 5]]); await getOK(["a.com", "foo", context], undefined); @@ -171,6 +173,65 @@ add_task(async function privateBrowsing() { await reset(); }); +/** + * Tests that when clearing data for normal browsing, private browsing is not + * affected and vice versa. + */ +add_task(async function privateBrowsingIsolatedRemoval() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setGlobal("qux", 5); + await set("b.com", "foo", 6); + await set("b.com", "bar", 7); + + await set("a.com", "foo", 8, privateLoadContext); + await set("b.com", "foo", 9, privateLoadContext); + await setGlobal("foo", 10, privateLoadContext); + + info("For a.com only clear the normal browsing entries."); + await new Promise(resolve => + cps.removeByDomain("a.com", loadContext, makeCallback(resolve)) + ); + + info("For a.com only the PBM entries should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], undefined); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], 6); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + await getOK(["b.com", "foo", privateLoadContext], 9); + await getGlobalOK(["foo", privateLoadContext], 10); + + info("For b.com only clear PBM entries."); + await new Promise(resolve => + cps.removeByDomain("b.com", privateLoadContext, makeCallback(resolve)) + ); + + info("For b.com only the non PBM entries should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], undefined); + await getGlobalOK(["foo", loadContext], 3); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], 6); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + info("b.com, foo, pbm should be cleared"); + // This still returns an entry because even if a PBM load context is passed we + // will fall back to non PBM entries with the same key. + await getOK(["b.com", "foo", privateLoadContext], 6); + await getGlobalOK(["foo", privateLoadContext], 10); + + await reset(); +}); + add_task(async function erroneous() { do_check_throws(() => cps.removeByDomain(null, null)); do_check_throws(() => cps.removeByDomain("", null)); diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js index b7fe310802d2..478256e043bb 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js @@ -60,7 +60,7 @@ add_task(async function privateBrowsing() { await setGlobal("foo", 8, context); await set("b.com", "bar", 9, context); await new Promise(resolve => - cps.removeByName("bar", context, makeCallback(resolve)) + cps.removeByName("bar", null, makeCallback(resolve)) ); await dbOK([ ["a.com", "foo", 1], @@ -83,6 +83,107 @@ add_task(async function privateBrowsing() { await reset(); }); +/** + * Tests that when clearing data for normal browsing, private browsing is not + * affected and vice versa. + */ +add_task(async function privateBrowsingIsolatedRemoval() { + await set("a.com", "foo", 1); + await set("a.com", "bar", 2); + await setGlobal("foo", 3); + await setGlobal("bar", 4); + await setGlobal("qux", 5); + await set("b.com", "foo", 6); + await set("b.com", "bar", 7); + + await set("a.com", "foo", 8, privateLoadContext); + await set("b.com", "foo", 9, privateLoadContext); + await set("b.com", "bar", 10, privateLoadContext); + await setGlobal("foo", 11, privateLoadContext); + + info("Clear 'foo' for non PBM."); + await new Promise(resolve => + cps.removeByName("foo", loadContext, makeCallback(resolve)) + ); + + info("For 'foo' only the PBM entries should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], undefined); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], undefined); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + await getOK(["b.com", "foo", privateLoadContext], 9); + await getOK(["b.com", "bar", privateLoadContext], 10); + await getGlobalOK(["foo", privateLoadContext], 11); + + info("Clear 'bar' for PBM."); + await new Promise(resolve => + cps.removeByName("bar", privateLoadContext, makeCallback(resolve)) + ); + + info("For 'bar' only the non PBM entries should remain."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], undefined); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], undefined); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], 8); + await getOK(["b.com", "foo", privateLoadContext], 9); + // This still returns an entry because even if a PBM load context is passed we + // will fall back to non PBM entry with the same key. + await getOK(["b.com", "bar", privateLoadContext], 7); + await getGlobalOK(["foo", privateLoadContext], 11); + + info("Clear 'foo' for PBM."); + await new Promise(resolve => + cps.removeByName("foo", privateLoadContext, makeCallback(resolve)) + ); + + info("All 'foo' entries should have been cleared."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], 2); + await getGlobalOK(["foo", loadContext], undefined); + await getGlobalOK(["bar", loadContext], 4); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], undefined); + await getOK(["b.com", "bar", loadContext], 7); + + await getOK(["a.com", "foo", privateLoadContext], undefined); + await getOK(["b.com", "foo", privateLoadContext], undefined); + // This still returns an entry because even if a PBM load context is passed we + // will fall back to non PBM entry with the same key. + await getOK(["b.com", "bar", privateLoadContext], 7); + await getGlobalOK(["foo", privateLoadContext], undefined); + + info("Clear 'bar' for non PBM."); + await new Promise(resolve => + cps.removeByName("bar", loadContext, makeCallback(resolve)) + ); + + info("All 'bar' and 'foo' entries should have been cleared."); + await getOK(["a.com", "foo", loadContext], undefined); + await getOK(["a.com", "bar", loadContext], undefined); + await getGlobalOK(["foo", loadContext], undefined); + await getGlobalOK(["bar", loadContext], undefined); + await getGlobalOK(["qux", loadContext], 5); + await getOK(["b.com", "foo", loadContext], undefined); + await getOK(["b.com", "bar", loadContext], undefined); + + await getOK(["a.com", "foo", privateLoadContext], undefined); + await getOK(["b.com", "foo", privateLoadContext], undefined); + await getOK(["b.com", "bar", privateLoadContext], undefined); + await getGlobalOK(["foo", privateLoadContext], undefined); + + await reset(); +}); + add_task(async function erroneous() { do_check_throws(() => cps.removeByName("", null)); do_check_throws(() => cps.removeByName(null, null)); diff --git a/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs index dfd5d2cd26e7..ea3b75aff4d8 100644 --- a/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs +++ b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs @@ -58,23 +58,17 @@ var observer = { if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) { Services.prefs.clearUserPref(LAST_DIR_PREF); } - // Ensure that purging session history causes both the session-only PB cache - // and persistent prefs to be cleared. - let promises = [ - new Promise(resolve => - lazy.cps2.removeByName(LAST_DIR_PREF, nonPrivateLoadContext, { - handleCompletion: resolve, - }) - ), - new Promise(resolve => - lazy.cps2.removeByName(LAST_DIR_PREF, privateLoadContext, { - handleCompletion: resolve, - }) - ), - ]; + // Ensure that purging session history causes both the session-only PB + // cache and persistent prefs to be cleared. Passing loadContext=null to + // cps will clear both. + let promise = new Promise(resolve => + lazy.cps2.removeByName(LAST_DIR_PREF, null, { + handleCompletion: resolve, + }) + ); // This is for testing purposes. if (aSubject && typeof subject == "object") { - aSubject.promise = Promise.all(promises); + aSubject.promise = promise; } break; }