summaryrefslogtreecommitdiffstats
path: root/toolkit/components/contentprefs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/contentprefs
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/contentprefs')
-rw-r--r--toolkit/components/contentprefs/ContentPrefService2.sys.mjs1427
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs185
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs160
-rw-r--r--toolkit/components/contentprefs/ContentPrefStore.sys.mjs122
-rw-r--r--toolkit/components/contentprefs/ContentPrefUtils.sys.mjs54
-rw-r--r--toolkit/components/contentprefs/components.conf14
-rw-r--r--toolkit/components/contentprefs/moz.build26
-rw-r--r--toolkit/components/contentprefs/tests/browser/browser.ini3
-rw-r--r--toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js242
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs59
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/head.js360
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js21
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js97
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js259
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js76
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js78
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js66
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_observers.js291
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_remove.js267
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js94
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js123
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js229
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js102
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js232
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini18
25 files changed, 4605 insertions, 0 deletions
diff --git a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs
new file mode 100644
index 0000000000..360689b4e8
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs
@@ -0,0 +1,1427 @@
+/* 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 {
+ ContentPref,
+ cbHandleCompletion,
+ cbHandleError,
+ cbHandleResult,
+} from "resource://gre/modules/ContentPrefUtils.sys.mjs";
+
+import { ContentPrefStore } from "resource://gre/modules/ContentPrefStore.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+const CACHE_MAX_GROUP_ENTRIES = 100;
+
+const GROUP_CLAUSE = `
+ SELECT id
+ FROM groups
+ WHERE name = :group OR
+ (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
+`;
+
+export function ContentPrefService2() {
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/ContentPrefServiceChild.sys.mjs"
+ ).ContentPrefServiceChild;
+ }
+
+ Services.obs.addObserver(this, "last-pb-context-exited");
+
+ // Observe shutdown so we can shut down the database connection.
+ Services.obs.addObserver(this, "profile-before-change");
+}
+
+const cache = new ContentPrefStore();
+cache.set = function CPS_cache_set(group, name, val) {
+ Object.getPrototypeOf(this).set.apply(this, arguments);
+ let groupCount = this._groups.size;
+ if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
+ // Clean half of the entries
+ for (let [group, name] of this) {
+ this.remove(group, name);
+ groupCount--;
+ if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) {
+ break;
+ }
+ }
+ }
+};
+
+const privModeStorage = new ContentPrefStore();
+
+function executeStatementsInTransaction(conn, stmts) {
+ return conn.executeTransaction(async () => {
+ let rows = [];
+ for (let { sql, params, cachable } of stmts) {
+ let execute = cachable ? conn.executeCached : conn.execute;
+ let stmtRows = await execute.call(conn, sql, params);
+ rows = rows.concat(stmtRows);
+ }
+ return rows;
+ });
+}
+
+function HostnameGrouper_group(aURI) {
+ var group;
+
+ try {
+ // Accessing the host property of the URI will throw an exception
+ // if the URI is of a type that doesn't have a host property.
+ // Otherwise, we manually throw an exception if the host is empty,
+ // since the effect is the same (we can't derive a group from it).
+
+ group = aURI.host;
+ if (!group) {
+ throw new Error("can't derive group from host; no host in URI");
+ }
+ } catch (ex) {
+ // If we don't have a host, then use the entire URI (minus the query,
+ // reference, and hash, if possible) as the group. This means that URIs
+ // like about:mozilla and about:blank will be considered separate groups,
+ // but at least they'll be grouped somehow.
+
+ // This also means that each individual file: URL will be considered
+ // its own group. This seems suboptimal, but so does treating the entire
+ // file: URL space as a single group (especially if folks start setting
+ // group-specific capabilities prefs).
+
+ // XXX Is there something better we can do here?
+
+ try {
+ var url = aURI.QueryInterface(Ci.nsIURL);
+ group = aURI.prePath + url.filePath;
+ } catch (ex) {
+ group = aURI.spec;
+ }
+ }
+
+ return group;
+}
+
+ContentPrefService2.prototype = {
+ // XPCOM Plumbing
+
+ classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
+
+ // Destruction
+
+ _destroy() {
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.obs.removeObserver(this, "last-pb-context-exited");
+
+ // Delete references to XPCOM components to make sure we don't leak them
+ // (although we haven't observed leakage in tests). Also delete references
+ // in _observers and _genericObservers to avoid cycles with those that
+ // refer to us and don't remove themselves from those observer pools.
+ delete this._observers;
+ delete this._genericObservers;
+ },
+
+ // in-memory cache and private-browsing stores
+
+ _cache: cache,
+ _pbStore: privModeStorage,
+
+ _connPromise: null,
+
+ get conn() {
+ if (this._connPromise) {
+ return this._connPromise;
+ }
+
+ return (this._connPromise = (async () => {
+ let conn;
+ try {
+ conn = await this._getConnection();
+ } catch (e) {
+ this.log("Failed to establish database connection: " + e);
+ throw e;
+ }
+ return conn;
+ })());
+ },
+
+ // nsIContentPrefService
+
+ getByName: function CPS2_getByName(name, context, callback) {
+ checkNameArg(name);
+ checkCallbackArg(callback, true);
+
+ // Some prefs may be in both the database and the private browsing store.
+ // Notify the caller of such prefs only once, using the values from private
+ // browsing.
+ let pbPrefs = new ContentPrefStore();
+ if (context && context.usePrivateBrowsing) {
+ for (let [sgroup, sname, val] of this._pbStore) {
+ if (sname == name) {
+ pbPrefs.set(sgroup, sname, val);
+ }
+ }
+ }
+
+ let stmt1 = this._stmt(`
+ SELECT groups.name AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE settings.name = :name
+ `);
+ stmt1.params.name = name;
+
+ let stmt2 = this._stmt(`
+ SELECT NULL AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE settings.name = :name AND prefs.groupID ISNULL
+ `);
+ stmt2.params.name = name;
+
+ this._execStmts([stmt1, stmt2], {
+ onRow: row => {
+ let grp = row.getResultByName("grp");
+ let val = row.getResultByName("value");
+ this._cache.set(grp, name, val);
+ if (!pbPrefs.has(grp, name)) {
+ cbHandleResult(callback, new ContentPref(grp, name, val));
+ }
+ },
+ onDone: (reason, ok, gotRow) => {
+ if (ok) {
+ for (let [pbGroup, pbName, pbVal] of pbPrefs) {
+ cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ getByDomainAndName: function CPS2_getByDomainAndName(
+ group,
+ name,
+ context,
+ callback
+ ) {
+ checkGroupArg(group);
+ this._get(group, name, false, context, callback);
+ },
+
+ getBySubdomainAndName: function CPS2_getBySubdomainAndName(
+ group,
+ name,
+ context,
+ callback
+ ) {
+ checkGroupArg(group);
+ this._get(group, name, true, context, callback);
+ },
+
+ getGlobal: function CPS2_getGlobal(name, context, callback) {
+ this._get(null, name, false, context, callback);
+ },
+
+ _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkCallbackArg(callback, true);
+
+ // Some prefs may be in both the database and the private browsing store.
+ // Notify the caller of such prefs only once, using the values from private
+ // browsing.
+ let pbPrefs = new ContentPrefStore();
+ if (context && context.usePrivateBrowsing) {
+ for (let [sgroup, val] of this._pbStore.match(
+ group,
+ name,
+ includeSubdomains
+ )) {
+ pbPrefs.set(sgroup, name, val);
+ }
+ }
+
+ this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
+ onRow: row => {
+ let grp = row.getResultByName("grp");
+ let val = row.getResultByName("value");
+ this._cache.set(grp, name, val);
+ if (!pbPrefs.has(group, name)) {
+ cbHandleResult(callback, new ContentPref(grp, name, val));
+ }
+ },
+ onDone: (reason, ok, gotRow) => {
+ if (ok) {
+ if (!gotRow) {
+ this._cache.set(group, name, undefined);
+ }
+ for (let [pbGroup, pbName, pbVal] of pbPrefs) {
+ cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
+ let stmt = group
+ ? this._stmtWithGroupClause(
+ group,
+ includeSubdomains,
+ `
+ SELECT groups.name AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
+ `
+ )
+ : this._stmt(`
+ SELECT NULL AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE settings.name = :name AND prefs.groupID ISNULL
+ `);
+ stmt.params.name = name;
+ return stmt;
+ },
+
+ _stmtWithGroupClause: function CPS2__stmtWithGroupClause(
+ group,
+ includeSubdomains,
+ sql
+ ) {
+ let stmt = this._stmt(sql, false);
+ stmt.params.group = group;
+ stmt.params.includeSubdomains = includeSubdomains || false;
+ stmt.params.pattern =
+ "%." + (group == null ? null : group.replace(/\/|%|_/g, "/$&"));
+ return stmt;
+ },
+
+ getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(
+ group,
+ name,
+ context
+ ) {
+ checkGroupArg(group);
+ let prefs = this._getCached(group, name, false, context);
+ return prefs[0] || null;
+ },
+
+ getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(
+ group,
+ name,
+ context
+ ) {
+ checkGroupArg(group);
+ return this._getCached(group, name, true, context);
+ },
+
+ getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
+ let prefs = this._getCached(null, name, false, context);
+ return prefs[0] || null;
+ },
+
+ _getCached: function CPS2__getCached(
+ group,
+ name,
+ includeSubdomains,
+ context
+ ) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+
+ let storesToCheck = [this._cache];
+ if (context && context.usePrivateBrowsing) {
+ storesToCheck.push(this._pbStore);
+ }
+
+ let outStore = new ContentPrefStore();
+ storesToCheck.forEach(function (store) {
+ for (let [sgroup, val] of store.match(group, name, includeSubdomains)) {
+ outStore.set(sgroup, name, val);
+ }
+ });
+
+ let prefs = [];
+ for (let [sgroup, sname, val] of outStore) {
+ prefs.push(new ContentPref(sgroup, sname, val));
+ }
+ return prefs;
+ },
+
+ set: function CPS2_set(group, name, value, context, callback) {
+ checkGroupArg(group);
+ this._set(group, name, value, context, callback);
+ },
+
+ setGlobal: function CPS2_setGlobal(name, value, context, callback) {
+ this._set(null, name, value, context, callback);
+ },
+
+ _set: function CPS2__set(group, name, value, context, callback) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkValueArg(value);
+ checkCallbackArg(callback, false);
+
+ if (context && context.usePrivateBrowsing) {
+ this._pbStore.set(group, name, value);
+ this._schedule(function () {
+ cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+ this._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
+ });
+ return;
+ }
+
+ // Invalidate the cached value so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ this._cache.remove(group, name);
+
+ let stmts = [];
+
+ // Create the setting if it doesn't exist.
+ let stmt = this._stmt(`
+ INSERT OR IGNORE INTO settings (id, name)
+ VALUES((SELECT id FROM settings WHERE name = :name), :name)
+ `);
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ // Create the group if it doesn't exist.
+ if (group) {
+ stmt = this._stmt(`
+ INSERT OR IGNORE INTO groups (id, name)
+ VALUES((SELECT id FROM groups WHERE name = :group), :group)
+ `);
+ stmt.params.group = group;
+ stmts.push(stmt);
+ }
+
+ // Finally create or update the pref.
+ if (group) {
+ stmt = this._stmt(`
+ INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
+ VALUES(
+ (SELECT prefs.id
+ FROM prefs
+ JOIN groups ON groups.id = prefs.groupID
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE groups.name = :group AND settings.name = :name),
+ (SELECT id FROM groups WHERE name = :group),
+ (SELECT id FROM settings WHERE name = :name),
+ :value,
+ :now
+ )
+ `);
+ stmt.params.group = group;
+ } else {
+ stmt = this._stmt(`
+ INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
+ VALUES(
+ (SELECT prefs.id
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE prefs.groupID IS NULL AND settings.name = :name),
+ NULL,
+ (SELECT id FROM settings WHERE name = :name),
+ :value,
+ :now
+ )
+ `);
+ }
+ stmt.params.name = name;
+ stmt.params.value = value;
+ stmt.params.now = Date.now() / 1000;
+ stmts.push(stmt);
+
+ this._execStmts(stmts, {
+ onDone: (reason, ok) => {
+ if (ok) {
+ this._cache.setWithCast(group, name, value);
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ this._notifyPrefSet(
+ group,
+ name,
+ value,
+ context && context.usePrivateBrowsing
+ );
+ }
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ removeByDomainAndName: function CPS2_removeByDomainAndName(
+ group,
+ name,
+ context,
+ callback
+ ) {
+ checkGroupArg(group);
+ this._remove(group, name, false, context, callback);
+ },
+
+ removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(
+ group,
+ name,
+ context,
+ callback
+ ) {
+ checkGroupArg(group);
+ this._remove(group, name, true, context, callback);
+ },
+
+ removeGlobal: function CPS2_removeGlobal(name, context, callback) {
+ this._remove(null, name, false, context, callback);
+ },
+
+ _remove: function CPS2__remove(
+ group,
+ name,
+ includeSubdomains,
+ context,
+ callback
+ ) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
+ this._cache.remove(sgroup, name);
+ }
+
+ let stmts = [];
+
+ // First get the matching prefs.
+ stmts.push(this._commonGetStmt(group, name, 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)
+ WHEN 'null' THEN prefs.groupID IS NULL
+ ELSE prefs.groupID IN (${GROUP_CLAUSE})
+ END
+ `
+ );
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
+
+ let prefs = 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);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, ,] of prefs) {
+ this._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ // Deletes settings and groups that are no longer used.
+ _settingsAndGroupsCleanupStmts() {
+ // The NOTNULL term in the subquery of the second statment is needed because of
+ // SQLite's weird IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html.
+ return [
+ this._stmt(`
+ DELETE FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `),
+ this._stmt(`
+ DELETE FROM groups WHERE id NOT IN (
+ SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
+ )
+ `),
+ ];
+ },
+
+ removeByDomain: function CPS2_removeByDomain(group, context, callback) {
+ checkGroupArg(group);
+ this._removeByDomain(group, false, context, callback);
+ },
+
+ removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
+ checkGroupArg(group);
+ this._removeByDomain(group, true, context, callback);
+ },
+
+ removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
+ this._removeByDomain(null, false, context, callback);
+ },
+
+ _removeByDomain: function CPS2__removeByDomain(
+ group,
+ includeSubdomains,
+ context,
+ callback
+ ) {
+ group = this._parseGroup(group);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
+ this._cache.removeGroup(sgroup);
+ }
+
+ 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})`
+ )
+ );
+ 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 settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `)
+ );
+
+ 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) => {
+ 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);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, sname] of prefs) {
+ this._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ _removeAllDomainsSince: function CPS2__removeAllDomainsSince(
+ since,
+ context,
+ callback
+ ) {
+ checkCallbackArg(callback, false);
+
+ since /= 1000;
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ // Invalidate all the group cache because we don't know which groups will be removed.
+ this._cache.removeAllGroups();
+
+ 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);
+
+ // 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());
+
+ 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();
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, sname] of prefs) {
+ this._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ removeAllDomainsSince: function CPS2_removeAllDomainsSince(
+ since,
+ context,
+ callback
+ ) {
+ this._removeAllDomainsSince(since, context, callback);
+ },
+
+ removeAllDomains: function CPS2_removeAllDomains(context, callback) {
+ this._removeAllDomainsSince(0, context, callback);
+ },
+
+ removeByName: function CPS2_removeByName(name, context, callback) {
+ checkNameArg(name);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let [group, sname] of this._cache) {
+ if (sname == name) {
+ this._cache.remove(group, name);
+ }
+ }
+
+ 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
+ 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 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;
+
+ 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);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, ,] of prefs) {
+ this._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: nsresult => {
+ cbHandleError(callback, nsresult);
+ },
+ });
+ },
+
+ /**
+ * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such
+ * statement is cached, one is created and cached.
+ *
+ * @param sql The SQL query string.
+ * @return The cached, possibly new, statement.
+ */
+ _stmt: function CPS2__stmt(sql, cachable = true) {
+ return {
+ sql,
+ cachable,
+ params: {},
+ };
+ },
+
+ /**
+ * Executes some async statements.
+ *
+ * @param stmts An array of mozIStorageAsyncStatements.
+ * @param callbacks An object with the following methods:
+ * onRow(row) (optional)
+ * Called once for each result row.
+ * row: A mozIStorageRow.
+ * onDone(reason, reasonOK, didGetRow) (required)
+ * Called when done.
+ * reason: A nsIContentPrefService2.COMPLETE_* value.
+ * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
+ * didGetRow: True if onRow was ever called.
+ * onError(nsresult) (optional)
+ * Called on error.
+ * nsresult: The error code.
+ */
+ _execStmts: async function CPS2__execStmts(stmts, callbacks) {
+ let conn = await this.conn;
+ let rows;
+ let ok = true;
+ try {
+ rows = await executeStatementsInTransaction(conn, stmts);
+ } catch (e) {
+ ok = false;
+ if (callbacks.onError) {
+ try {
+ callbacks.onError(e);
+ } catch (e) {
+ console.error(e);
+ }
+ } else {
+ console.error(e);
+ }
+ }
+
+ if (rows && callbacks.onRow) {
+ for (let row of rows) {
+ try {
+ callbacks.onRow(row);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ try {
+ callbacks.onDone(
+ ok
+ ? Ci.nsIContentPrefCallback2.COMPLETE_OK
+ : Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+ ok,
+ rows && !!rows.length
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Parses the domain (the "group", to use the database's term) from the given
+ * string.
+ *
+ * @param groupStr Assumed to be either a string or falsey.
+ * @return If groupStr is a valid URL string, returns the domain of
+ * that URL. If groupStr is some other nonempty string,
+ * returns groupStr itself. Otherwise returns null.
+ * The return value is truncated at GROUP_NAME_MAX_LENGTH.
+ */
+ _parseGroup: function CPS2__parseGroup(groupStr) {
+ if (!groupStr) {
+ return null;
+ }
+ try {
+ var groupURI = Services.io.newURI(groupStr);
+ groupStr = HostnameGrouper_group(groupURI);
+ } catch (err) {}
+ return groupStr.substring(
+ 0,
+ Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH - 1
+ );
+ },
+
+ _schedule: function CPS2__schedule(fn) {
+ Services.tm.dispatchToMainThread(fn.bind(this));
+ },
+
+ // A hash of arrays of observers, indexed by setting name.
+ _observers: new Map(),
+
+ // An array of generic observers, which observe all settings.
+ _genericObservers: new Set(),
+
+ addObserverForName(aName, aObserver) {
+ let observers;
+ if (aName) {
+ observers = this._observers.get(aName);
+ if (!observers) {
+ observers = new Set();
+ this._observers.set(aName, observers);
+ }
+ } else {
+ observers = this._genericObservers;
+ }
+
+ observers.add(aObserver);
+ },
+
+ removeObserverForName(aName, aObserver) {
+ let observers;
+ if (aName) {
+ observers = this._observers.get(aName);
+ if (!observers) {
+ return;
+ }
+ } else {
+ observers = this._genericObservers;
+ }
+
+ observers.delete(aObserver);
+ },
+
+ /**
+ * Construct a list of observers to notify about a change to some setting,
+ * putting setting-specific observers before before generic ones, so observers
+ * that initialize individual settings (like the page style controller)
+ * execute before observers that display multiple settings and depend on them
+ * being initialized first (like the content prefs sidebar).
+ */
+ _getObservers(aName) {
+ let genericObserverList = Array.from(this._genericObservers);
+ if (aName) {
+ let observersForName = this._observers.get(aName);
+ if (observersForName) {
+ return Array.from(observersForName).concat(genericObserverList);
+ }
+ }
+ return genericObserverList;
+ },
+
+ /**
+ * Notify all observers about the removal of a preference.
+ */
+ _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(
+ aGroup,
+ aName,
+ aIsPrivate
+ ) {
+ for (var observer of this._getObservers(aName)) {
+ try {
+ observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ /**
+ * Notify all observers about a preference change.
+ */
+ _notifyPrefSet: function ContentPrefService__notifyPrefSet(
+ aGroup,
+ aName,
+ aValue,
+ aIsPrivate
+ ) {
+ for (var observer of this._getObservers(aName)) {
+ try {
+ observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ extractDomain: function CPS2_extractDomain(str) {
+ return this._parseGroup(str);
+ },
+
+ /**
+ * Tests use this as a backchannel by calling it directly.
+ *
+ * @param subj This value depends on topic.
+ * @param topic The backchannel "method" name.
+ * @param data This value depends on topic.
+ */
+ observe: function CPS2_observe(subj, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ this._destroy();
+ break;
+ case "last-pb-context-exited":
+ this._pbStore.removeAll();
+ break;
+ case "test:reset":
+ let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+ this._reset(fn);
+ break;
+ case "test:db":
+ let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+ obj.value = this.conn;
+ break;
+ }
+ },
+
+ /**
+ * Removes all state from the service. Used by tests.
+ *
+ * @param callback A function that will be called when done.
+ */
+ async _reset(callback) {
+ this._pbStore.removeAll();
+ this._cache.removeAll();
+
+ this._observers = new Map();
+ this._genericObservers = new Set();
+
+ let tables = ["prefs", "groups", "settings"];
+ let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
+ this._execStmts(stmts, {
+ onDone: () => {
+ callback();
+ },
+ });
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIContentPrefService2",
+ "nsIObserver",
+ ]),
+
+ // Database Creation & Access
+
+ _dbVersion: 5,
+
+ _dbSchema: {
+ tables: {
+ groups:
+ "id INTEGER PRIMARY KEY, \
+ name TEXT NOT NULL",
+
+ settings:
+ "id INTEGER PRIMARY KEY, \
+ name TEXT NOT NULL",
+
+ prefs:
+ "id INTEGER PRIMARY KEY, \
+ groupID INTEGER REFERENCES groups(id), \
+ settingID INTEGER NOT NULL REFERENCES settings(id), \
+ value BLOB, \
+ timestamp INTEGER NOT NULL DEFAULT 0", // Storage in seconds, API in ms. 0 for migrated values.
+ },
+ indices: {
+ groups_idx: {
+ table: "groups",
+ columns: ["name"],
+ },
+ settings_idx: {
+ table: "settings",
+ columns: ["name"],
+ },
+ prefs_idx: {
+ table: "prefs",
+ columns: ["timestamp", "groupID", "settingID"],
+ },
+ },
+ },
+
+ _debugLog: false,
+
+ log: function CPS2_log(aMessage) {
+ if (this._debugLog) {
+ Services.console.logStringMessage("ContentPrefService2: " + aMessage);
+ }
+ },
+
+ async _getConnection(aAttemptNum = 0) {
+ if (
+ Services.startup.isInOrBeyondShutdownPhase(
+ Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWN
+ )
+ ) {
+ throw new Error("Can't open content prefs, we're in shutdown.");
+ }
+ let path = PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite");
+ let conn;
+ let resetAndRetry = async e => {
+ if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
+ throw e;
+ }
+
+ if (aAttemptNum >= this.MAX_ATTEMPTS) {
+ if (conn) {
+ await conn.close();
+ }
+ this.log("Establishing connection failed too many times. Giving up.");
+ throw e;
+ }
+
+ try {
+ await this._failover(conn, path);
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+ return this._getConnection(++aAttemptNum);
+ };
+ try {
+ conn = await lazy.Sqlite.openConnection({
+ path,
+ incrementalVacuum: true,
+ vacuumOnIdle: true,
+ });
+ try {
+ lazy.Sqlite.shutdown.addBlocker(
+ "Closing ContentPrefService2 connection.",
+ () => conn.close()
+ );
+ } catch (ex) {
+ // Uh oh, we failed to add a shutdown blocker. Close the connection
+ // anyway, but make sure that doesn't throw.
+ try {
+ await conn?.close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ return null;
+ }
+ } catch (e) {
+ console.error(e);
+ return resetAndRetry(e);
+ }
+
+ try {
+ await this._dbMaybeInit(conn);
+ } catch (e) {
+ console.error(e);
+ return resetAndRetry(e);
+ }
+
+ // Turn off disk synchronization checking to reduce disk churn and speed up
+ // operations when prefs are changed rapidly (such as when a user repeatedly
+ // changes the value of the browser zoom setting for a site).
+ //
+ // Note: this could cause database corruption if the OS crashes or machine
+ // loses power before the data gets written to disk, but this is considered
+ // a reasonable risk for the not-so-critical data stored in this database.
+ await conn.execute("PRAGMA synchronous = OFF");
+
+ return conn;
+ },
+
+ async _failover(aConn, aPath) {
+ this.log("Cleaning up DB file - close & remove & backup.");
+ if (aConn) {
+ await aConn.close();
+ }
+ let uniquePath = await IOUtils.createUniqueFile(
+ PathUtils.parent(aPath),
+ PathUtils.filename(aPath) + ".corrupt",
+ 0o600
+ );
+ await IOUtils.copy(aPath, uniquePath);
+ await IOUtils.remove(aPath);
+ this.log("Completed DB cleanup.");
+ },
+
+ _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
+ let version = parseInt(await aConn.getSchemaVersion(), 10);
+ this.log("Schema version: " + version);
+
+ if (version == 0) {
+ await this._dbCreateSchema(aConn);
+ } else if (version != this._dbVersion) {
+ await this._dbMigrate(aConn, version, this._dbVersion);
+ }
+ },
+
+ _createTable: async function CPS2__createTable(aConn, aName) {
+ let tSQL = this._dbSchema.tables[aName];
+ this.log("Creating table " + aName + " with " + tSQL);
+ await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`);
+ },
+
+ _createIndex: async function CPS2__createTable(aConn, aName) {
+ let index = this._dbSchema.indices[aName];
+ let statement =
+ "CREATE INDEX IF NOT EXISTS " +
+ aName +
+ " ON " +
+ index.table +
+ "(" +
+ index.columns.join(", ") +
+ ")";
+ await aConn.execute(statement);
+ },
+
+ _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) {
+ await aConn.executeTransaction(async () => {
+ this.log("Creating DB -- tables");
+ for (let name in this._dbSchema.tables) {
+ await this._createTable(aConn, name);
+ }
+
+ this.log("Creating DB -- indices");
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
+
+ await aConn.setSchemaVersion(this._dbVersion);
+ });
+ },
+
+ _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
+ /**
+ * Migrations should follow the template rules in bug 1074817 comment 3 which are:
+ * 1. Migration should be incremental and non-breaking.
+ * 2. It should be idempotent because one can downgrade an upgrade again.
+ * On downgrade:
+ * 1. Decrement schema version so that upgrade runs the migrations again.
+ */
+ await aConn.executeTransaction(async () => {
+ for (let i = aOldVersion; i < aNewVersion; i++) {
+ let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+ if (typeof this[migrationName] != "function") {
+ throw new Error(
+ "no migrator function from version " +
+ aOldVersion +
+ " to version " +
+ aNewVersion
+ );
+ }
+ await this[migrationName](aConn);
+ }
+ await aConn.setSchemaVersion(aNewVersion);
+ });
+ },
+
+ _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) {
+ await aConn.execute("ALTER TABLE groups RENAME TO groupsOld");
+ await this._createTable(aConn, "groups");
+ await aConn.execute(`
+ INSERT INTO groups (id, name)
+ SELECT id, name FROM groupsOld
+ `);
+
+ await aConn.execute("DROP TABLE groupers");
+ await aConn.execute("DROP TABLE groupsOld");
+ },
+
+ _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
+ },
+
+ _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
+ // Add timestamp column if it does not exist yet. This operation is idempotent.
+ try {
+ await aConn.execute("SELECT timestamp FROM prefs");
+ } catch (e) {
+ await aConn.execute(
+ "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"
+ );
+ }
+
+ // To modify prefs_idx drop it and create again.
+ await aConn.execute("DROP INDEX IF EXISTS prefs_idx");
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
+ },
+
+ async _dbMigrate4To5(conn) {
+ // This is a data migration for browser.download.lastDir. While it may not
+ // affect all consumers, it's simpler and safer to do it here than elsewhere.
+ await conn.execute(`
+ DELETE FROM prefs
+ WHERE id IN (
+ SELECT p.id FROM prefs p
+ JOIN groups g ON g.id = p.groupID
+ JOIN settings s ON s.id = p.settingID
+ WHERE s.name = 'browser.download.lastDir'
+ AND (
+ (g.name BETWEEN 'data:' AND 'data:' || X'FFFF') OR
+ (g.name BETWEEN 'file:' AND 'file:' || X'FFFF')
+ )
+ )
+ `);
+ await conn.execute(`
+ DELETE FROM groups WHERE NOT EXISTS (
+ SELECT 1 FROM prefs WHERE groupId = groups.id
+ )
+ `);
+ // Trim group names longer than MAX_GROUP_LENGTH.
+ await conn.execute(
+ `
+ UPDATE groups
+ SET name = substr(name, 0, :maxlen)
+ WHERE LENGTH(name) > :maxlen
+ `,
+ {
+ maxlen: Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH,
+ }
+ );
+ },
+};
+
+function checkGroupArg(group) {
+ if (!group || typeof group != "string") {
+ throw invalidArg("domain must be nonempty string.");
+ }
+}
+
+function checkNameArg(name) {
+ if (!name || typeof name != "string") {
+ throw invalidArg("name must be nonempty string.");
+ }
+}
+
+function checkValueArg(value) {
+ if (value === undefined) {
+ throw invalidArg("value must not be undefined.");
+ }
+}
+
+function checkCallbackArg(callback, required) {
+ if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) {
+ throw invalidArg("callback must be an nsIContentPrefCallback2.");
+ }
+ if (!callback && required) {
+ throw invalidArg("callback must be given.");
+ }
+}
+
+function invalidArg(msg) {
+ return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);
+}
+
+// XPCOM Plumbing
diff --git a/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs b/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs
new file mode 100644
index 0000000000..38d0d3c160
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceChild.sys.mjs
@@ -0,0 +1,185 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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 {
+ ContentPref,
+ _methodsCallableFromChild,
+ cbHandleCompletion,
+ cbHandleError,
+ cbHandleResult,
+ safeCallback,
+} from "resource://gre/modules/ContentPrefUtils.sys.mjs";
+
+// We only need one bit of information out of the context.
+function contextArg(context) {
+ return context && context.usePrivateBrowsing
+ ? { usePrivateBrowsing: true }
+ : null;
+}
+
+function NYI() {
+ throw new Error("Do not add any new users of these functions");
+}
+
+function CallbackCaller(callback) {
+ this._callback = callback;
+}
+
+CallbackCaller.prototype = {
+ handleResult(contentPref) {
+ cbHandleResult(
+ this._callback,
+ new ContentPref(contentPref.domain, contentPref.name, contentPref.value)
+ );
+ },
+
+ handleError(result) {
+ cbHandleError(this._callback, result);
+ },
+
+ handleCompletion(reason) {
+ cbHandleCompletion(this._callback, reason);
+ },
+};
+
+export class ContentPrefsChild extends JSProcessActorChild {
+ constructor() {
+ super();
+
+ // Map from pref name -> set of observers
+ this._observers = new Map();
+
+ // Map from random ID string -> CallbackCaller, per request
+ this._requests = new Map();
+ }
+
+ _getRandomId() {
+ return Services.uuid.generateUUID().toString();
+ }
+
+ receiveMessage(msg) {
+ let data = msg.data;
+ let callback;
+ switch (msg.name) {
+ case "ContentPrefs:HandleResult":
+ callback = this._requests.get(data.requestId);
+ callback.handleResult(data.contentPref);
+ break;
+
+ case "ContentPrefs:HandleError":
+ callback = this._requests.get(data.requestId);
+ callback.handleError(data.error);
+ break;
+
+ case "ContentPrefs:HandleCompletion":
+ callback = this._requests.get(data.requestId);
+ this._requests.delete(data.requestId);
+ callback.handleCompletion(data.reason);
+ break;
+
+ case "ContentPrefs:NotifyObservers": {
+ let observerList = this._observers.get(data.name);
+ if (!observerList) {
+ break;
+ }
+
+ for (let observer of observerList) {
+ safeCallback(observer, data.callback, data.args);
+ }
+
+ break;
+ }
+ }
+ }
+
+ callFunction(call, args, callback) {
+ let requestId = this._getRandomId();
+ let data = { call, args, requestId };
+
+ this._requests.set(requestId, new CallbackCaller(callback));
+ this.sendAsyncMessage("ContentPrefs:FunctionCall", data);
+ }
+
+ addObserverForName(name, observer) {
+ let set = this._observers.get(name);
+ if (!set) {
+ set = new Set();
+
+ // This is the first observer for this name. Start listening for changes
+ // to it.
+ this.sendAsyncMessage("ContentPrefs:AddObserverForName", {
+ name,
+ });
+ this._observers.set(name, set);
+ }
+
+ set.add(observer);
+ }
+
+ removeObserverForName(name, observer) {
+ let set = this._observers.get(name);
+ if (!set) {
+ return;
+ }
+
+ set.delete(observer);
+ if (set.size === 0) {
+ // This was the last observer for this name. Stop listening for changes.
+ this.sendAsyncMessage("ContentPrefs:RemoveObserverForName", {
+ name,
+ });
+
+ this._observers.delete(name);
+ }
+ }
+}
+
+export var ContentPrefServiceChild = {
+ QueryInterface: ChromeUtils.generateQI(["nsIContentPrefService2"]),
+
+ addObserverForName: (name, observer) => {
+ ChromeUtils.domProcessChild
+ .getActor("ContentPrefs")
+ .addObserverForName(name, observer);
+ },
+ removeObserverForName: (name, observer) => {
+ ChromeUtils.domProcessChild
+ .getActor("ContentPrefs")
+ .removeObserverForName(name, observer);
+ },
+
+ getCachedByDomainAndName: NYI,
+ getCachedBySubdomainAndName: NYI,
+ getCachedGlobal: NYI,
+ extractDomain: NYI,
+};
+
+function forwardMethodToParent(method, signature, ...args) {
+ // Ignore superfluous arguments
+ args = args.slice(0, signature.length);
+
+ // Process context argument for forwarding
+ let contextIndex = signature.indexOf("context");
+ if (contextIndex > -1) {
+ args[contextIndex] = contextArg(args[contextIndex]);
+ }
+ // Take out the callback argument, if present.
+ let callbackIndex = signature.indexOf("callback");
+ let callback = null;
+ if (callbackIndex > -1 && args.length > callbackIndex) {
+ callback = args.splice(callbackIndex, 1)[0];
+ }
+
+ let actor = ChromeUtils.domProcessChild.getActor("ContentPrefs");
+ actor.callFunction(method, args, callback);
+}
+
+for (let [method, signature] of _methodsCallableFromChild) {
+ ContentPrefServiceChild[method] = forwardMethodToParent.bind(
+ ContentPrefServiceChild,
+ method,
+ signature
+ );
+}
diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs
new file mode 100644
index 0000000000..66b4a02741
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs
@@ -0,0 +1,160 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ _methodsCallableFromChild: "resource://gre/modules/ContentPrefUtils.sys.mjs",
+});
+
+let loadContext = Cu.createLoadContext();
+let privateLoadContext = Cu.createPrivateLoadContext();
+
+function contextArg(context) {
+ return context && context.usePrivateBrowsing
+ ? privateLoadContext
+ : loadContext;
+}
+
+export class ContentPrefsParent extends JSProcessActorParent {
+ constructor() {
+ super();
+
+ // The names we're using this observer object for, used to keep track
+ // of the number of names we care about as well as for removing this
+ // observer if its associated process goes away.
+ this._prefsToObserve = new Set();
+ this._observer = null;
+ }
+
+ didDestroy() {
+ if (this._observer) {
+ for (let i of this._prefsToObserve) {
+ lazy.cps2.removeObserverForName(i, this._observer);
+ }
+ this._observer = null;
+ }
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "ContentPrefs:AddObserverForName": {
+ // The child process is responsible for not adding multiple parent
+ // observers for the same name.
+ let actor = this;
+ if (!this._observer) {
+ this._observer = {
+ onContentPrefSet(group, name, value, isPrivate) {
+ actor.onContentPrefSet(group, name, value, isPrivate);
+ },
+ onContentPrefRemoved(group, name, isPrivate) {
+ actor.onContentPrefRemoved(group, name, isPrivate);
+ },
+ };
+ }
+
+ let prefName = msg.data.name;
+ this._prefsToObserve.add(prefName);
+ lazy.cps2.addObserverForName(prefName, this._observer);
+ break;
+ }
+
+ case "ContentPrefs:RemoveObserverForName": {
+ let prefName = msg.data.name;
+ this._prefsToObserve.delete(prefName);
+ if (this._prefsToObserve.size == 0) {
+ lazy.cps2.removeObserverForName(prefName, this._observer);
+ this._observer = null;
+ }
+ break;
+ }
+
+ case "ContentPrefs:FunctionCall":
+ let data = msg.data;
+ let signature;
+
+ if (
+ !lazy._methodsCallableFromChild.some(([method, args]) => {
+ if (method == data.call) {
+ signature = args;
+ return true;
+ }
+ return false;
+ })
+ ) {
+ throw new Error(`Can't call ${data.call} from child!`);
+ }
+
+ let actor = this;
+ let args = data.args;
+
+ return new Promise(resolve => {
+ let listener = {
+ handleResult(pref) {
+ actor.sendAsyncMessage("ContentPrefs:HandleResult", {
+ requestId: data.requestId,
+ contentPref: {
+ domain: pref.domain,
+ name: pref.name,
+ value: pref.value,
+ },
+ });
+ },
+
+ handleError(error) {
+ actor.sendAsyncMessage("ContentPrefs:HandleError", {
+ requestId: data.requestId,
+ error,
+ });
+ },
+ handleCompletion(reason) {
+ actor.sendAsyncMessage("ContentPrefs:HandleCompletion", {
+ requestId: data.requestId,
+ reason,
+ });
+ },
+ };
+ // Push our special listener.
+ args.push(listener);
+
+ // Process context argument for forwarding
+ let contextIndex = signature.indexOf("context");
+ if (contextIndex > -1) {
+ args[contextIndex] = contextArg(args[contextIndex]);
+ }
+
+ // And call the function.
+ lazy.cps2[data.call](...args);
+ });
+ }
+
+ return undefined;
+ }
+
+ onContentPrefSet(group, name, value, isPrivate) {
+ this.sendAsyncMessage("ContentPrefs:NotifyObservers", {
+ name,
+ callback: "onContentPrefSet",
+ args: [group, name, value, isPrivate],
+ });
+ }
+
+ onContentPrefRemoved(group, name, isPrivate) {
+ this.sendAsyncMessage("ContentPrefs:NotifyObservers", {
+ name,
+ callback: "onContentPrefRemoved",
+ args: [group, name, isPrivate],
+ });
+ }
+}
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "cps2",
+ "@mozilla.org/content-pref/service;1",
+ "nsIContentPrefService2"
+);
diff --git a/toolkit/components/contentprefs/ContentPrefStore.sys.mjs b/toolkit/components/contentprefs/ContentPrefStore.sys.mjs
new file mode 100644
index 0000000000..67ba7de317
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefStore.sys.mjs
@@ -0,0 +1,122 @@
+/* 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/. */
+
+export function ContentPrefStore() {
+ this._groups = new Map();
+ this._globalNames = new Map();
+}
+
+ContentPrefStore.prototype = {
+ set: function CPS_set(group, name, val) {
+ if (group) {
+ if (!this._groups.has(group)) {
+ this._groups.set(group, new Map());
+ }
+ this._groups.get(group).set(name, val);
+ } else {
+ this._globalNames.set(name, val);
+ }
+ },
+
+ setWithCast: function CPS_setWithCast(group, name, val) {
+ if (typeof val == "boolean") {
+ val = val ? 1 : 0;
+ } else if (val === undefined) {
+ val = null;
+ }
+ this.set(group, name, val);
+ },
+
+ has: function CPS_has(group, name) {
+ if (group) {
+ return this._groups.has(group) && this._groups.get(group).has(name);
+ }
+ return this._globalNames.has(name);
+ },
+
+ get: function CPS_get(group, name) {
+ if (group && this._groups.has(group)) {
+ return this._groups.get(group).get(name);
+ }
+ return this._globalNames.get(name);
+ },
+
+ remove: function CPS_remove(group, name) {
+ if (group) {
+ if (this._groups.has(group)) {
+ this._groups.get(group).delete(name);
+ if (this._groups.get(group).size == 0) {
+ this._groups.delete(group);
+ }
+ }
+ } else {
+ this._globalNames.delete(name);
+ }
+ },
+
+ removeGroup: function CPS_removeGroup(group) {
+ if (group) {
+ this._groups.delete(group);
+ } else {
+ this._globalNames.clear();
+ }
+ },
+
+ removeAllGroups: function CPS_removeAllGroups() {
+ this._groups.clear();
+ },
+
+ removeAll: function CPS_removeAll() {
+ this.removeAllGroups();
+ this._globalNames.clear();
+ },
+
+ groupsMatchIncludingSubdomains: function CPS_groupsMatchIncludingSubdomains(
+ group,
+ group2
+ ) {
+ let idx = group2.indexOf(group);
+ return (
+ idx == group2.length - group.length &&
+ (idx == 0 || group2[idx - 1] == ".")
+ );
+ },
+
+ *[Symbol.iterator]() {
+ for (let [group, names] of this._groups) {
+ for (let [name, val] of names) {
+ yield [group, name, val];
+ }
+ }
+ for (let [name, val] of this._globalNames) {
+ yield [null, name, val];
+ }
+ },
+
+ *match(group, name, includeSubdomains) {
+ for (let sgroup of this.matchGroups(group, includeSubdomains)) {
+ if (this.has(sgroup, name)) {
+ yield [sgroup, this.get(sgroup, name)];
+ }
+ }
+ },
+
+ *matchGroups(group, includeSubdomains) {
+ if (group) {
+ if (includeSubdomains) {
+ for (let [sgroup, ,] of this) {
+ if (sgroup) {
+ if (this.groupsMatchIncludingSubdomains(group, sgroup)) {
+ yield sgroup;
+ }
+ }
+ }
+ } else if (this._groups.has(group)) {
+ yield group;
+ }
+ } else if (this._globalNames.size) {
+ yield null;
+ }
+ },
+};
diff --git a/toolkit/components/contentprefs/ContentPrefUtils.sys.mjs b/toolkit/components/contentprefs/ContentPrefUtils.sys.mjs
new file mode 100644
index 0000000000..0a17913d99
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefUtils.sys.mjs
@@ -0,0 +1,54 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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/. */
+
+export function ContentPref(domain, name, value) {
+ this.domain = domain;
+ this.name = name;
+ this.value = value;
+}
+
+ContentPref.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIContentPref"]),
+};
+
+export function cbHandleResult(callback, pref) {
+ safeCallback(callback, "handleResult", [pref]);
+}
+
+export function cbHandleCompletion(callback, reason) {
+ safeCallback(callback, "handleCompletion", [reason]);
+}
+
+export function cbHandleError(callback, nsresult) {
+ safeCallback(callback, "handleError", [nsresult]);
+}
+
+export function safeCallback(callbackObj, methodName, args) {
+ if (!callbackObj || typeof callbackObj[methodName] != "function") {
+ return;
+ }
+ try {
+ callbackObj[methodName].apply(callbackObj, args);
+ } catch (err) {
+ console.error(err);
+ }
+}
+
+export const _methodsCallableFromChild = Object.freeze([
+ ["getByName", ["name", "context", "callback"]],
+ ["getByDomainAndName", ["domain", "name", "context", "callback"]],
+ ["getBySubdomainAndName", ["domain", "name", "context", "callback"]],
+ ["getGlobal", ["name", "context", "callback"]],
+ ["set", ["domain", "name", "value", "context", "callback"]],
+ ["setGlobal", ["name", "value", "context", "callback"]],
+ ["removeByDomainAndName", ["domain", "name", "context", "callback"]],
+ ["removeBySubdomainAndName", ["domain", "name", "context", "callback"]],
+ ["removeGlobal", ["name", "context", "callback"]],
+ ["removeByDomain", ["domain", "context", "callback"]],
+ ["removeBySubdomain", ["domain", "context", "callback"]],
+ ["removeByName", ["name", "context", "callback"]],
+ ["removeAllDomains", ["context", "callback"]],
+ ["removeAllGlobals", ["context", "callback"]],
+]);
diff --git a/toolkit/components/contentprefs/components.conf b/toolkit/components/contentprefs/components.conf
new file mode 100644
index 0000000000..dac48777b5
--- /dev/null
+++ b/toolkit/components/contentprefs/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{e3f772f3-023f-4b32-b074-36cf0fd5d414}',
+ 'contract_ids': ['@mozilla.org/content-pref/service;1'],
+ 'esModule': 'resource://gre/modules/ContentPrefService2.sys.mjs',
+ 'constructor': 'ContentPrefService2',
+ },
+]
diff --git a/toolkit/components/contentprefs/moz.build b/toolkit/components/contentprefs/moz.build
new file mode 100644
index 0000000000..6d9ff8d805
--- /dev/null
+++ b/toolkit/components/contentprefs/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "tests/unit_cps2/xpcshell.ini",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+EXTRA_JS_MODULES += [
+ "ContentPrefService2.sys.mjs",
+ "ContentPrefServiceChild.sys.mjs",
+ "ContentPrefServiceParent.sys.mjs",
+ "ContentPrefStore.sys.mjs",
+ "ContentPrefUtils.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Preferences")
diff --git a/toolkit/components/contentprefs/tests/browser/browser.ini b/toolkit/components/contentprefs/tests/browser/browser.ini
new file mode 100644
index 0000000000..9722f5df24
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/browser/browser.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[browser_remoteContentPrefs.js]
diff --git a/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js
new file mode 100644
index 0000000000..00c845e488
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js
@@ -0,0 +1,242 @@
+"use strict";
+
+const childFrameURL = Services.io.newURI(
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>"
+);
+
+async function runTestsForFrame(browser, isPrivate) {
+ let loadContext = Cu.createLoadContext();
+ let privateLoadContext = Cu.createPrivateLoadContext();
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ Assert.ok(cps !== null, "got the content pref service");
+
+ await new Promise(resolve => {
+ cps.setGlobal("testing", 42, null, {
+ handleCompletion(reason) {
+ Assert.equal(reason, 0, "set a pref?");
+ resolve();
+ },
+ });
+ });
+
+ let numResults = 0;
+ await new Promise(resolve => {
+ cps.getGlobal("testing", null, {
+ handleResult(pref) {
+ numResults++;
+ Assert.equal(pref.name, "testing", "pref has the right name");
+ Assert.equal(pref.value, 42, "pref has the right value");
+ },
+
+ handleCompletion(reason) {
+ Assert.equal(reason, 0, "get a pref?");
+ Assert.equal(numResults, 1, "got the right number of prefs");
+ resolve();
+ },
+ });
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [isPrivate], async isFramePrivate => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ if (!content._result) {
+ content._result = [];
+ }
+
+ let observer;
+ cps.addObserverForName(
+ "testName",
+ (observer = {
+ onContentPrefSet(group, name, value, isPrivate) {
+ Assert.equal(group, null, "group should be null");
+ Assert.equal(name, "testName", "should only see testName");
+ Assert.equal(value, 42, "value should be correct");
+ Assert.equal(isPrivate, isFramePrivate, "privacy should match");
+
+ content._result.push("set");
+ },
+
+ onContentPrefRemoved(group, name, isPrivate) {
+ Assert.equal(group, null, "group should be null");
+ Assert.equal(name, "testName", "name should match");
+ Assert.equal(isPrivate, isFramePrivate, "privacy should match");
+
+ content._result.push("removed");
+ },
+ })
+ );
+ content._observer = observer;
+ });
+
+ for (let expectedResponse of ["set", "removed"]) {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ if (expectedResponse == "set") {
+ cps.setGlobal(
+ "testName",
+ 42,
+ isPrivate ? privateLoadContext : loadContext
+ );
+ } else {
+ cps.removeGlobal(
+ "testName",
+ isPrivate ? privateLoadContext : loadContext
+ );
+ }
+
+ let response = await SpecialPowers.spawn(
+ browser,
+ [expectedResponse],
+ async expected => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => content._result.length == 1,
+ "expecting one notification"
+ );
+
+ if (expected == "removed") {
+ cps.removeObserverForName("testName", content._observer);
+ }
+
+ Assert.equal(
+ content._result.length,
+ 1,
+ "correct number of notifications"
+ );
+ let result = content._result[0];
+ content._result = [];
+ return result;
+ }
+ );
+
+ is(response, expectedResponse, "got correct observer notification");
+ }
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ await new Promise(resolve => {
+ cps.setGlobal("testName", 42, null, {
+ handleCompletion(reason) {
+ Assert.equal(reason, 0, "set a pref");
+ cps.set("http://mochi.test", "testpref", "str", null, {
+ /* eslint no-shadow: 0 */
+ handleCompletion(reason) {
+ Assert.equal(reason, 0, "set a pref");
+ resolve();
+ },
+ });
+ },
+ });
+ });
+
+ await new Promise(resolve => {
+ cps.removeByDomain("http://mochi.test", null, {
+ handleCompletion(reason) {
+ Assert.equal(reason, 0, "remove succeeded");
+ cps.getByDomainAndName("http://mochi.test", "testpref", null, {
+ handleResult() {
+ throw new Error("got removed pref in test3");
+ },
+ handleCompletion() {
+ resolve();
+ },
+ handleError(rv) {
+ throw new Error(`got a pref error ${rv}`);
+ },
+ });
+ },
+ });
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ let event = await new Promise((resolve, reject) => {
+ let prefObserver = {
+ onContentPrefSet(group, name, value, isPrivate) {
+ resolve({ group, name, value, isPrivate });
+ },
+ onContentPrefRemoved(group, name, isPrivate) {
+ reject("got unexpected notification");
+ },
+ };
+
+ cps.addObserverForName("test", prefObserver);
+
+ let privateLoadContext = Cu.createPrivateLoadContext();
+ cps.set("http://mochi.test", "test", 42, privateLoadContext);
+
+ cps.addObserverForName("test", prefObserver);
+ });
+
+ Assert.equal(event.name, "test", "got the right event");
+ Assert.equal(event.isPrivate, true, "the event was for an isPrivate pref");
+ });
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ let results = [];
+ await new Promise(resolve => {
+ cps.getByDomainAndName("http://mochi.test", "test", null, {
+ handleResult(pref) {
+ info("received handleResult");
+ results.push(pref);
+ },
+ handleCompletion(reason) {
+ resolve();
+ },
+ handleError(rv) {
+ ok(false, `failed to get pref ${rv}`);
+ throw new Error("got unexpected error");
+ },
+ });
+ });
+
+ Assert.equal(results.length, 0, "should not have seen the pb pref");
+ });
+}
+
+async function runTest(isPrivate) {
+ /* globals Services */
+ info("testing with isPrivate=" + isPrivate);
+
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPrivate,
+ });
+
+ const system = Services.scriptSecurityManager.getSystemPrincipal();
+
+ let browser = newWindow.gBrowser.selectedBrowser;
+ let loadedPromise = BrowserTestUtils.browserLoaded(browser);
+ browser.loadURI(childFrameURL, { triggeringPrincipal: system });
+ await loadedPromise;
+
+ await runTestsForFrame(browser, isPrivate);
+
+ newWindow.close();
+}
+
+add_task(async function () {
+ await runTest(false);
+ await runTest(true);
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs
new file mode 100644
index 0000000000..3fab7c981f
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.sys.mjs
@@ -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/. */
+
+export function AsyncRunner(callbacks) {
+ this._callbacks = callbacks;
+ this._iteratorQueue = [];
+
+ // This catches errors reported to the console, e.g., via Cu.reportError.
+ Services.console.registerListener(this);
+}
+
+AsyncRunner.prototype = {
+ appendIterator: function AR_appendIterator(iter) {
+ this._iteratorQueue.push(iter);
+ },
+
+ next: function AR_next(arg) {
+ if (!this._iteratorQueue.length) {
+ this.destroy();
+ this._callbacks.done();
+ return;
+ }
+
+ try {
+ var { done, value } = this._iteratorQueue[0].next(arg);
+ if (done) {
+ this._iteratorQueue.shift();
+ this.next();
+ return;
+ }
+ } catch (err) {
+ this._callbacks.error(err);
+ }
+
+ // val is truthy => call next
+ // val is an iterator => prepend it to the queue and start on it
+ if (value) {
+ if (typeof value != "boolean") {
+ this._iteratorQueue.unshift(value);
+ }
+ this.next();
+ }
+ },
+
+ destroy: function AR_destroy() {
+ Services.console.unregisterListener(this);
+ this.destroy = function AR_alreadyDestroyed() {};
+ },
+
+ observe: function AR_consoleServiceListener(msg) {
+ if (
+ msg instanceof Ci.nsIScriptError &&
+ !(msg.flags & Ci.nsIScriptError.warningFlag)
+ ) {
+ this._callbacks.consoleError(msg);
+ }
+ },
+};
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/head.js b/toolkit/components/contentprefs/tests/unit_cps2/head.js
new file mode 100644
index 0000000000..32b6167e19
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js
@@ -0,0 +1,360 @@
+/* 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/. */
+
+let loadContext = Cu.createLoadContext();
+let privateLoadContext = Cu.createPrivateLoadContext();
+
+const CURRENT_DB_VERSION = 5;
+
+// There has to be a profile directory before the CPS service is gotten.
+do_get_profile();
+let cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+function makeCallback(resolve, callbacks, success = null) {
+ callbacks = callbacks || {};
+ if (!callbacks.handleError) {
+ callbacks.handleError = function (error) {
+ do_throw("handleError call was not expected, error: " + error);
+ };
+ }
+ if (!callbacks.handleResult) {
+ callbacks.handleResult = function () {
+ do_throw("handleResult call was not expected");
+ };
+ }
+ if (!callbacks.handleCompletion) {
+ callbacks.handleCompletion = function (reason) {
+ equal(reason, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+ if (success) {
+ success();
+ } else {
+ resolve();
+ }
+ };
+ }
+ return callbacks;
+}
+
+async function do_check_throws(fn) {
+ let threw = false;
+ try {
+ await fn();
+ } catch (err) {
+ threw = true;
+ }
+ ok(threw);
+}
+
+function sendMessage(msg, callback) {
+ let obj = callback || {};
+ let ref = Cu.getWeakReference(obj);
+ cps.QueryInterface(Ci.nsIObserver).observe(ref, "test:" + msg, null);
+ return "value" in obj ? obj.value : undefined;
+}
+
+function reset() {
+ return new Promise(resolve => {
+ sendMessage("reset", resolve);
+ });
+}
+
+function setWithDate(group, name, val, timestamp, context) {
+ return new Promise(resolve => {
+ async function updateDate() {
+ let conn = await sendMessage("db");
+ await conn.execute(
+ `
+ UPDATE prefs SET timestamp = :timestamp
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `,
+ { name, group, timestamp: timestamp / 1000 }
+ );
+
+ resolve();
+ }
+
+ cps.set(group, name, val, context, makeCallback(null, null, updateDate));
+ });
+}
+
+async function getDate(group, name, context) {
+ let conn = await sendMessage("db");
+ let [result] = await conn.execute(
+ `
+ SELECT timestamp FROM prefs
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `,
+ { name, group }
+ );
+
+ return result.getResultByName("timestamp") * 1000;
+}
+
+function set(group, name, val, context) {
+ return new Promise(resolve => {
+ cps.set(group, name, val, context, makeCallback(resolve));
+ });
+}
+
+function setGlobal(name, val, context) {
+ return new Promise(resolve => {
+ cps.setGlobal(name, val, context, makeCallback(resolve));
+ });
+}
+
+function prefOK(actual, expected, strict) {
+ ok(actual instanceof Ci.nsIContentPref);
+ equal(actual.domain, expected.domain);
+ equal(actual.name, expected.name);
+ if (strict) {
+ strictEqual(actual.value, expected.value);
+ } else {
+ equal(actual.value, expected.value);
+ }
+}
+
+async function getOK(args, expectedVal, expectedGroup, strict) {
+ if (args.length == 2) {
+ args.push(undefined);
+ }
+ let expectedPrefs =
+ expectedVal === undefined
+ ? []
+ : [
+ {
+ domain: expectedGroup || args[0],
+ name: args[1],
+ value: expectedVal,
+ },
+ ];
+ await getOKEx("getByDomainAndName", args, expectedPrefs, strict);
+}
+
+async function getSubdomainsOK(args, expectedGroupValPairs) {
+ if (args.length == 2) {
+ args.push(undefined);
+ }
+ let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+ return { domain: group, name: args[1], value: val };
+ });
+ await getOKEx("getBySubdomainAndName", args, expectedPrefs);
+}
+
+async function getGlobalOK(args, expectedVal) {
+ if (args.length == 1) {
+ args.push(undefined);
+ }
+ let expectedPrefs =
+ expectedVal === undefined
+ ? []
+ : [{ domain: null, name: args[0], value: expectedVal }];
+ await getOKEx("getGlobal", args, expectedPrefs);
+}
+
+async function getOKEx(methodName, args, expectedPrefs, strict, context) {
+ let actualPrefs = [];
+ await new Promise(resolve => {
+ args.push(
+ makeCallback(resolve, {
+ handleResult: pref => actualPrefs.push(pref),
+ })
+ );
+ cps[methodName].apply(cps, args);
+ });
+ arraysOfArraysOK([actualPrefs], [expectedPrefs], function (actual, expected) {
+ prefOK(actual, expected, strict);
+ });
+}
+
+function getCachedOK(
+ args,
+ expectedIsCached,
+ expectedVal,
+ expectedGroup,
+ strict
+) {
+ if (args.length == 2) {
+ args.push(undefined);
+ }
+ let expectedPref = !expectedIsCached
+ ? null
+ : {
+ domain: expectedGroup || args[0],
+ name: args[1],
+ value: expectedVal,
+ };
+ getCachedOKEx("getCachedByDomainAndName", args, expectedPref, strict);
+}
+
+function getCachedSubdomainsOK(args, expectedGroupValPairs) {
+ if (args.length == 2) {
+ args.push(undefined);
+ }
+ let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args);
+ actualPrefs = actualPrefs.sort(function (a, b) {
+ return a.domain.localeCompare(b.domain);
+ });
+ let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+ return { domain: group, name: args[1], value: val };
+ });
+ arraysOfArraysOK([actualPrefs], [expectedPrefs], prefOK);
+}
+
+function getCachedGlobalOK(args, expectedIsCached, expectedVal) {
+ if (args.length == 1) {
+ args.push(undefined);
+ }
+ let expectedPref = !expectedIsCached
+ ? null
+ : {
+ domain: null,
+ name: args[0],
+ value: expectedVal,
+ };
+ getCachedOKEx("getCachedGlobal", args, expectedPref);
+}
+
+function getCachedOKEx(methodName, args, expectedPref, strict) {
+ let actualPref = cps[methodName].apply(cps, args);
+ if (expectedPref) {
+ prefOK(actualPref, expectedPref, strict);
+ } else {
+ strictEqual(actualPref, null);
+ }
+}
+
+function arraysOK(actual, expected, cmp) {
+ if (actual.length != expected.length) {
+ do_throw(
+ "Length is not equal: " +
+ JSON.stringify(actual) +
+ "==" +
+ JSON.stringify(expected)
+ );
+ } else {
+ actual.forEach(function (actualElt, j) {
+ let expectedElt = expected[j];
+ cmp(actualElt, expectedElt);
+ });
+ }
+}
+
+function arraysOfArraysOK(actual, expected, cmp) {
+ cmp = cmp || equal;
+ arraysOK(actual, expected, function (act, exp) {
+ arraysOK(act, exp, cmp);
+ });
+}
+
+async function dbOK(expectedRows) {
+ let conn = await sendMessage("db");
+ let stmt = `
+ SELECT groups.name AS grp, settings.name AS name, prefs.value AS value
+ FROM prefs
+ LEFT JOIN groups ON groups.id = prefs.groupID
+ LEFT JOIN settings ON settings.id = prefs.settingID
+ UNION
+
+ /*
+ These second two SELECTs get the rows of the groups and settings tables
+ that aren't referenced by the prefs table. Neither should return any
+ rows if the component is working properly.
+ */
+ SELECT groups.name AS grp, NULL AS name, NULL AS value
+ FROM groups
+ WHERE id NOT IN (
+ SELECT DISTINCT groupID
+ FROM prefs
+ WHERE groupID NOTNULL
+ )
+ UNION
+ SELECT NULL AS grp, settings.name AS name, NULL AS value
+ FROM settings
+ WHERE id NOT IN (
+ SELECT DISTINCT settingID
+ FROM prefs
+ WHERE settingID NOTNULL
+ )
+
+ ORDER BY value ASC, grp ASC, name ASC
+ `;
+
+ let cols = ["grp", "name", "value"];
+
+ let actualRows = (await conn.execute(stmt)).map(row =>
+ cols.map(c => row.getResultByName(c))
+ );
+ arraysOfArraysOK(actualRows, expectedRows);
+}
+
+function on(event, names, dontRemove) {
+ return onEx(event, names, dontRemove).promise;
+}
+
+function onEx(event, names, dontRemove) {
+ let args = {
+ reset() {
+ for (let prop in this) {
+ if (Array.isArray(this[prop])) {
+ this[prop].splice(0, this[prop].length);
+ }
+ }
+ },
+ };
+
+ let observers = {};
+ let deferred = null;
+ let triggered = false;
+
+ names.forEach(function (name) {
+ let obs = {};
+ ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) {
+ obs[meth] = () => do_throw(meth + " should not be called for " + name);
+ });
+ obs["onContentPref" + event] = function () {
+ args[name].push(Array.from(arguments));
+
+ if (!triggered) {
+ triggered = true;
+ executeSoon(function () {
+ if (!dontRemove) {
+ names.forEach(n => cps.removeObserverForName(n, observers[n]));
+ }
+ deferred.resolve(args);
+ });
+ }
+ };
+ observers[name] = obs;
+ args[name] = [];
+ args[name].observer = obs;
+ cps.addObserverForName(name, obs);
+ });
+
+ return {
+ observers,
+ promise: new Promise(resolve => {
+ deferred = { resolve };
+ }),
+ };
+}
+
+async function schemaVersionIs(expectedVersion) {
+ let db = await sendMessage("db");
+ equal(await db.getSchemaVersion(), expectedVersion);
+}
+
+function wait() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+function observerArgsOK(actualArgs, expectedArgs) {
+ notEqual(actualArgs, undefined);
+ arraysOfArraysOK(actualArgs, expectedArgs);
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js
new file mode 100644
index 0000000000..013e8fc327
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+function run_test() {
+ let tests = {
+ "http://example.com": "example.com",
+ "http://example.com/": "example.com",
+ "http://example.com/foo/bar/baz": "example.com",
+ "http://subdomain.example.com/foo/bar/baz": "subdomain.example.com",
+ "http://qix.quux.example.com/foo/bar/baz": "qix.quux.example.com",
+ "file:///home/foo/bar": "file:///home/foo/bar",
+ "not a url": "not a url",
+ };
+ let cps = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ for (let url in tests) {
+ Assert.equal(cps.extractDomain(url), tests[url]);
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
new file mode 100644
index 0000000000..d8a70a545a
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ getCachedOK(["a.com", "foo"], false, undefined);
+ getCachedGlobalOK(["foo"], false, undefined);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com");
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+
+ await set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+
+ await setGlobal("foo", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ await setGlobal("bar", 4);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.a.com", "foo"], true, 2);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ getCachedOK(["a.com", "foo", context], true, 6);
+ getCachedOK(["a.com", "bar", context], true, 2);
+ getCachedGlobalOK(["foo", context], true, 7);
+ getCachedGlobalOK(["bar", context], true, 4);
+ getCachedOK(["b.com", "foo", context], true, 5);
+
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedOK(["b.com", "foo"], true, 5);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.getCachedByDomainAndName(null, "foo", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("", "foo", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("a.com", "", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("a.com", null, null));
+ do_check_throws(() => cps.getCachedGlobal("", null));
+ do_check_throws(() => cps.getCachedGlobal(null, null));
+ await reset();
+});
+
+add_task(async function casts() {
+ // SQLite casts booleans to integers. This makes sure the values stored in
+ // the cache are the same as the casted values in the database.
+
+ await set("a.com", "foo", false);
+ await getOK(["a.com", "foo"], 0, "a.com", true);
+ getCachedOK(["a.com", "foo"], true, 0, "a.com", true);
+
+ await set("a.com", "bar", true);
+ await getOK(["a.com", "bar"], 1, "a.com", true);
+ getCachedOK(["a.com", "bar"], true, 1, "a.com", true);
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
new file mode 100644
index 0000000000..ed15e6fb0e
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
@@ -0,0 +1,259 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ await set("a.com", "bar", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ await setGlobal("foo", 3);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ await setGlobal("bar", 4);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]
+ );
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+ await reset();
+});
+
+add_task(async function populateViaGet() {
+ await new Promise(resolve =>
+ cps.getByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ await new Promise(resolve =>
+ cps.getGlobal("foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ await reset();
+});
+
+add_task(async function populateViaGetSubdomains() {
+ await new Promise(resolve =>
+ cps.getBySubdomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ await reset();
+});
+
+add_task(async function populateViaRemove() {
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ await new Promise(resolve =>
+ cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+
+ await new Promise(resolve =>
+ cps.removeGlobal("foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ await set("a.com", "foo", 1);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ await set("a.com", "foo", 2);
+ await set("b.a.com", "foo", 3);
+ await new Promise(resolve =>
+ cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", undefined],
+ ]
+ );
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+
+ await setGlobal("foo", 4);
+ await new Promise(resolve =>
+ cps.removeGlobal("foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", undefined],
+ ]
+ );
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+ await reset();
+});
+
+add_task(async function populateViaRemoveByDomain() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await set("b.a.com", "foo", 3);
+ await set("b.a.com", "bar", 4);
+ await new Promise(resolve =>
+ cps.removeByDomain("a.com", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", 3],
+ ]
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "bar"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", 4],
+ ]
+ );
+
+ await set("a.com", "foo", 5);
+ await set("a.com", "bar", 6);
+ await new Promise(resolve =>
+ cps.removeBySubdomain("a.com", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", undefined],
+ ]
+ );
+ getCachedSubdomainsOK(
+ ["a.com", "bar"],
+ [
+ ["a.com", undefined],
+ ["b.a.com", undefined],
+ ]
+ );
+
+ await setGlobal("foo", 7);
+ await setGlobal("bar", 8);
+ await new Promise(resolve =>
+ cps.removeAllGlobals(null, makeCallback(resolve))
+ );
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ await reset();
+});
+
+add_task(async function populateViaRemoveAllDomains() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await set("b.com", "foo", 3);
+ await set("b.com", "bar", 4);
+ await new Promise(resolve =>
+ cps.removeAllDomains(null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "bar"], [["b.com", undefined]]);
+ await reset();
+});
+
+add_task(async function populateViaRemoveByName() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await new Promise(resolve =>
+ cps.removeByName("foo", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, 4);
+
+ await new Promise(resolve =>
+ cps.removeByName("bar", null, makeCallback(resolve))
+ );
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ getCachedSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+ getCachedSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+ getCachedGlobalOK(["foo", context], true, 7);
+ getCachedGlobalOK(["bar", context], true, 4);
+ getCachedSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.getCachedBySubdomainAndName(null, "foo", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("", "foo", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", "", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", null, null));
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
new file mode 100644
index 0000000000..7015174d53
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
@@ -0,0 +1,76 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function get_nonexistent() {
+ await getSubdomainsOK(["a.com", "foo"], []);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ await getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ await set("a.com", "bar", 2);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ await setGlobal("foo", 3);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ await getSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]
+ );
+ await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ await getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+ await getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+ await getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ await getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ await getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.getBySubdomainAndName(null, "foo", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("", "foo", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", "", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", null, null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", "foo", null, null));
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js
new file mode 100644
index 0000000000..916fc55498
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+// Dump of version we migrate from
+var schema_version3 = `
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+ CREATE TABLE groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
+ INSERT INTO "groups" VALUES(1,'foo.com');
+ INSERT INTO "groups" VALUES(2,'bar.com');
+
+ CREATE TABLE settings (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
+ INSERT INTO "settings" VALUES(1,'zoom-setting');
+ INSERT INTO "settings" VALUES(2,'dir-setting');
+
+ CREATE TABLE prefs (id INTEGER PRIMARY KEY, groupID INTEGER REFERENCES groups(id), settingID INTEGER NOT NULL REFERENCES settings(id), value BLOB);
+ INSERT INTO "prefs" VALUES(1,1,1,0.5);
+ INSERT INTO "prefs" VALUES(2,1,2,'/download/dir');
+ INSERT INTO "prefs" VALUES(3,2,1,0.3);
+ INSERT INTO "prefs" VALUES(4,NULL,1,0.1);
+
+ CREATE INDEX groups_idx ON groups(name);
+ CREATE INDEX settings_idx ON settings(name);
+ CREATE INDEX prefs_idx ON prefs(groupID, settingID);
+COMMIT;`;
+
+function prepareVersion3Schema(callback) {
+ var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("content-prefs.sqlite");
+
+ ok(!dbFile.exists(), "Db should not exist yet.");
+
+ var dbConnection = Services.storage.openDatabase(dbFile);
+ equal(dbConnection.schemaVersion, 0);
+
+ dbConnection.executeSimpleSQL(schema_version3);
+ dbConnection.schemaVersion = 3;
+
+ dbConnection.close();
+}
+
+add_task(async function resetBeforeTests() {
+ prepareVersion3Schema();
+});
+
+// WARNING: Database will reset after every test. This limitation comes from
+// the fact that we ContentPrefService constructor is run only once per test file
+// and so migration will be run only once.
+add_task(async function testMigration() {
+ // Test migrated db content.
+ await schemaVersionIs(CURRENT_DB_VERSION);
+ let dbExpectedState = [
+ [null, "zoom-setting", 0.1],
+ ["bar.com", "zoom-setting", 0.3],
+ ["foo.com", "zoom-setting", 0.5],
+ ["foo.com", "dir-setting", "/download/dir"],
+ ];
+ await dbOK(dbExpectedState);
+
+ // Migrated fields should have timestamp set to 0.
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(1000, null, makeCallback(resolve))
+ );
+ await dbOK(dbExpectedState);
+
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(0, null, makeCallback(resolve))
+ );
+ await dbOK([[null, "zoom-setting", 0.1]]);
+
+ // Test that dates are present after migration (column is added).
+ const timestamp = 1234;
+ await setWithDate("a.com", "pref-name", "val", timestamp);
+ let actualTimestamp = await getDate("a.com", "pref-name");
+ equal(actualTimestamp, timestamp);
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js
new file mode 100644
index 0000000000..a904542899
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema5.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+ChromeUtils.defineESModuleGetters(this, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+const MAX_LENGTH = Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH;
+const LONG_DATA_URL = `data:,${new Array(MAX_LENGTH).fill("x").join("")}`;
+
+// Dump of version we migrate from
+const schema_queries = [
+ "PRAGMA foreign_keys=OFF",
+ "CREATE TABLE groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
+ `INSERT INTO groups VALUES (1,'foo.com'),
+ (2,'bar.com'),
+ (3,'data:image/png;base64,1234'),
+ (4,'file:///d/test.file'),
+ (5,'${LONG_DATA_URL}')`,
+ "CREATE TABLE settings (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
+ `INSERT INTO settings VALUES (1,'zoom-setting'),
+ (2,'browser.download.lastDir')`,
+ `CREATE TABLE prefs (id INTEGER PRIMARY KEY,
+ groupID INTEGER REFERENCES groups(id),
+ settingID INTEGER NOT NULL REFERENCES settings(id),
+ value BLOB,
+ timestamp INTEGER NOT NULL DEFAULT 0)`,
+ `INSERT INTO prefs VALUES (1,1,1,0.5,0),
+ (2,1,2,'/download/dir',0),
+ (3,2,1,0.3,0),
+ (4,NULL,1,0.1,0),
+ (5,3,2,'/download/dir',0),
+ (6,4,2,'/download/dir',0),
+ (7,5,1,0.7,0)`,
+ "CREATE INDEX groups_idx ON groups(name)",
+ "CREATE INDEX settings_idx ON settings(name)",
+ "CREATE INDEX prefs_idx ON prefs(timestamp, groupID, settingID)",
+];
+
+add_setup(async function () {
+ let conn = await Sqlite.openConnection({
+ path: PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite"),
+ });
+ Assert.equal(await conn.getSchemaVersion(), 0);
+ await conn.executeTransaction(async () => {
+ for (let query of schema_queries) {
+ await conn.execute(query);
+ }
+ });
+ await conn.setSchemaVersion(4);
+ await conn.close();
+});
+
+add_task(async function test() {
+ // Test migrated db content.
+ await schemaVersionIs(CURRENT_DB_VERSION);
+ let dbExpectedState = [
+ [null, "zoom-setting", 0.1],
+ ["bar.com", "zoom-setting", 0.3],
+ ["foo.com", "zoom-setting", 0.5],
+ [LONG_DATA_URL.substring(0, MAX_LENGTH - 1), "zoom-setting", 0.7],
+ ["foo.com", "browser.download.lastDir", "/download/dir"],
+ ];
+ await dbOK(dbExpectedState);
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
new file mode 100644
index 0000000000..cc31cd740a
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
@@ -0,0 +1,291 @@
+/* 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/. */
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+function raceWithIdleDispatch(promise) {
+ return Promise.race([
+ promise,
+ new Promise(resolve => {
+ Services.tm.idleDispatchToMainThread(() => resolve(null));
+ }),
+ ]);
+}
+
+var tests = [
+ async function observerForName_set(context) {
+ let argsPromise;
+ argsPromise = on("Set", ["foo", null, "bar"]);
+ await set("a.com", "foo", 1, context);
+ let args = await argsPromise;
+ observerArgsOK(args.foo, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", 1, context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.bar, []);
+
+ argsPromise = on("Set", ["foo", null, "bar"]);
+ await setGlobal("foo", 2, context);
+ args = await argsPromise;
+ observerArgsOK(args.foo, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ await reset();
+ },
+
+ async function observerForName_remove(context) {
+ let argsPromise;
+ await set("a.com", "foo", 1, context);
+ await setGlobal("foo", 2, context);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName(
+ "a.com",
+ "bogus",
+ context,
+ makeCallback(resolve)
+ )
+ );
+ let args = await raceWithIdleDispatch(argsPromise);
+ strictEqual(args, null);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", context, makeCallback(resolve))
+ );
+ args = await argsPromise;
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeGlobal("foo", context, makeCallback(resolve))
+ );
+ args = await argsPromise;
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ await reset();
+ },
+
+ async function observerForName_removeByDomain(context) {
+ let argsPromise;
+ await set("a.com", "foo", 1, context);
+ await set("b.a.com", "bar", 2, context);
+ await setGlobal("foo", 3, context);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeByDomain("bogus", context, makeCallback(resolve))
+ );
+ let args = await raceWithIdleDispatch(argsPromise);
+ strictEqual(args, null);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeBySubdomain("a.com", context, makeCallback(resolve))
+ );
+ args = await argsPromise;
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", context.usePrivateBrowsing],
+ ["b.a.com", "bar", context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.bar, [["b.a.com", "bar", context.usePrivateBrowsing]]);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeAllGlobals(context, makeCallback(resolve))
+ );
+ args = await argsPromise;
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ await reset();
+ },
+
+ async function observerForName_removeAllDomains(context) {
+ let argsPromise;
+ await set("a.com", "foo", 1, context);
+ await setGlobal("foo", 2, context);
+ await set("b.com", "bar", 3, context);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeAllDomains(context, makeCallback(resolve))
+ );
+ let args = await argsPromise;
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", context.usePrivateBrowsing],
+ ["b.com", "bar", context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.bar, [["b.com", "bar", context.usePrivateBrowsing]]);
+ await reset();
+ },
+
+ async function observerForName_removeByName(context) {
+ let argsPromise;
+ await set("a.com", "foo", 1, context);
+ await set("a.com", "bar", 2, context);
+ await setGlobal("foo", 3, context);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeByName("bogus", context, makeCallback(resolve))
+ );
+ let args = await raceWithIdleDispatch(argsPromise);
+ strictEqual(args, null);
+
+ argsPromise = on("Removed", ["foo", null, "bar"]);
+ await new Promise(resolve =>
+ cps.removeByName("foo", context, makeCallback(resolve))
+ );
+ args = await argsPromise;
+ observerArgsOK(args.foo, [
+ ["a.com", "foo", context.usePrivateBrowsing],
+ [null, "foo", context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", context.usePrivateBrowsing],
+ [null, "foo", context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.bar, []);
+ await reset();
+ },
+
+ async function removeObserverForName(context) {
+ let { promise, observers } = onEx("Set", ["foo", null, "bar"]);
+ cps.removeObserverForName("foo", observers.foo);
+ await set("a.com", "foo", 1, context);
+ await wait();
+ let args = await promise;
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", 1, context.usePrivateBrowsing],
+ ]);
+ observerArgsOK(args.bar, []);
+ args.reset();
+
+ cps.removeObserverForName(null, args.null.observer);
+ await set("a.com", "foo", 2, context);
+ await wait();
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+ args.reset();
+ await reset();
+ },
+];
+
+// These tests are for functionality that doesn't behave the same way in private and public
+// contexts, so the expected results cannot be automatically generated like the previous tests.
+var specialTests = [
+ async function observerForName_removeAllDomainsSince() {
+ let argsPromise;
+ await setWithDate("a.com", "foo", 1, 100, null);
+ await setWithDate("b.com", "foo", 2, 200, null);
+ await setWithDate("c.com", "foo", 3, 300, null);
+
+ await setWithDate("a.com", "bar", 1, 0, null);
+ await setWithDate("b.com", "bar", 2, 100, null);
+ await setWithDate("c.com", "bar", 3, 200, null);
+ await setGlobal("foo", 2, null);
+
+ argsPromise = on("Removed", ["foo", "bar", null]);
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(200, null, makeCallback(resolve))
+ );
+
+ let args = await argsPromise;
+
+ observerArgsOK(args.foo, [
+ ["b.com", "foo", false],
+ ["c.com", "foo", false],
+ ]);
+ observerArgsOK(args.bar, [["c.com", "bar", false]]);
+ observerArgsOK(args.null, [
+ ["b.com", "foo", false],
+ ["c.com", "bar", false],
+ ["c.com", "foo", false],
+ ]);
+ await reset();
+ },
+
+ async function observerForName_removeAllDomainsSince_private() {
+ let argsPromise;
+ let context = privateLoadContext;
+ await setWithDate("a.com", "foo", 1, 100, context);
+ await setWithDate("b.com", "foo", 2, 200, context);
+ await setWithDate("c.com", "foo", 3, 300, context);
+
+ await setWithDate("a.com", "bar", 1, 0, context);
+ await setWithDate("b.com", "bar", 2, 100, context);
+ await setWithDate("c.com", "bar", 3, 200, context);
+ await setGlobal("foo", 2, context);
+
+ argsPromise = on("Removed", ["foo", "bar", null]);
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(200, context, makeCallback(resolve))
+ );
+
+ let args = await argsPromise;
+
+ observerArgsOK(args.foo, [
+ ["a.com", "foo", true],
+ ["b.com", "foo", true],
+ ["c.com", "foo", true],
+ ]);
+ observerArgsOK(args.bar, [
+ ["a.com", "bar", true],
+ ["b.com", "bar", true],
+ ["c.com", "bar", true],
+ ]);
+ observerArgsOK(args.null, [
+ ["a.com", "foo", true],
+ ["a.com", "bar", true],
+ ["b.com", "foo", true],
+ ["b.com", "bar", true],
+ ["c.com", "foo", true],
+ ["c.com", "bar", true],
+ ]);
+ await reset();
+ },
+];
+
+for (let i = 0; i < tests.length; i++) {
+ // Generate two wrappers of each test function that invoke the original test with an
+ // appropriate privacy context.
+ /* eslint-disable no-eval */
+ var pub = eval(
+ "var f = async function " +
+ tests[i].name +
+ "() { await tests[" +
+ i +
+ "](privateLoadContext); }; f"
+ );
+ var priv = eval(
+ "var f = async function " +
+ tests[i].name +
+ "_private() { await tests[" +
+ i +
+ "](privateLoadContext); }; f"
+ );
+ /* eslint-enable no-eval */
+ add_task(pub);
+ add_task(priv);
+}
+
+for (let test of specialTests) {
+ add_task(test);
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
new file mode 100644
index 0000000000..0c9e797903
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
@@ -0,0 +1,267 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ await set("a.com", "foo", 1);
+ await setGlobal("foo", 2);
+
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+
+ await new Promise(resolve =>
+ cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+
+ await new Promise(resolve =>
+ cps.removeGlobal("bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("bogus", "bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+
+ await set("a.com", "foo", 2);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName(
+ "http://a.com/huh",
+ "foo",
+ null,
+ makeCallback(resolve)
+ )
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+
+ await new Promise(resolve =>
+ cps.removeGlobal("foo", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], 4);
+
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "bar", null, makeCallback(resolve))
+ );
+ await dbOK([[null, "bar", 4]]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], 4);
+
+ await new Promise(resolve =>
+ cps.removeGlobal("bar", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], undefined);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([["b.a.com", "foo", 2]]);
+ await getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ await set("a.com", "foo", 3);
+ await new Promise(resolve =>
+ cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getSubdomainsOK(["a.com", "foo"], []);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+
+ await set("a.com", "foo", 4);
+ await set("b.a.com", "foo", 5);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([["a.com", "foo", 4]]);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+
+ await set("b.a.com", "foo", 6);
+ await new Promise(resolve =>
+ cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback(resolve))
+ );
+ await dbOK([["a.com", "foo", 4]]);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ 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);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 8, context);
+ await setGlobal("foo", 9, context);
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("a.com", "foo", context, makeCallback(resolve))
+ );
+ await new Promise(resolve =>
+ cps.removeGlobal("foo", context, makeCallback(resolve))
+ );
+ await new Promise(resolve =>
+ cps.removeGlobal("qux", context, makeCallback(resolve))
+ );
+ await new Promise(resolve =>
+ cps.removeByDomainAndName("b.com", "foo", context, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 7],
+ ]);
+ await getOK(["a.com", "foo", context], undefined);
+ await getOK(["a.com", "bar", context], 2);
+ await getGlobalOK(["foo", context], undefined);
+ await getGlobalOK(["bar", context], 4);
+ await getGlobalOK(["qux", context], undefined);
+ await getOK(["b.com", "foo", context], undefined);
+ await getOK(["b.com", "bar", context], 7);
+
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], 4);
+ await getGlobalOK(["qux"], undefined);
+ await getOK(["b.com", "foo"], undefined);
+ await getOK(["b.com", "bar"], 7);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.removeByDomainAndName(null, "foo", null));
+ do_check_throws(() => cps.removeByDomainAndName("", "foo", null));
+ do_check_throws(() =>
+ cps.removeByDomainAndName("a.com", "foo", null, "bogus")
+ );
+ do_check_throws(() => cps.removeBySubdomainAndName(null, "foo", null));
+ do_check_throws(() => cps.removeBySubdomainAndName("", "foo", null));
+ do_check_throws(() =>
+ cps.removeBySubdomainAndName("a.com", "foo", null, "bogus")
+ );
+ do_check_throws(() => cps.removeGlobal("", null));
+ do_check_throws(() => cps.removeGlobal(null, null));
+ do_check_throws(() => cps.removeGlobal("foo", null, "bogus"));
+ await reset();
+});
+
+add_task(async function removeByDomainAndName_invalidateCache() {
+ await set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback(resolve));
+ });
+ getCachedOK(["a.com", "foo"], false);
+ await promiseRemoved;
+ await reset();
+});
+
+add_task(async function removeBySubdomainAndName_invalidateCache() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]
+ );
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback(resolve));
+ });
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ await promiseRemoved;
+ await reset();
+});
+
+add_task(async function removeGlobal_invalidateCache() {
+ await setGlobal("foo", 1);
+ getCachedGlobalOK(["foo"], true, 1);
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeGlobal("foo", null, makeCallback(resolve));
+ });
+ getCachedGlobalOK(["foo"], false);
+ await promiseRemoved;
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
new file mode 100644
index 0000000000..7615757f97
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ await setGlobal("foo", 1);
+ await new Promise(resolve =>
+ cps.removeAllDomains(null, makeCallback(resolve))
+ );
+ await dbOK([[null, "foo", 1]]);
+ await getGlobalOK(["foo"], 1);
+ await reset();
+});
+
+add_task(async function domains() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+ await set("b.com", "bar", 6);
+
+ await new Promise(resolve =>
+ cps.removeAllDomains(null, makeCallback(resolve))
+ );
+ await dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], undefined);
+ await getOK(["b.com", "bar"], undefined);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ await new Promise(resolve =>
+ cps.removeAllDomains(context, makeCallback(resolve))
+ );
+ await dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo", context], undefined);
+ await getOK(["a.com", "bar", context], undefined);
+ await getGlobalOK(["foo", context], 7);
+ await getGlobalOK(["bar", context], 4);
+ await getOK(["b.com", "foo", context], undefined);
+
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], undefined);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.removeAllDomains(null, "bogus"));
+ await reset();
+});
+
+add_task(async function invalidateCache() {
+ await set("a.com", "foo", 1);
+ await set("b.com", "bar", 2);
+ await setGlobal("baz", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.com", "bar"], true, 2);
+ getCachedGlobalOK(["baz"], true, 3);
+ let promiseRemoved = new Promise(resolve =>
+ cps.removeAllDomains(null, makeCallback(resolve))
+ );
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["b.com", "bar"], false);
+ getCachedGlobalOK(["baz"], true, 3);
+ await promiseRemoved;
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js
new file mode 100644
index 0000000000..a78d35054b
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ await setGlobal("foo", 1);
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(0, null, makeCallback(resolve))
+ );
+ await getGlobalOK(["foo"], 1);
+ await reset();
+});
+
+add_task(async function domainsAll() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+ await set("b.com", "bar", 6);
+
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(0, null, makeCallback(resolve))
+ );
+ await dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], undefined);
+ await getOK(["b.com", "bar"], undefined);
+ await reset();
+});
+
+add_task(async function domainsWithDate() {
+ await setWithDate("a.com", "foobar", 0, 0);
+ await setWithDate("a.com", "foo", 1, 1000);
+ await setWithDate("a.com", "bar", 2, 4000);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await setWithDate("b.com", "foo", 5, 2000);
+ await setWithDate("b.com", "bar", 6, 3000);
+ await setWithDate("b.com", "foobar", 7, 1000);
+
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(2000, null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foobar", 0],
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foobar", 7],
+ ]);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ await new Promise(resolve =>
+ cps.removeAllDomainsSince(0, context, makeCallback(resolve))
+ );
+ await dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo", context], undefined);
+ await getOK(["a.com", "bar", context], undefined);
+ await getGlobalOK(["foo", context], 7);
+ await getGlobalOK(["bar", context], 4);
+ await getOK(["b.com", "foo", context], undefined);
+
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], undefined);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.removeAllDomainsSince(null, "bogus"));
+ await reset();
+});
+
+add_task(async function invalidateCache() {
+ await setWithDate("a.com", "foobar", 0, 0);
+ await setWithDate("a.com", "foo", 1, 1000);
+ await setWithDate("a.com", "bar", 2, 4000);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await setWithDate("b.com", "foo", 5, 2000);
+ await setWithDate("b.com", "bar", 6, 3000);
+ await setWithDate("b.com", "foobar", 7, 1000);
+ let clearPromise = new Promise(resolve => {
+ cps.removeAllDomainsSince(0, null, makeCallback(resolve));
+ });
+ getCachedOK(["a.com", "foobar"], false);
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["a.com", "bar"], false);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedOK(["b.com", "foo"], false);
+ getCachedOK(["b.com", "bar"], false);
+ getCachedOK(["b.com", "foobar"], false);
+ await clearPromise;
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
new file mode 100644
index 0000000000..b68d589dbf
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ await set("a.com", "foo", 1);
+ await setGlobal("foo", 2);
+
+ await new Promise(resolve =>
+ cps.removeByDomain("bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+
+ await new Promise(resolve =>
+ cps.removeBySubdomain("bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ await new Promise(resolve =>
+ cps.removeByDomain("a.com", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+
+ await set("a.com", "foo", 2);
+ await new Promise(resolve =>
+ cps.removeByDomain("http://a.com/huh", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+ await reset();
+});
+
+add_task(async function domains() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+ await set("b.com", "bar", 6);
+
+ await new Promise(resolve =>
+ cps.removeByDomain("a.com", null, makeCallback(resolve))
+ );
+ await dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], 5);
+ await getOK(["b.com", "bar"], 6);
+
+ await new Promise(resolve =>
+ cps.removeAllGlobals(null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], undefined);
+ await getOK(["b.com", "foo"], 5);
+ await getOK(["b.com", "bar"], 6);
+
+ await new Promise(resolve =>
+ cps.removeByDomain("b.com", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], undefined);
+ await getOK(["b.com", "foo"], undefined);
+ await getOK(["b.com", "bar"], undefined);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ await new Promise(resolve =>
+ cps.removeByDomain("a.com", null, makeCallback(resolve))
+ );
+ await dbOK([["b.a.com", "foo", 2]]);
+ await getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ await getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ await set("a.com", "foo", 3);
+ await new Promise(resolve =>
+ cps.removeBySubdomain("a.com", null, makeCallback(resolve))
+ );
+ await dbOK([]);
+ await getSubdomainsOK(["a.com", "foo"], []);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+
+ await set("a.com", "foo", 4);
+ await set("b.a.com", "foo", 5);
+ await new Promise(resolve =>
+ cps.removeByDomain("b.a.com", null, makeCallback(resolve))
+ );
+ await dbOK([["a.com", "foo", 4]]);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+
+ await set("b.a.com", "foo", 6);
+ await new Promise(resolve =>
+ cps.removeBySubdomain("b.a.com", null, makeCallback(resolve))
+ );
+ await dbOK([["a.com", "foo", 4]]);
+ await getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ await getSubdomainsOK(["b.a.com", "foo"], []);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await set("b.com", "foo", 7, context);
+ await setGlobal("foo", 8, context);
+ await new Promise(resolve =>
+ cps.removeByDomain("a.com", context, makeCallback(resolve))
+ );
+ await getOK(["b.com", "foo", context], 7);
+ await getGlobalOK(["foo", context], 8);
+ await new Promise(resolve =>
+ cps.removeAllGlobals(context, makeCallback(resolve))
+ );
+ await dbOK([["b.com", "foo", 5]]);
+ await getOK(["a.com", "foo", context], undefined);
+ await getOK(["a.com", "bar", context], undefined);
+ await getGlobalOK(["foo", context], undefined);
+ await getGlobalOK(["bar", context], undefined);
+ await getOK(["b.com", "foo", context], 5);
+
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], undefined);
+ await getOK(["b.com", "foo"], 5);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.removeByDomain(null, null));
+ do_check_throws(() => cps.removeByDomain("", null));
+ do_check_throws(() => cps.removeByDomain("a.com", null, "bogus"));
+ do_check_throws(() => cps.removeBySubdomain(null, null));
+ do_check_throws(() => cps.removeBySubdomain("", null));
+ do_check_throws(() => cps.removeBySubdomain("a.com", null, "bogus"));
+ do_check_throws(() => cps.removeAllGlobals(null, "bogus"));
+ await reset();
+});
+
+add_task(async function removeByDomain_invalidateCache() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeByDomain("a.com", null, makeCallback(resolve));
+ });
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["a.com", "bar"], false);
+ await promiseRemoved;
+ await reset();
+});
+
+add_task(async function removeBySubdomain_invalidateCache() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(
+ ["a.com", "foo"],
+ [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]
+ );
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeBySubdomain("a.com", null, makeCallback(resolve));
+ });
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ await promiseRemoved;
+ await reset();
+});
+
+add_task(async function removeAllGlobals_invalidateCache() {
+ await setGlobal("foo", 1);
+ await setGlobal("bar", 2);
+ getCachedGlobalOK(["foo"], true, 1);
+ getCachedGlobalOK(["bar"], true, 2);
+ let promiseRemoved = new Promise(resolve => {
+ cps.removeAllGlobals(null, makeCallback(resolve));
+ });
+ getCachedGlobalOK(["foo"], false);
+ getCachedGlobalOK(["bar"], false);
+ await promiseRemoved;
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
new file mode 100644
index 0000000000..b7fe310802
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function nonexistent() {
+ await set("a.com", "foo", 1);
+ await setGlobal("foo", 2);
+
+ await new Promise(resolve =>
+ cps.removeByName("bogus", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getGlobalOK(["foo"], 2);
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+ await set("b.com", "bar", 6);
+
+ await new Promise(resolve =>
+ cps.removeByName("foo", null, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 6],
+ ]);
+ await getOK(["a.com", "foo"], undefined);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], undefined);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], undefined);
+ await getOK(["b.com", "bar"], 6);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+ await set("b.com", "bar", 6);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 7, context);
+ await setGlobal("foo", 8, context);
+ await set("b.com", "bar", 9, context);
+ await new Promise(resolve =>
+ cps.removeByName("bar", context, makeCallback(resolve))
+ );
+ await dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ ["b.com", "foo", 5],
+ ]);
+ await getOK(["a.com", "foo", context], 7);
+ await getOK(["a.com", "bar", context], undefined);
+ await getGlobalOK(["foo", context], 8);
+ await getGlobalOK(["bar", context], undefined);
+ await getOK(["b.com", "foo", context], 5);
+ await getOK(["b.com", "bar", context], undefined);
+
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["a.com", "bar"], undefined);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], undefined);
+ await getOK(["b.com", "foo"], 5);
+ await getOK(["b.com", "bar"], undefined);
+ await reset();
+});
+
+add_task(async function erroneous() {
+ do_check_throws(() => cps.removeByName("", null));
+ do_check_throws(() => cps.removeByName(null, null));
+ do_check_throws(() => cps.removeByName("foo", null, "bogus"));
+ await reset();
+});
+
+add_task(async function invalidateCache() {
+ await set("a.com", "foo", 1);
+ await set("b.com", "foo", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.com", "foo"], true, 2);
+ cps.removeByName("foo", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["b.com", "foo"], false);
+ await reset();
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
new file mode 100644
index 0000000000..5b8ecb2bc0
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
@@ -0,0 +1,232 @@
+/* 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/. */
+
+add_task(async function resetBeforeTests() {
+ await reset();
+});
+
+add_task(async function get_nonexistent() {
+ await getOK(["a.com", "foo"], undefined);
+ await getGlobalOK(["foo"], undefined);
+ await reset();
+});
+
+add_task(async function isomorphicDomains() {
+ await set("a.com", "foo", 1);
+ await dbOK([["a.com", "foo", 1]]);
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["http://a.com/huh", "foo"], 1, "a.com");
+
+ await set("http://a.com/huh", "foo", 2);
+ await dbOK([["a.com", "foo", 2]]);
+ await getOK(["a.com", "foo"], 2);
+ await getOK(["http://a.com/yeah", "foo"], 2, "a.com");
+ await reset();
+});
+
+add_task(async function names() {
+ await set("a.com", "foo", 1);
+ await dbOK([["a.com", "foo", 1]]);
+ await getOK(["a.com", "foo"], 1);
+
+ await set("a.com", "bar", 2);
+ await dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["a.com", "bar"], 2);
+
+ await setGlobal("foo", 3);
+ await dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], 3);
+
+ await setGlobal("bar", 4);
+ await dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await reset();
+});
+
+add_task(async function subdomains() {
+ await set("a.com", "foo", 1);
+ await set("b.a.com", "foo", 2);
+ await dbOK([
+ ["a.com", "foo", 1],
+ ["b.a.com", "foo", 2],
+ ]);
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["b.a.com", "foo"], 2);
+ await reset();
+});
+
+add_task(async function privateBrowsing() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await setGlobal("foo", 3);
+ await setGlobal("bar", 4);
+ await set("b.com", "foo", 5);
+
+ let context = privateLoadContext;
+ await set("a.com", "foo", 6, context);
+ await setGlobal("foo", 7, context);
+ await dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ]);
+ await getOK(["a.com", "foo", context], 6, "a.com");
+ await getOK(["a.com", "bar", context], 2);
+ await getGlobalOK(["foo", context], 7);
+ await getGlobalOK(["bar", context], 4);
+ await getOK(["b.com", "foo", context], 5);
+
+ await getOK(["a.com", "foo"], 1);
+ await getOK(["a.com", "bar"], 2);
+ await getGlobalOK(["foo"], 3);
+ await getGlobalOK(["bar"], 4);
+ await getOK(["b.com", "foo"], 5);
+ await reset();
+});
+
+add_task(async function set_erroneous() {
+ do_check_throws(() => cps.set(null, "foo", 1, null));
+ do_check_throws(() => cps.set("", "foo", 1, null));
+ do_check_throws(() => cps.set("a.com", "", 1, null));
+ do_check_throws(() => cps.set("a.com", null, 1, null));
+ do_check_throws(() => cps.set("a.com", "foo", undefined, null));
+ do_check_throws(() => cps.set("a.com", "foo", 1, null, "bogus"));
+ do_check_throws(() => cps.setGlobal("", 1, null));
+ do_check_throws(() => cps.setGlobal(null, 1, null));
+ do_check_throws(() => cps.setGlobal("foo", undefined, null));
+ do_check_throws(() => cps.setGlobal("foo", 1, null, "bogus"));
+ await reset();
+});
+
+add_task(async function get_erroneous() {
+ do_check_throws(() => cps.getByDomainAndName(null, "foo", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("", "foo", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", "", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", null, null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", "foo", null, null));
+ do_check_throws(() => cps.getGlobal("", null, {}));
+ do_check_throws(() => cps.getGlobal(null, null, {}));
+ do_check_throws(() => cps.getGlobal("foo", null, null));
+ await reset();
+});
+
+add_task(async function set_invalidateCache() {
+ // (1) Set a pref and wait for it to finish.
+ await set("a.com", "foo", 1);
+
+ // (2) It should be cached.
+ getCachedOK(["a.com", "foo"], true, 1);
+
+ // (3) Set the pref to a new value but don't wait for it to finish.
+ cps.set("a.com", "foo", 2, null, {
+ handleCompletion() {
+ // (6) The pref should be cached after setting it.
+ getCachedOK(["a.com", "foo"], true, 2);
+ },
+ });
+
+ // (4) Group "a.com" and name "foo" should no longer be cached.
+ getCachedOK(["a.com", "foo"], false);
+
+ // (5) Call getByDomainAndName.
+ let fetchedPref;
+ let getPromise = new Promise(resolve =>
+ cps.getByDomainAndName("a.com", "foo", null, {
+ handleResult(pref) {
+ fetchedPref = pref;
+ },
+ handleCompletion() {
+ // (7) Finally, this callback should be called after set's above.
+ Assert.ok(!!fetchedPref);
+ Assert.equal(fetchedPref.value, 2);
+ resolve();
+ },
+ })
+ );
+
+ await getPromise;
+ await reset();
+});
+
+add_task(async function get_nameOnly() {
+ await set("a.com", "foo", 1);
+ await set("a.com", "bar", 2);
+ await set("b.com", "foo", 3);
+ await setGlobal("foo", 4);
+
+ await getOKEx(
+ "getByName",
+ ["foo", undefined],
+ [
+ { domain: "a.com", name: "foo", value: 1 },
+ { domain: "b.com", name: "foo", value: 3 },
+ { domain: null, name: "foo", value: 4 },
+ ]
+ );
+
+ let context = privateLoadContext;
+ await set("b.com", "foo", 5, context);
+
+ await getOKEx(
+ "getByName",
+ ["foo", context],
+ [
+ { domain: "a.com", name: "foo", value: 1 },
+ { domain: null, name: "foo", value: 4 },
+ { domain: "b.com", name: "foo", value: 5 },
+ ]
+ );
+ await reset();
+});
+
+add_task(async function setSetsCurrentDate() {
+ // Because Date.now() is not guaranteed to be monotonically increasing
+ // we just do here rough sanity check with one minute tolerance.
+ const MINUTE = 60 * 1000;
+ let now = Date.now();
+ let start = now - MINUTE;
+ let end = now + MINUTE;
+ await set("a.com", "foo", 1);
+ let timestamp = await getDate("a.com", "foo");
+ ok(
+ start <= timestamp,
+ "Timestamp is not too early (" + start + "<=" + timestamp + ")."
+ );
+ ok(
+ timestamp <= end,
+ "Timestamp is not too late (" + timestamp + "<=" + end + ")."
+ );
+ await reset();
+});
+
+add_task(async function maxLength() {
+ const MAX_LENGTH = Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH;
+ const LONG_DATA_URL = `data:,${new Array(MAX_LENGTH).fill("x").join("")}`;
+ await set(LONG_DATA_URL, "foo", 1);
+ await getOK(
+ [LONG_DATA_URL, "foo"],
+ 1,
+ LONG_DATA_URL.substring(0, MAX_LENGTH - 1)
+ );
+});
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
new file mode 100644
index 0000000000..41e18191e5
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = head.js
+skip-if = toolkit == 'android'
+support-files = AsyncRunner.sys.mjs
+
+[test_setGet.js]
+[test_getSubdomains.js]
+[test_remove.js]
+[test_removeByDomain.js]
+[test_removeAllDomains.js]
+[test_removeByName.js]
+[test_getCached.js]
+[test_getCachedSubdomains.js]
+[test_observers.js]
+[test_extractDomain.js]
+[test_migrationToSchema4.js]
+[test_migrationToSchema5.js]
+[test_removeAllDomainsSince.js]