summaryrefslogtreecommitdiffstats
path: root/toolkit/components/contentprefs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/contentprefs')
-rw-r--r--toolkit/components/contentprefs/ContentPrefInstance.jsm75
-rw-r--r--toolkit/components/contentprefs/ContentPrefService2.jsm885
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceChild.jsm182
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceParent.jsm137
-rw-r--r--toolkit/components/contentprefs/ContentPrefStore.jsm123
-rw-r--r--toolkit/components/contentprefs/ContentPrefUtils.jsm70
-rw-r--r--toolkit/components/contentprefs/moz.build31
-rw-r--r--toolkit/components/contentprefs/nsContentPrefService.js1332
-rw-r--r--toolkit/components/contentprefs/nsContentPrefService.manifest5
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/mochitest.ini4
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html311
-rw-r--r--toolkit/components/contentprefs/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/unit/head_contentPrefs.js162
-rw-r--r--toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js6
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug248970.js42
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug503971.js35
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug679784.js103
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_contentPrefs.js463
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js244
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js34
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_stringGroups.js128
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js52
-rw-r--r--toolkit/components/contentprefs/tests/unit/xpcshell.ini12
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm69
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/head.js401
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js20
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js95
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js186
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js68
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js82
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_observers.js178
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_remove.js222
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js87
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js111
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js199
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js96
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_service.js12
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js206
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini19
41 files changed, 6508 insertions, 0 deletions
diff --git a/toolkit/components/contentprefs/ContentPrefInstance.jsm b/toolkit/components/contentprefs/ContentPrefInstance.jsm
new file mode 100644
index 000000000..395569995
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefInstance.jsm
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ['ContentPrefInstance'];
+
+// This is a wrapper for nsIContentPrefService that alleviates the need to pass
+// an nsILoadContext argument to every method. Pass the context to the constructor
+// instead and continue on your way in blissful ignorance.
+
+this.ContentPrefInstance = function ContentPrefInstance(aContext) {
+ this._contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService);
+ this._context = aContext;
+};
+
+ContentPrefInstance.prototype = {
+ getPref: function ContentPrefInstance_init(aName, aGroup, aCallback) {
+ return this._contentPrefSvc.getPref(aName, aGroup, this._context, aCallback);
+ },
+
+ setPref: function ContentPrefInstance_setPref(aGroup, aName, aValue, aContext) {
+ return this._contentPrefSvc.setPref(aGroup, aName, aValue,
+ aContext ? aContext : this._context);
+ },
+
+ hasPref: function ContentPrefInstance_hasPref(aGroup, aName) {
+ return this._contentPrefSvc.hasPref(aGroup, aName, this._context);
+ },
+
+ hasCachedPref: function ContentPrefInstance_hasCachedPref(aGroup, aName) {
+ return this._contentPrefSvc.hasCachedPref(aGroup, aName, this._context);
+ },
+
+ removePref: function ContentPrefInstance_removePref(aGroup, aName) {
+ return this._contentPrefSvc.removePref(aGroup, aName, this._context);
+ },
+
+ removeGroupedPrefs: function ContentPrefInstance_removeGroupedPrefs() {
+ return this._contentPrefSvc.removeGroupedPrefs(this._context);
+ },
+
+ removePrefsByName: function ContentPrefInstance_removePrefsByName(aName) {
+ return this._contentPrefSvc.removePrefsByName(aName, this._context);
+ },
+
+ getPrefs: function ContentPrefInstance_getPrefs(aGroup) {
+ return this._contentPrefSvc.getPrefs(aGroup, this._context);
+ },
+
+ getPrefsByName: function ContentPrefInstance_getPrefsByName(aName) {
+ return this._contentPrefSvc.getPrefsByName(aName, this._context);
+ },
+
+ addObserver: function ContentPrefInstance_addObserver(aName, aObserver) {
+ return this._contentPrefSvc.addObserver(aName, aObserver);
+ },
+
+ removeObserver: function ContentPrefInstance_removeObserver(aName, aObserver) {
+ return this._contentPrefSvc.removeObserver(aName, aObserver);
+ },
+
+ get grouper() {
+ return this._contentPrefSvc.grouper;
+ },
+
+ get DBConnection() {
+ return this._contentPrefSvc.DBConnection;
+ }
+};
diff --git a/toolkit/components/contentprefs/ContentPrefService2.jsm b/toolkit/components/contentprefs/ContentPrefService2.jsm
new file mode 100644
index 000000000..87063d170
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefService2.jsm
@@ -0,0 +1,885 @@
+/* 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/. */
+
+// This file is an XPCOM component that implements nsIContentPrefService2.
+// Although it's a JSM, it's not intended to be imported by consumers like JSMs
+// are usually imported. It's only a JSM so that nsContentPrefService.js can
+// easily use it. Consumers should access this component with the usual XPCOM
+// rigmarole:
+//
+// Cc["@mozilla.org/content-pref/service;1"].
+// getService(Ci.nsIContentPrefService2);
+//
+// That contract ID actually belongs to nsContentPrefService.js, which, when
+// QI'ed to nsIContentPrefService2, returns an instance of this component.
+//
+// The plan is to eventually remove nsIContentPrefService and its
+// implementation, nsContentPrefService.js. At such time this file can stop
+// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and
+// NSGetFactory and all the other XPCOM initialization goop in
+// nsContentPrefService.js can be moved here.
+//
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859
+
+var EXPORTED_SYMBOLS = [
+ "ContentPrefService2",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+const GROUP_CLAUSE = `
+ SELECT id
+ FROM groups
+ WHERE name = :group OR
+ (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
+`;
+
+function ContentPrefService2(cps) {
+ this._cps = cps;
+ this._cache = cps._cache;
+ this._pbStore = cps._privModeStorage;
+}
+
+ContentPrefService2.prototype = {
+
+ 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: function 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: function onDone(reason, ok, gotRow) {
+ if (ok) {
+ for (let [pbGroup, pbName, pbVal] of pbPrefs) {
+ cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ },
+ onError: function 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: function 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: function 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: function 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);
+ stmt.params.group = group;
+ stmt.params.includeSubdomains = includeSubdomains || false;
+ stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/");
+ 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,
+ len) {
+ checkGroupArg(group);
+ let prefs = this._getCached(group, name, true, context);
+ if (len)
+ len.value = prefs.length;
+ return prefs;
+ },
+
+ 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._cps._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: function onDone(reason, ok) {
+ if (ok)
+ this._cache.setWithCast(group, name, value);
+ cbHandleCompletion(callback, reason);
+ if (ok)
+ this._cps._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
+ },
+ onError: function 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: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function 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._cps._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ // Deletes settings and groups that are no longer used.
+ _settingsAndGroupsCleanupStmts: function() {
+ // 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: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let name = row.getResultByName("name");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function 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._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: function 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: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let name = row.getResultByName("name");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function 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._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: function 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: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function 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._cps._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ destroy: function CPS2_destroy() {
+ if (this._statements) {
+ for (let sql in this._statements) {
+ let stmt = this._statements[sql];
+ stmt.finalize();
+ }
+ }
+ },
+
+ /**
+ * 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) {
+ if (!this._statements)
+ this._statements = {};
+ if (!this._statements[sql])
+ this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql);
+ return this._statements[sql];
+ },
+
+ /**
+ * 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: function CPS2__execStmts(stmts, callbacks) {
+ let self = this;
+ let gotRow = false;
+ this._cps._dbConnection.executeAsync(stmts, stmts.length, {
+ handleResult: function handleResult(results) {
+ try {
+ let row = null;
+ while ((row = results.getNextRow())) {
+ gotRow = true;
+ if (callbacks.onRow)
+ callbacks.onRow.call(self, row);
+ }
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ },
+ handleCompletion: function handleCompletion(reason) {
+ try {
+ let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED;
+ callbacks.onDone.call(self,
+ ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
+ Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+ ok, gotRow);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ },
+ handleError: function handleError(error) {
+ try {
+ if (callbacks.onError)
+ callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ }
+ });
+ },
+
+ /**
+ * 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.
+ */
+ _parseGroup: function CPS2__parseGroup(groupStr) {
+ if (!groupStr)
+ return null;
+ try {
+ var groupURI = Services.io.newURI(groupStr, null, null);
+ }
+ catch (err) {
+ return groupStr;
+ }
+ return this._cps._grouper.group(groupURI);
+ },
+
+ _schedule: function CPS2__schedule(fn) {
+ Services.tm.mainThread.dispatch(fn.bind(this),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ addObserverForName: function CPS2_addObserverForName(name, observer) {
+ this._cps._addObserver(name, observer);
+ },
+
+ removeObserverForName: function CPS2_removeObserverForName(name, observer) {
+ this._cps._removeObserver(name, observer);
+ },
+
+ 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 "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._cps._dbConnection;
+ break;
+ }
+ },
+
+ /**
+ * Removes all state from the service. Used by tests.
+ *
+ * @param callback A function that will be called when done.
+ */
+ _reset: function CPS2__reset(callback) {
+ this._pbStore.removeAll();
+ this._cache.removeAll();
+
+ let cps = this._cps;
+ cps._observers = {};
+ cps._genericObservers = [];
+
+ let tables = ["prefs", "groups", "settings"];
+ let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
+ this._execStmts(stmts, { onDone: () => callback() });
+ },
+
+ QueryInterface: function CPS2_QueryInterface(iid) {
+ let supportedIIDs = [
+ Ci.nsIContentPrefService2,
+ Ci.nsIObserver,
+ Ci.nsISupports,
+ ];
+ if (supportedIIDs.some(i => iid.equals(i)))
+ return this;
+ if (iid.equals(Ci.nsIContentPrefService))
+ return this._cps;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+};
+
+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);
+}
diff --git a/toolkit/components/contentprefs/ContentPrefServiceChild.jsm b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm
new file mode 100644
index 000000000..7faca8970
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm
@@ -0,0 +1,182 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceChild" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+// 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: function(contentPref) {
+ cbHandleResult(this._callback,
+ new ContentPref(contentPref.domain,
+ contentPref.name,
+ contentPref.value));
+ },
+
+ handleError: function(result) {
+ cbHandleError(this._callback, result);
+ },
+
+ handleCompletion: function(reason) {
+ cbHandleCompletion(this._callback, reason);
+ },
+};
+
+var ContentPrefServiceChild = {
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIContentPrefService2 ]),
+
+ // Map from pref name -> set of observers
+ _observers: new Map(),
+
+ _mm: Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageSender),
+
+ _getRandomId: function() {
+ return Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ },
+
+ // Map from random ID string -> CallbackCaller, per request
+ _requests: new Map(),
+
+ init: function() {
+ this._mm.addMessageListener("ContentPrefs:HandleResult", this);
+ this._mm.addMessageListener("ContentPrefs:HandleError", this);
+ this._mm.addMessageListener("ContentPrefs:HandleCompletion", this);
+ },
+
+ receiveMessage: function(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: function(call, args, callback) {
+ let requestId = this._getRandomId();
+ let data = { call: call, args: args, requestId: requestId };
+
+ this._mm.sendAsyncMessage("ContentPrefs:FunctionCall", data);
+
+ this._requests.set(requestId, new CallbackCaller(callback));
+ },
+
+ getCachedByDomainAndName: NYI,
+ getCachedBySubdomainAndName: NYI,
+ getCachedGlobal: NYI,
+
+ addObserverForName: function(name, observer) {
+ let set = this._observers.get(name);
+ if (!set) {
+ set = new Set();
+ if (this._observers.size === 0) {
+ // This is the first observer of any kind. Start listening for changes.
+ this._mm.addMessageListener("ContentPrefs:NotifyObservers", this);
+ }
+
+ // This is the first observer for this name. Start listening for changes
+ // to it.
+ this._mm.sendAsyncMessage("ContentPrefs:AddObserverForName", { name: name });
+ this._observers.set(name, set);
+ }
+
+ set.add(observer);
+ },
+
+ removeObserverForName: function(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._mm.sendAsyncMessage("ContentPrefs:RemoveObserverForName", { name: name });
+
+ this._observers.delete(name);
+ if (this._observers.size === 0) {
+ // This was the last observer for this process. Stop listing for all
+ // changes.
+ this._mm.removeMessageListener("ContentPrefs:NotifyObservers", this);
+ }
+ }
+ },
+
+ 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];
+ }
+ this._callFunction(method, args, callback);
+}
+
+for (let [method, signature] of _methodsCallableFromChild) {
+ ContentPrefServiceChild[method] = forwardMethodToParent.bind(ContentPrefServiceChild, method, signature);
+}
+
+ContentPrefServiceChild.init();
diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.jsm b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm
new file mode 100644
index 000000000..32e31a789
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm
@@ -0,0 +1,137 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceParent" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+
+var ContentPrefServiceParent = {
+ _cps2: null,
+
+ init: function() {
+ let globalMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+ this._cps2 = Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2);
+
+ globalMM.addMessageListener("ContentPrefs:FunctionCall", this);
+
+ let observerChangeHandler = this.handleObserverChange.bind(this);
+ globalMM.addMessageListener("ContentPrefs:AddObserverForName", observerChangeHandler);
+ globalMM.addMessageListener("ContentPrefs:RemoveObserverForName", observerChangeHandler);
+ globalMM.addMessageListener("child-process-shutdown", observerChangeHandler);
+ },
+
+ // Map from message manager -> content pref observer.
+ _observers: new Map(),
+
+ handleObserverChange: function(msg) {
+ let observer = this._observers.get(msg.target);
+ if (msg.name === "child-process-shutdown") {
+ // If we didn't have any observers for this child process, don't do
+ // anything.
+ if (!observer)
+ return;
+
+ for (let i of observer._names) {
+ this._cps2.removeObserverForName(i, observer);
+ }
+
+ this._observers.delete(msg.target);
+ return;
+ }
+
+ let prefName = msg.data.name;
+ if (msg.name === "ContentPrefs:AddObserverForName") {
+ // The child process is responsible for not adding multiple parent
+ // observers for the same name.
+ if (!observer) {
+ observer = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+ { name: name, callback: "onContentPrefSet",
+ args: [ group, name, value, isPrivate ] });
+ },
+
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+ { name: name, callback: "onContentPrefRemoved",
+ args: [ group, name, isPrivate ] });
+ },
+
+ // 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.
+ _names: new Set()
+ };
+
+ this._observers.set(msg.target, observer);
+ }
+
+ observer._names.add(prefName);
+
+ this._cps2.addObserverForName(prefName, observer);
+ } else {
+ // RemoveObserverForName
+
+ // We must have an observer.
+ this._cps2.removeObserverForName(prefName, observer);
+
+ observer._names.delete(prefName);
+ if (observer._names.size === 0) {
+ // This was the last use for this observer.
+ this._observers.delete(msg.target);
+ }
+ }
+ },
+
+ receiveMessage: function(msg) {
+ let data = msg.data;
+
+ if (!_methodsCallableFromChild.some(([method, args]) => method == data.call)) {
+ throw new Error(`Can't call ${data.call} from child!`);
+ }
+
+ let args = data.args;
+ let requestId = data.requestId;
+
+ let listener = {
+ handleResult: function(pref) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleResult",
+ { requestId: requestId,
+ contentPref: {
+ domain: pref.domain,
+ name: pref.name,
+ value: pref.value
+ }
+ });
+ },
+
+ handleError: function(error) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleError",
+ { requestId: requestId,
+ error: error });
+ },
+ handleCompletion: function(reason) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleCompletion",
+ { requestId: requestId,
+ reason: reason });
+ }
+ };
+
+ // Push our special listener.
+ args.push(listener);
+
+ // And call the function.
+ this._cps2[data.call](...args);
+ }
+};
diff --git a/toolkit/components/contentprefs/ContentPrefStore.jsm b/toolkit/components/contentprefs/ContentPrefStore.jsm
new file mode 100644
index 000000000..7a552662f
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefStore.jsm
@@ -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/. */
+
+var EXPORTED_SYMBOLS = [
+ "ContentPrefStore",
+];
+
+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.jsm b/toolkit/components/contentprefs/ContentPrefUtils.jsm
new file mode 100644
index 000000000..557872e1a
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefUtils.jsm
@@ -0,0 +1,70 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "ContentPref",
+ "cbHandleResult",
+ "cbHandleError",
+ "cbHandleCompletion",
+ "safeCallback",
+ "_methodsCallableFromChild",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPref(domain, name, value) {
+ this.domain = domain;
+ this.name = name;
+ this.value = value;
+}
+
+ContentPref.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]),
+};
+
+function cbHandleResult(callback, pref) {
+ safeCallback(callback, "handleResult", [pref]);
+}
+
+function cbHandleCompletion(callback, reason) {
+ safeCallback(callback, "handleCompletion", [reason]);
+}
+
+function cbHandleError(callback, nsresult) {
+ safeCallback(callback, "handleError", [nsresult]);
+}
+
+function safeCallback(callbackObj, methodName, args) {
+ if (!callbackObj || typeof(callbackObj[methodName]) != "function")
+ return;
+ try {
+ callbackObj[methodName].apply(callbackObj, args);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+}
+
+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/moz.build b/toolkit/components/contentprefs/moz.build
new file mode 100644
index 000000000..24bb296f1
--- /dev/null
+++ b/toolkit/components/contentprefs/moz.build
@@ -0,0 +1,31 @@
+# -*- 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/xpcshell.ini',
+ 'tests/unit_cps2/xpcshell.ini',
+]
+
+MOCHITEST_MANIFESTS += [
+ 'tests/mochitest/mochitest.ini'
+]
+
+EXTRA_COMPONENTS += [
+ 'nsContentPrefService.js',
+ 'nsContentPrefService.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'ContentPrefInstance.jsm',
+ 'ContentPrefService2.jsm',
+ 'ContentPrefServiceChild.jsm',
+ 'ContentPrefServiceParent.jsm',
+ 'ContentPrefStore.jsm',
+ 'ContentPrefUtils.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Preferences')
diff --git a/toolkit/components/contentprefs/nsContentPrefService.js b/toolkit/components/contentprefs/nsContentPrefService.js
new file mode 100644
index 000000000..6360134a5
--- /dev/null
+++ b/toolkit/components/contentprefs/nsContentPrefService.js
@@ -0,0 +1,1332 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+const CACHE_MAX_GROUP_ENTRIES = 100;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPrefService() {
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm")
+ .ContentPrefServiceChild;
+ }
+
+ // If this throws an exception, it causes the getService call to fail,
+ // but the next time a consumer tries to retrieve the service, we'll try
+ // to initialize the database again, which might work if the failure
+ // was due to a temporary condition (like being out of disk space).
+ this._dbInit();
+
+ this._observerSvc.addObserver(this, "last-pb-context-exited", false);
+
+ // Observe shutdown so we can shut down the database connection.
+ this._observerSvc.addObserver(this, "xpcom-shutdown", false);
+}
+
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+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();
+
+ContentPrefService.prototype = {
+ // XPCOM Plumbing
+
+ classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
+
+ QueryInterface: function CPS_QueryInterface(iid) {
+ let supportedIIDs = [
+ Ci.nsIContentPrefService,
+ Ci.nsISupports,
+ ];
+ if (supportedIIDs.some(i => iid.equals(i)))
+ return this;
+ if (iid.equals(Ci.nsIContentPrefService2)) {
+ if (!this._contentPrefService2) {
+ let s = {};
+ Cu.import("resource://gre/modules/ContentPrefService2.jsm", s);
+ this._contentPrefService2 = new s.ContentPrefService2(this);
+ }
+ return this._contentPrefService2;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ // Convenience Getters
+
+ // Observer Service
+ __observerSvc: null,
+ get _observerSvc() {
+ if (!this.__observerSvc)
+ this.__observerSvc = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ return this.__observerSvc;
+ },
+
+ // Console Service
+ __consoleSvc: null,
+ get _consoleSvc() {
+ if (!this.__consoleSvc)
+ this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService);
+ return this.__consoleSvc;
+ },
+
+ // Preferences Service
+ __prefSvc: null,
+ get _prefSvc() {
+ if (!this.__prefSvc)
+ this.__prefSvc = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ return this.__prefSvc;
+ },
+
+
+ // Destruction
+
+ _destroy: function ContentPrefService__destroy() {
+ this._observerSvc.removeObserver(this, "xpcom-shutdown");
+ this._observerSvc.removeObserver(this, "last-pb-context-exited");
+
+ // Finalize statements which may have been used asynchronously.
+ // FIXME(696499): put them in an object cache like other components.
+ if (this.__stmtSelectPrefID) {
+ this.__stmtSelectPrefID.finalize();
+ this.__stmtSelectPrefID = null;
+ }
+ if (this.__stmtSelectGlobalPrefID) {
+ this.__stmtSelectGlobalPrefID.finalize();
+ this.__stmtSelectGlobalPrefID = null;
+ }
+ if (this.__stmtInsertPref) {
+ this.__stmtInsertPref.finalize();
+ this.__stmtInsertPref = null;
+ }
+ if (this.__stmtInsertGroup) {
+ this.__stmtInsertGroup.finalize();
+ this.__stmtInsertGroup = null;
+ }
+ if (this.__stmtInsertSetting) {
+ this.__stmtInsertSetting.finalize();
+ this.__stmtInsertSetting = null;
+ }
+ if (this.__stmtSelectGroupID) {
+ this.__stmtSelectGroupID.finalize();
+ this.__stmtSelectGroupID = null;
+ }
+ if (this.__stmtSelectSettingID) {
+ this.__stmtSelectSettingID.finalize();
+ this.__stmtSelectSettingID = null;
+ }
+ if (this.__stmtSelectPref) {
+ this.__stmtSelectPref.finalize();
+ this.__stmtSelectPref = null;
+ }
+ if (this.__stmtSelectGlobalPref) {
+ this.__stmtSelectGlobalPref.finalize();
+ this.__stmtSelectGlobalPref = null;
+ }
+ if (this.__stmtSelectPrefsByName) {
+ this.__stmtSelectPrefsByName.finalize();
+ this.__stmtSelectPrefsByName = null;
+ }
+ if (this.__stmtDeleteSettingIfUnused) {
+ this.__stmtDeleteSettingIfUnused.finalize();
+ this.__stmtDeleteSettingIfUnused = null;
+ }
+ if (this.__stmtSelectPrefs) {
+ this.__stmtSelectPrefs.finalize();
+ this.__stmtSelectPrefs = null;
+ }
+ if (this.__stmtDeleteGroupIfUnused) {
+ this.__stmtDeleteGroupIfUnused.finalize();
+ this.__stmtDeleteGroupIfUnused = null;
+ }
+ if (this.__stmtDeletePref) {
+ this.__stmtDeletePref.finalize();
+ this.__stmtDeletePref = null;
+ }
+ if (this.__stmtUpdatePref) {
+ this.__stmtUpdatePref.finalize();
+ this.__stmtUpdatePref = null;
+ }
+
+ if (this._contentPrefService2)
+ this._contentPrefService2.destroy();
+
+ this._dbConnection.asyncClose();
+
+ // 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;
+ delete this.__consoleSvc;
+ delete this.__grouper;
+ delete this.__observerSvc;
+ delete this.__prefSvc;
+ },
+
+
+ // nsIObserver
+
+ observe: function ContentPrefService_observe(subject, topic, data) {
+ switch (topic) {
+ case "xpcom-shutdown":
+ this._destroy();
+ break;
+ case "last-pb-context-exited":
+ this._privModeStorage.removeAll();
+ break;
+ }
+ },
+
+
+ // in-memory cache and private-browsing stores
+
+ _cache: cache,
+ _privModeStorage: privModeStorage,
+
+ // nsIContentPrefService
+
+ getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ if (this._privModeStorage.has(group, aName)) {
+ let value = this._privModeStorage.get(group, aName);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+ // if we don't have a pref specific to this private mode browsing
+ // session, to try to get one from normal mode
+ }
+
+ if (group == null)
+ return this._selectGlobalPref(aName, aCallback);
+ return this._selectPref(group, aName, aCallback);
+ },
+
+ setPref: function ContentPrefService_setPref(aGroup, aName, aValue, aContext) {
+ warnDeprecated();
+
+ // If the pref is already set to the value, there's nothing more to do.
+ var currentValue = this.getPref(aGroup, aName, aContext);
+ if (typeof currentValue != "undefined") {
+ if (currentValue == aValue)
+ return;
+ }
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ this._privModeStorage.setWithCast(group, aName, aValue);
+ this._notifyPrefSet(group, aName, aValue, aContext.usePrivateBrowsing);
+ return;
+ }
+
+ var settingID = this._selectSettingID(aName) || this._insertSetting(aName);
+ var groupID, prefID;
+ if (group == null) {
+ groupID = null;
+ prefID = this._selectGlobalPrefID(settingID);
+ }
+ else {
+ groupID = this._selectGroupID(group) || this._insertGroup(group);
+ prefID = this._selectPrefID(groupID, settingID);
+ }
+
+ // Update the existing record, if any, or create a new one.
+ if (prefID)
+ this._updatePref(prefID, aValue);
+ else
+ this._insertPref(groupID, settingID, aValue);
+
+ this._cache.setWithCast(group, aName, aValue);
+
+ this._notifyPrefSet(group, aName, aValue,
+ aContext ? aContext.usePrivateBrowsing : false);
+ },
+
+ hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ // XXX If consumers end up calling this method regularly, then we should
+ // optimize this to query the database directly.
+ return (typeof this.getPref(aGroup, aName, aContext) != "undefined");
+ },
+
+ hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ let group = this._parseGroupParam(aGroup);
+ let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage: this._cache;
+ return storage.has(group, aName);
+ },
+
+ removePref: function ContentPrefService_removePref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ // If there's no old value, then there's nothing to remove.
+ if (!this.hasPref(aGroup, aName, aContext))
+ return;
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ this._privModeStorage.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, true);
+ return;
+ }
+
+ var settingID = this._selectSettingID(aName);
+ var groupID, prefID;
+ if (group == null) {
+ groupID = null;
+ prefID = this._selectGlobalPrefID(settingID);
+ }
+ else {
+ groupID = this._selectGroupID(group);
+ prefID = this._selectPrefID(groupID, settingID);
+ }
+
+ this._deletePref(prefID);
+
+ // Get rid of extraneous records that are no longer being used.
+ this._deleteSettingIfUnused(settingID);
+ if (groupID)
+ this._deleteGroupIfUnused(groupID);
+
+ this._cache.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, false);
+ },
+
+ removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) {
+ warnDeprecated();
+
+ // will not delete global preferences
+ if (aContext && aContext.usePrivateBrowsing) {
+ // keep only global prefs
+ this._privModeStorage.removeAllGroups();
+ }
+ this._cache.removeAllGroups();
+ this._dbConnection.beginTransaction();
+ try {
+ this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL");
+ this._dbConnection.executeSimpleSQL("DELETE FROM groups");
+ this._dbConnection.executeSimpleSQL(`
+ DELETE FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+ this._dbConnection.commitTransaction();
+ }
+ catch (ex) {
+ this._dbConnection.rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ removePrefsByName: function ContentPrefService_removePrefsByName(aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ for (let [group, name, ] of this._privModeStorage) {
+ if (name === aName) {
+ this._privModeStorage.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, true);
+ }
+ }
+ }
+
+ var settingID = this._selectSettingID(aName);
+ if (!settingID)
+ return;
+
+ var selectGroupsStmt = this._dbCreateStatement(`
+ SELECT groups.id AS groupID, groups.name AS groupName
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ WHERE prefs.settingID = :setting
+ `);
+
+ var groupNames = [];
+ var groupIDs = [];
+ try {
+ selectGroupsStmt.params.setting = settingID;
+
+ while (selectGroupsStmt.executeStep()) {
+ groupIDs.push(selectGroupsStmt.row["groupID"]);
+ groupNames.push(selectGroupsStmt.row["groupName"]);
+ }
+ }
+ finally {
+ selectGroupsStmt.reset();
+ }
+
+ if (this.hasPref(null, aName)) {
+ groupNames.push(null);
+ }
+
+ this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID);
+ this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID);
+
+ for (var i = 0; i < groupNames.length; i++) {
+ this._cache.remove(groupNames[i], aName);
+ if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
+ this._deleteGroupIfUnused(groupIDs[i]);
+ if (!aContext || !aContext.usePrivateBrowsing) {
+ this._notifyPrefRemoved(groupNames[i], aName, false);
+ }
+ }
+ },
+
+ getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) {
+ warnDeprecated();
+
+ var group = this._parseGroupParam(aGroup);
+ if (aContext && aContext.usePrivateBrowsing) {
+ let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let [sgroup, sname, sval] of this._privModeStorage) {
+ if (sgroup === group)
+ prefs.setProperty(sname, sval);
+ }
+ return prefs;
+ }
+
+ if (group == null)
+ return this._selectGlobalPrefs();
+ return this._selectPrefs(group);
+ },
+
+ getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let [sgroup, sname, sval] of this._privModeStorage) {
+ if (sname === aName)
+ prefs.setProperty(sgroup, sval);
+ }
+ return prefs;
+ }
+
+ return this._selectPrefsByName(aName);
+ },
+
+ // A hash of arrays of observers, indexed by setting name.
+ _observers: {},
+
+ // An array of generic observers, which observe all settings.
+ _genericObservers: [],
+
+ addObserver: function ContentPrefService_addObserver(aName, aObserver) {
+ warnDeprecated();
+ this._addObserver.apply(this, arguments);
+ },
+
+ _addObserver: function ContentPrefService__addObserver(aName, aObserver) {
+ var observers;
+ if (aName) {
+ if (!this._observers[aName])
+ this._observers[aName] = [];
+ observers = this._observers[aName];
+ }
+ else
+ observers = this._genericObservers;
+
+ if (observers.indexOf(aObserver) == -1)
+ observers.push(aObserver);
+ },
+
+ removeObserver: function ContentPrefService_removeObserver(aName, aObserver) {
+ warnDeprecated();
+ this._removeObserver.apply(this, arguments);
+ },
+
+ _removeObserver: function ContentPrefService__removeObserver(aName, aObserver) {
+ var observers;
+ if (aName) {
+ if (!this._observers[aName])
+ return;
+ observers = this._observers[aName];
+ }
+ else
+ observers = this._genericObservers;
+
+ if (observers.indexOf(aObserver) != -1)
+ observers.splice(observers.indexOf(aObserver), 1);
+ },
+
+ /**
+ * 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: function ContentPrefService__getObservers(aName) {
+ var observers = [];
+
+ if (aName && this._observers[aName])
+ observers = observers.concat(this._observers[aName]);
+ observers = observers.concat(this._genericObservers);
+
+ return observers;
+ },
+
+ /**
+ * 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) {
+ Cu.reportError(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) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ get grouper() {
+ warnDeprecated();
+ return this._grouper;
+ },
+ __grouper: null,
+ get _grouper() {
+ if (!this.__grouper)
+ this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
+ getService(Ci.nsIContentURIGrouper);
+ return this.__grouper;
+ },
+
+ get DBConnection() {
+ warnDeprecated();
+ return this._dbConnection;
+ },
+
+
+ // Data Retrieval & Modification
+
+ __stmtSelectPref: null,
+ get _stmtSelectPref() {
+ if (!this.__stmtSelectPref)
+ this.__stmtSelectPref = this._dbCreateStatement(`
+ SELECT prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE groups.name = :group
+ AND settings.name = :setting
+ `);
+
+ return this.__stmtSelectPref;
+ },
+
+ _scheduleCallback: function(func) {
+ let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+ tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) {
+ let value = undefined;
+ if (this._cache.has(aGroup, aSetting)) {
+ value = this._cache.get(aGroup, aSetting);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+
+ try {
+ this._stmtSelectPref.params.group = aGroup;
+ this._stmtSelectPref.params.setting = aSetting;
+
+ if (aCallback) {
+ let cache = this._cache;
+ new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) {
+ cache.set(aGroup, aSetting, aResult);
+ aCallback.onResult(aResult);
+ }});
+ }
+ else {
+ if (this._stmtSelectPref.executeStep()) {
+ value = this._stmtSelectPref.row["value"];
+ }
+ this._cache.set(aGroup, aSetting, value);
+ }
+ }
+ finally {
+ this._stmtSelectPref.reset();
+ }
+
+ return value;
+ },
+
+ __stmtSelectGlobalPref: null,
+ get _stmtSelectGlobalPref() {
+ if (!this.__stmtSelectGlobalPref)
+ this.__stmtSelectGlobalPref = this._dbCreateStatement(`
+ SELECT prefs.value AS value
+ FROM prefs
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE prefs.groupID IS NULL
+ AND settings.name = :name
+ `);
+
+ return this.__stmtSelectGlobalPref;
+ },
+
+ _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) {
+ let value = undefined;
+ if (this._cache.has(null, aName)) {
+ value = this._cache.get(null, aName);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+
+ try {
+ this._stmtSelectGlobalPref.params.name = aName;
+
+ if (aCallback) {
+ let cache = this._cache;
+ new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) {
+ cache.set(null, aName, aResult);
+ aCallback.onResult(aResult);
+ }});
+ }
+ else {
+ if (this._stmtSelectGlobalPref.executeStep()) {
+ value = this._stmtSelectGlobalPref.row["value"];
+ }
+ this._cache.set(null, aName, value);
+ }
+ }
+ finally {
+ this._stmtSelectGlobalPref.reset();
+ }
+
+ return value;
+ },
+
+ __stmtSelectGroupID: null,
+ get _stmtSelectGroupID() {
+ if (!this.__stmtSelectGroupID)
+ this.__stmtSelectGroupID = this._dbCreateStatement(`
+ SELECT groups.id AS id
+ FROM groups
+ WHERE groups.name = :name
+ `);
+
+ return this.__stmtSelectGroupID;
+ },
+
+ _selectGroupID: function ContentPrefService__selectGroupID(aName) {
+ var id;
+
+ try {
+ this._stmtSelectGroupID.params.name = aName;
+
+ if (this._stmtSelectGroupID.executeStep())
+ id = this._stmtSelectGroupID.row["id"];
+ }
+ finally {
+ this._stmtSelectGroupID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertGroup: null,
+ get _stmtInsertGroup() {
+ if (!this.__stmtInsertGroup)
+ this.__stmtInsertGroup = this._dbCreateStatement(
+ "INSERT INTO groups (name) VALUES (:name)"
+ );
+
+ return this.__stmtInsertGroup;
+ },
+
+ _insertGroup: function ContentPrefService__insertGroup(aName) {
+ this._stmtInsertGroup.params.name = aName;
+ this._stmtInsertGroup.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtSelectSettingID: null,
+ get _stmtSelectSettingID() {
+ if (!this.__stmtSelectSettingID)
+ this.__stmtSelectSettingID = this._dbCreateStatement(
+ "SELECT id FROM settings WHERE name = :name"
+ );
+
+ return this.__stmtSelectSettingID;
+ },
+
+ _selectSettingID: function ContentPrefService__selectSettingID(aName) {
+ var id;
+
+ try {
+ this._stmtSelectSettingID.params.name = aName;
+
+ if (this._stmtSelectSettingID.executeStep())
+ id = this._stmtSelectSettingID.row["id"];
+ }
+ finally {
+ this._stmtSelectSettingID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertSetting: null,
+ get _stmtInsertSetting() {
+ if (!this.__stmtInsertSetting)
+ this.__stmtInsertSetting = this._dbCreateStatement(
+ "INSERT INTO settings (name) VALUES (:name)"
+ );
+
+ return this.__stmtInsertSetting;
+ },
+
+ _insertSetting: function ContentPrefService__insertSetting(aName) {
+ this._stmtInsertSetting.params.name = aName;
+ this._stmtInsertSetting.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtSelectPrefID: null,
+ get _stmtSelectPrefID() {
+ if (!this.__stmtSelectPrefID)
+ this.__stmtSelectPrefID = this._dbCreateStatement(
+ "SELECT id FROM prefs WHERE groupID = :groupID AND settingID = :settingID"
+ );
+
+ return this.__stmtSelectPrefID;
+ },
+
+ _selectPrefID: function ContentPrefService__selectPrefID(aGroupID, aSettingID) {
+ var id;
+
+ try {
+ this._stmtSelectPrefID.params.groupID = aGroupID;
+ this._stmtSelectPrefID.params.settingID = aSettingID;
+
+ if (this._stmtSelectPrefID.executeStep())
+ id = this._stmtSelectPrefID.row["id"];
+ }
+ finally {
+ this._stmtSelectPrefID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtSelectGlobalPrefID: null,
+ get _stmtSelectGlobalPrefID() {
+ if (!this.__stmtSelectGlobalPrefID)
+ this.__stmtSelectGlobalPrefID = this._dbCreateStatement(
+ "SELECT id FROM prefs WHERE groupID IS NULL AND settingID = :settingID"
+ );
+
+ return this.__stmtSelectGlobalPrefID;
+ },
+
+ _selectGlobalPrefID: function ContentPrefService__selectGlobalPrefID(aSettingID) {
+ var id;
+
+ try {
+ this._stmtSelectGlobalPrefID.params.settingID = aSettingID;
+
+ if (this._stmtSelectGlobalPrefID.executeStep())
+ id = this._stmtSelectGlobalPrefID.row["id"];
+ }
+ finally {
+ this._stmtSelectGlobalPrefID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertPref: null,
+ get _stmtInsertPref() {
+ if (!this.__stmtInsertPref)
+ this.__stmtInsertPref = this._dbCreateStatement(`
+ INSERT INTO prefs (groupID, settingID, value)
+ VALUES (:groupID, :settingID, :value)
+ `);
+
+ return this.__stmtInsertPref;
+ },
+
+ _insertPref: function ContentPrefService__insertPref(aGroupID, aSettingID, aValue) {
+ this._stmtInsertPref.params.groupID = aGroupID;
+ this._stmtInsertPref.params.settingID = aSettingID;
+ this._stmtInsertPref.params.value = aValue;
+ this._stmtInsertPref.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtUpdatePref: null,
+ get _stmtUpdatePref() {
+ if (!this.__stmtUpdatePref)
+ this.__stmtUpdatePref = this._dbCreateStatement(
+ "UPDATE prefs SET value = :value WHERE id = :id"
+ );
+
+ return this.__stmtUpdatePref;
+ },
+
+ _updatePref: function ContentPrefService__updatePref(aPrefID, aValue) {
+ this._stmtUpdatePref.params.id = aPrefID;
+ this._stmtUpdatePref.params.value = aValue;
+ this._stmtUpdatePref.execute();
+ },
+
+ __stmtDeletePref: null,
+ get _stmtDeletePref() {
+ if (!this.__stmtDeletePref)
+ this.__stmtDeletePref = this._dbCreateStatement(
+ "DELETE FROM prefs WHERE id = :id"
+ );
+
+ return this.__stmtDeletePref;
+ },
+
+ _deletePref: function ContentPrefService__deletePref(aPrefID) {
+ this._stmtDeletePref.params.id = aPrefID;
+ this._stmtDeletePref.execute();
+ },
+
+ __stmtDeleteSettingIfUnused: null,
+ get _stmtDeleteSettingIfUnused() {
+ if (!this.__stmtDeleteSettingIfUnused)
+ this.__stmtDeleteSettingIfUnused = this._dbCreateStatement(`
+ DELETE FROM settings WHERE id = :id
+ AND id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+
+ return this.__stmtDeleteSettingIfUnused;
+ },
+
+ _deleteSettingIfUnused: function ContentPrefService__deleteSettingIfUnused(aSettingID) {
+ this._stmtDeleteSettingIfUnused.params.id = aSettingID;
+ this._stmtDeleteSettingIfUnused.execute();
+ },
+
+ __stmtDeleteGroupIfUnused: null,
+ get _stmtDeleteGroupIfUnused() {
+ if (!this.__stmtDeleteGroupIfUnused)
+ this.__stmtDeleteGroupIfUnused = this._dbCreateStatement(`
+ DELETE FROM groups WHERE id = :id
+ AND id NOT IN (SELECT DISTINCT groupID FROM prefs)
+ `);
+
+ return this.__stmtDeleteGroupIfUnused;
+ },
+
+ _deleteGroupIfUnused: function ContentPrefService__deleteGroupIfUnused(aGroupID) {
+ this._stmtDeleteGroupIfUnused.params.id = aGroupID;
+ this._stmtDeleteGroupIfUnused.execute();
+ },
+
+ __stmtSelectPrefs: null,
+ get _stmtSelectPrefs() {
+ if (!this.__stmtSelectPrefs)
+ this.__stmtSelectPrefs = this._dbCreateStatement(`
+ SELECT settings.name AS name, prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE groups.name = :group
+ `);
+
+ return this.__stmtSelectPrefs;
+ },
+
+ _selectPrefs: function ContentPrefService__selectPrefs(aGroup) {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ this._stmtSelectPrefs.params.group = aGroup;
+
+ while (this._stmtSelectPrefs.executeStep())
+ prefs.setProperty(this._stmtSelectPrefs.row["name"],
+ this._stmtSelectPrefs.row["value"]);
+ }
+ finally {
+ this._stmtSelectPrefs.reset();
+ }
+
+ return prefs;
+ },
+
+ __stmtSelectGlobalPrefs: null,
+ get _stmtSelectGlobalPrefs() {
+ if (!this.__stmtSelectGlobalPrefs)
+ this.__stmtSelectGlobalPrefs = this._dbCreateStatement(`
+ SELECT settings.name AS name, prefs.value AS value
+ FROM prefs
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE prefs.groupID IS NULL
+ `);
+
+ return this.__stmtSelectGlobalPrefs;
+ },
+
+ _selectGlobalPrefs: function ContentPrefService__selectGlobalPrefs() {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ while (this._stmtSelectGlobalPrefs.executeStep())
+ prefs.setProperty(this._stmtSelectGlobalPrefs.row["name"],
+ this._stmtSelectGlobalPrefs.row["value"]);
+ }
+ finally {
+ this._stmtSelectGlobalPrefs.reset();
+ }
+
+ return prefs;
+ },
+
+ __stmtSelectPrefsByName: null,
+ get _stmtSelectPrefsByName() {
+ if (!this.__stmtSelectPrefsByName)
+ this.__stmtSelectPrefsByName = this._dbCreateStatement(`
+ SELECT groups.name AS groupName, prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE settings.name = :setting
+ `);
+
+ return this.__stmtSelectPrefsByName;
+ },
+
+ _selectPrefsByName: function ContentPrefService__selectPrefsByName(aName) {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ this._stmtSelectPrefsByName.params.setting = aName;
+
+ while (this._stmtSelectPrefsByName.executeStep())
+ prefs.setProperty(this._stmtSelectPrefsByName.row["groupName"],
+ this._stmtSelectPrefsByName.row["value"]);
+ }
+ finally {
+ this._stmtSelectPrefsByName.reset();
+ }
+
+ var global = this._selectGlobalPref(aName);
+ if (typeof global != "undefined") {
+ prefs.setProperty(null, global);
+ }
+
+ return prefs;
+ },
+
+
+ // Database Creation & Access
+
+ _dbVersion: 4,
+
+ _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"]
+ }
+ }
+ },
+
+ _dbConnection: null,
+
+ _dbCreateStatement: function ContentPrefService__dbCreateStatement(aSQLString) {
+ try {
+ var statement = this._dbConnection.createStatement(aSQLString);
+ }
+ catch (ex) {
+ Cu.reportError("error creating statement " + aSQLString + ": " +
+ this._dbConnection.lastError + " - " +
+ this._dbConnection.lastErrorString);
+ throw ex;
+ }
+
+ return statement;
+ },
+
+ // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
+ // specific migration methods) must be careful not to call any method
+ // of the service that assumes the database connection has already been
+ // initialized, since it won't be initialized until at the end of _dbInit.
+
+ _dbInit: function ContentPrefService__dbInit() {
+ var dirService = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ var dbFile = dirService.get("ProfD", Ci.nsIFile);
+ dbFile.append("content-prefs.sqlite");
+
+ var dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+
+ var dbConnection;
+
+ if (!dbFile.exists())
+ dbConnection = this._dbCreate(dbService, dbFile);
+ else {
+ try {
+ dbConnection = dbService.openDatabase(dbFile);
+ }
+ // If the connection isn't ready after we open the database, that means
+ // the database has been corrupted, so we back it up and then recreate it.
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
+ throw e;
+ dbConnection = this._dbBackUpAndRecreate(dbService, dbFile,
+ dbConnection);
+ }
+
+ // Get the version of the schema in the file.
+ var version = dbConnection.schemaVersion;
+
+ // Try to migrate the schema in the database to the current schema used by
+ // the service. If migration fails, back up the database and recreate it.
+ if (version != this._dbVersion) {
+ try {
+ this._dbMigrate(dbConnection, version, this._dbVersion);
+ }
+ catch (ex) {
+ Cu.reportError("error migrating DB: " + ex + "; backing up and recreating");
+ dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, dbConnection);
+ }
+ }
+ }
+
+ // 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.
+ //
+ // If you really don't want to take this risk, however, just set the
+ // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2
+ // (FULL synchronization), in which case mozStorageConnection::Initialize
+ // will use that value, and we won't override it here.
+ if (!this._prefSvc.prefHasUserValue("toolkit.storage.synchronous"))
+ dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF");
+
+ this._dbConnection = dbConnection;
+ },
+
+ _dbCreate: function ContentPrefService__dbCreate(aDBService, aDBFile) {
+ var dbConnection = aDBService.openDatabase(aDBFile);
+
+ try {
+ this._dbCreateSchema(dbConnection);
+ dbConnection.schemaVersion = this._dbVersion;
+ }
+ catch (ex) {
+ // If we failed to create the database (perhaps because the disk ran out
+ // of space), then remove the database file so we don't leave it in some
+ // half-created state from which we won't know how to recover.
+ dbConnection.close();
+ aDBFile.remove(false);
+ throw ex;
+ }
+
+ return dbConnection;
+ },
+
+ _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) {
+ this._dbCreateTables(aDBConnection);
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) {
+ for (let name in this._dbSchema.tables)
+ aDBConnection.createTable(name, this._dbSchema.tables[name]);
+ },
+
+ _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) {
+ for (let name in this._dbSchema.indices) {
+ let index = this._dbSchema.indices[name];
+ let statement = `
+ CREATE INDEX IF NOT EXISTS ${name} ON ${index.table}
+ (${index.columns.join(", ")})
+ `;
+ aDBConnection.executeSimpleSQL(statement);
+ }
+ },
+
+ _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBService,
+ aDBFile,
+ aDBConnection) {
+ aDBService.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt");
+
+ // Close the database, ignoring the "already closed" exception, if any.
+ // It'll be open if we're here because of a migration failure but closed
+ // if we're here because of database corruption.
+ try { aDBConnection.close() } catch (ex) {}
+
+ aDBFile.remove(false);
+
+ let dbConnection = this._dbCreate(aDBService, aDBFile);
+
+ return dbConnection;
+ },
+
+ _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, 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.
+ */
+ aDBConnection.beginTransaction();
+
+ try {
+ /**
+ * If the schema version is 0, that means it was never set, which means
+ * the database was somehow created without the schema being applied, perhaps
+ * because the system ran out of disk space (although we check for this
+ * in _createDB) or because some other code created the database file without
+ * applying the schema. In any case, recover by simply reapplying the schema.
+ */
+ if (aOldVersion == 0) {
+ this._dbCreateSchema(aDBConnection);
+ } else {
+ for (let i = aOldVersion; i < aNewVersion; i++) {
+ let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+ if (typeof this[migrationName] != 'function') {
+ throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion);
+ }
+ this[migrationName](aDBConnection);
+ }
+ }
+ aDBConnection.schemaVersion = aNewVersion;
+ aDBConnection.commitTransaction();
+ } catch (ex) {
+ aDBConnection.rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) {
+ aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld");
+ aDBConnection.createTable("groups", this._dbSchema.tables.groups);
+ aDBConnection.executeSimpleSQL(`
+ INSERT INTO groups (id, name)
+ SELECT id, name FROM groupsOld
+ `);
+
+ aDBConnection.executeSimpleSQL("DROP TABLE groupers");
+ aDBConnection.executeSimpleSQL("DROP TABLE groupsOld");
+ },
+
+ _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
+ // Add timestamp column if it does not exist yet. This operation is idempotent.
+ try {
+ let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
+ stmt.finalize();
+ } catch (e) {
+ aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
+ }
+
+ // To modify prefs_idx drop it and create again.
+ aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _parseGroupParam: function ContentPrefService__parseGroupParam(aGroup) {
+ if (aGroup == null)
+ return null;
+ if (aGroup.constructor.name == "String")
+ return aGroup.toString();
+ if (aGroup instanceof Ci.nsIURI)
+ return this.grouper.group(aGroup);
+
+ throw Components.Exception("aGroup is not a string, nsIURI or null",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ },
+};
+
+function warnDeprecated() {
+ let Deprecated = Cu.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ Deprecated.warning("nsIContentPrefService is deprecated. Please use nsIContentPrefService2 instead.",
+ "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIContentPrefService2",
+ Components.stack.caller);
+}
+
+
+function HostnameGrouper() {}
+
+HostnameGrouper.prototype = {
+ // XPCOM Plumbing
+
+ classID: Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentURIGrouper]),
+
+ // nsIContentURIGrouper
+
+ group: 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 ("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;
+ }
+};
+
+function AsyncStatement(aStatement) {
+ this.stmt = aStatement;
+}
+
+AsyncStatement.prototype = {
+ execute: function AsyncStmt_execute(aCallback) {
+ let stmt = this.stmt;
+ stmt.executeAsync({
+ _callback: aCallback,
+ _hadResult: false,
+ handleResult: function(aResult) {
+ this._hadResult = true;
+ if (this._callback) {
+ let row = aResult.getNextRow();
+ this._callback.onResult(row.getResultByName("value"));
+ }
+ },
+ handleCompletion: function(aReason) {
+ if (!this._hadResult && this._callback &&
+ aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ this._callback.onResult(undefined);
+ },
+ handleError: function(aError) {}
+ });
+ }
+};
+
+// XPCOM Plumbing
+
+var components = [ContentPrefService, HostnameGrouper];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/contentprefs/nsContentPrefService.manifest b/toolkit/components/contentprefs/nsContentPrefService.manifest
new file mode 100644
index 000000000..b6bc15721
--- /dev/null
+++ b/toolkit/components/contentprefs/nsContentPrefService.manifest
@@ -0,0 +1,5 @@
+component {e3f772f3-023f-4b32-b074-36cf0fd5d414} nsContentPrefService.js
+contract @mozilla.org/content-pref/service;1 {e3f772f3-023f-4b32-b074-36cf0fd5d414}
+component {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} nsContentPrefService.js
+contract @mozilla.org/content-pref/hostname-grouper;1 {8df290ae-dcaa-4c11-98a5-2429a4dc97bb}
+
diff --git a/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js
new file mode 100644
index 000000000..64a4eda73
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/mochitest/mochitest.ini b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini
new file mode 100644
index 000000000..ec4f05945
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_remoteContentPrefs.html]
+skip-if = toolkit == 'android' || e10s # bug 783513
diff --git a/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
new file mode 100644
index 000000000..d14e85a25
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
@@ -0,0 +1,311 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for nsIContentPrefService2 in child processes</title>
+ <script type="application/javascript"
+ src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+
+ const childFrameURL =
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+
+ function childFrameScript(isFramePrivate) {
+ "use strict";
+
+ function Tester(resultArray) {
+ this.results = [];
+ }
+
+ Tester.prototype.is =
+ function(a, b, note) {
+ this.results.push([a === b, note + " (" + a + ", " + b + ")"]);
+ };
+ Tester.prototype.ok =
+ function(b, note) {
+ this.results.push([b != false, note]);
+ };
+
+ var cps = Components.classes["@mozilla.org/content-pref/service;1"]
+ .getService(Components.interfaces.nsIContentPrefService2);
+
+ let test = null;
+ function* test1(message) {
+ let tester = new Tester();
+
+ tester.ok(cps !== null, "got the content pref service");
+
+ cps.setGlobal("testing", 42, null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref?");
+ test.next();
+ }
+ });
+
+ yield;
+
+ let numResults = 0;
+ cps.getGlobal("testing", null, {
+ handleResult: function(pref) {
+ numResults++;
+ tester.is(pref.name, "testing", "pref has the right name");
+ tester.is(pref.value, 42, "pref has the right value");
+ },
+
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "get a pref?");
+ tester.is(numResults, 1, "got the right number of prefs");
+ tester.is(test.next().done, true, "done with test1");
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test1Finished",
+ { results: tester.results });
+ }
+ });
+
+ yield;
+ }
+
+ function* test2(message) {
+ let tester = new Tester();
+
+ let observer;
+ let removed = false;
+ cps.addObserverForName("testName", observer = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ if (removed) {
+ message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+ { reason: "unexpected notification" });
+ }
+ tester.is(group, null, "group should be null");
+ tester.is(name, "testName", "should only see testName");
+ tester.is(value, 42, "value should be correct");
+ tester.is(isPrivate, isFramePrivate, "privacy should match");
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke2", {})
+ },
+
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ tester.is(group, null, "group should be null");
+ tester.is(name, "testName");
+ tester.is(isPrivate, isFramePrivate, "privacy should match");
+ tester.is(test.next().done, true, "should be done with test2");
+
+ cps.removeObserverForName("testName", observer);
+ removed = true;
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2Finished",
+ { results: tester.results });
+ }
+ });
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke", {});
+ yield;
+ }
+
+ function* test3(message) {
+ let tester = new Tester();
+
+ cps.setGlobal("testName", 42, null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref");
+ cps.set("http://mochi.test", "testpref", "str", null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref");
+ test.next();
+ }
+ });
+ }
+ });
+
+ yield;
+
+ cps.removeByDomain("http://mochi.test", null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "remove succeeded");
+ cps.getByDomainAndName("http://mochi.test", "testpref", null, {
+ handleResult: function() {
+ message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+ { reason: "got removed pref in test3" });
+ },
+ handleCompletion: function() {
+ test.next();
+ }
+ });
+ }
+ });
+
+ yield;
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test3Finished",
+ { results: tester.results });
+ }
+
+ function* test4(message) {
+ let tester = new Tester();
+
+ let prefObserver = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ test.next({ group: group, name: name, value: value, isPrivate: isPrivate });
+ },
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ test.next({ group: group, name: name, isPrivate: isPrivate });
+ }
+ };
+
+ addMessageListener("testRemoteContentPrefs:prefResults", (msg) => {
+ test.next(msg.data.results);
+ });
+
+ cps.addObserverForName("test", prefObserver);
+
+ cps.set("http://mochi.test", "test", 42, { usePrivateBrowsing: true });
+ let event = yield;
+ tester.is(event.name, "test");
+ tester.is(event.isPrivate, true);
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:getPref",
+ { group: "http://mochi.test", name: "test" });
+
+ let results = yield;
+ tester.is(results.length, 0, "should not have seen the pb pref");
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test4Finished",
+ { results: tester.results });
+ }
+
+ addMessageListener("testRemoteContentPrefs:test1", function(message) {
+ test = test1(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test2", function(message) {
+ test = test2(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test3", function(message) {
+ test = test3(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test4", function(message) {
+ test = test4(message);
+ test.next();
+ });
+ }
+
+ function processResults(results) {
+ for (let i of results) {
+ ok(...i);
+ }
+ }
+
+ let test;
+ function* testStructure(mm, isPrivate, callback) {
+ let lastResult;
+
+ function testDone(msg) {
+ test.next(msg.data);
+ }
+
+ mm.addMessageListener("testRemoteContentPrefs:test1Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test2Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test3Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test4Finished", testDone);
+
+ mm.addMessageListener("testRemoteContentPrefs:fail", function(msg) {
+ ok(false, msg.data.reason);
+ });
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test1", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ var cps = SpecialPowers.Cc["@mozilla.org/content-pref/service;1"]
+ .getService(SpecialPowers.Ci.nsIContentPrefService2);
+ mm.sendAsyncMessage("testRemoteContentPrefs:test2", {});
+ mm.addMessageListener("testRemoteContentPrefs:test2poke", function() {
+ cps.setGlobal("testName", 42, {usePrivateBrowsing: isPrivate});
+ });
+ mm.addMessageListener("testRemoteContentPrefs:test2poke2", function() {
+ cps.removeGlobal("testName", {usePrivateBrowsing: isPrivate});
+ });
+
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test3", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ mm.addMessageListener("testRemoteContentPrefs:getPref", function(msg) {
+ let results = [];
+ cps.getByDomainAndName(msg.data.group, msg.data.name, null, {
+ handleResult: function(pref) {
+ results.push(pref);
+ },
+ handleCompletion: function(reason) {
+ mm.sendAsyncMessage("testRemoteContentPrefs:prefResults",
+ { results: results });
+ }
+ });
+ });
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test4", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ document.getElementById('iframe').remove();
+ setTimeout(callback, 0);
+ }
+
+ function runTest(isPrivate, callback) {
+ info("testing with isPrivate=" + isPrivate);
+ let iframe = document.createElement("iframe");
+ SpecialPowers.wrap(iframe).mozbrowser = true;
+ if (isPrivate) {
+ SpecialPowers.wrap(iframe).mozprivatebrowsing = true;
+ }
+ iframe.id = "iframe";
+ iframe.src = childFrameURL;
+
+ iframe.addEventListener("mozbrowserloadend", function() {
+ info("Got iframe load event.");
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")(" + isPrivate + ");",
+ false);
+
+ test = testStructure(mm, isPrivate, callback);
+ test.next();
+ });
+
+ document.body.appendChild(iframe);
+ }
+
+ function runTests() {
+ info("Browser prefs set.");
+ runTest(false, function() {
+ runTest(true, function() {
+ SimpleTest.finish();
+ });
+ });
+ }
+
+ addEventListener("load", function() {
+ info("Got load event.");
+
+ SpecialPowers.addPermission("browser", true, document);
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["browser.pagethumbnails.capturing_disabled", true]
+ ]
+ }, runTests);
+ });
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/contentprefs/tests/unit/.eslintrc.js b/toolkit/components/contentprefs/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
new file mode 100644
index 000000000..84ca1bebf
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
@@ -0,0 +1,162 @@
+/* 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/. */
+
+// Inspired by the Places infrastructure in head_bookmarks.js
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/ContentPrefInstance.jsm');
+
+const CONTENT_PREFS_DB_FILENAME = "content-prefs.sqlite";
+const CONTENT_PREFS_BACKUP_DB_FILENAME = "content-prefs.sqlite.corrupt";
+
+var ContentPrefTest = {
+ // Convenience Getters
+
+ __dirSvc: null,
+ get _dirSvc() {
+ if (!this.__dirSvc)
+ this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ return this.__dirSvc;
+ },
+
+ __consoleSvc: null,
+ get _consoleSvc() {
+ if (!this.__consoleSvc)
+ this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService);
+ return this.__consoleSvc;
+ },
+
+ __ioSvc: null,
+ get _ioSvc() {
+ if (!this.__ioSvc)
+ this.__ioSvc = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return this.__ioSvc;
+ },
+
+
+ // nsISupports
+
+ interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+
+ // nsIDirectoryServiceProvider
+
+ getFile: function ContentPrefTest_getFile(property, persistent) {
+ persistent.value = true;
+
+ if (property == "ProfD")
+ return this._dirSvc.get("CurProcD", Ci.nsIFile);
+
+ // This causes extraneous errors to show up in the log when the directory
+ // service asks us first for CurProcD and MozBinD. I wish there was a way
+ // to suppress those errors.
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+
+ // Utilities
+
+ getURI: function ContentPrefTest_getURI(spec) {
+ return this._ioSvc.newURI(spec, null, null);
+ },
+
+ /**
+ * Get the profile directory.
+ */
+ getProfileDir: function ContentPrefTest_getProfileDir() {
+ // do_get_profile can be only called from a parent process
+ if (runningInParent) {
+ return do_get_profile();
+ }
+ // if running in a content process, this just returns the path
+ // profile was initialized in the ipc head file
+ let env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ // the python harness sets this in the environment for us
+ let profd = env.get("XPCSHELL_TEST_PROFILE_DIR");
+ let file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ file.initWithPath(profd);
+ return file;
+ },
+
+ /**
+ * Delete the content pref service's persistent datastore. We do this before
+ * and after running tests to make sure we start from scratch each time. We
+ * also do it during the database creation, schema migration, and backup tests.
+ */
+ deleteDatabase: function ContentPrefTest_deleteDatabase() {
+ var file = this.getProfileDir();
+ file.append(CONTENT_PREFS_DB_FILENAME);
+ if (file.exists())
+ try { file.remove(false); } catch (e) { /* stupid windows box */ }
+ return file;
+ },
+
+ /**
+ * Delete the backup of the content pref service's persistent datastore.
+ * We do this during the database creation, schema migration, and backup tests.
+ */
+ deleteBackupDatabase: function ContentPrefTest_deleteBackupDatabase() {
+ var file = this.getProfileDir();
+ file.append(CONTENT_PREFS_BACKUP_DB_FILENAME);
+ if (file.exists())
+ file.remove(false);
+ return file;
+ },
+
+ /**
+ * Log a message to the console and the test log.
+ */
+ log: function ContentPrefTest_log(message) {
+ message = "*** ContentPrefTest: " + message;
+ this._consoleSvc.logStringMessage(message);
+ print(message);
+ }
+
+};
+
+var gInPrivateBrowsing = false;
+function enterPBMode() {
+ gInPrivateBrowsing = true;
+}
+function exitPBMode() {
+ gInPrivateBrowsing = false;
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+}
+
+ContentPrefTest.deleteDatabase();
+
+function inChildProcess() {
+ var appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType ==
+ Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ return false;
+ }
+ return true;
+}
+
+// Turn on logging for the content preferences service so we can troubleshoot
+// problems with the tests. Note that we cannot do this in a child process
+// without crashing (but we don't need it anyhow)
+if (!inChildProcess()) {
+ var prefBranch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefBranch.setBoolPref("browser.preferences.content.log", true);
+}
+
diff --git a/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
new file mode 100644
index 000000000..f3c95dac8
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
@@ -0,0 +1,6 @@
+/* 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/. */
+
+ContentPrefTest.deleteDatabase();
+ContentPrefTest.__dirSvc = null;
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug248970.js b/toolkit/components/contentprefs/tests/unit/test_bug248970.js
new file mode 100644
index 000000000..5f4aa25c5
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug248970.js
@@ -0,0 +1,42 @@
+/* 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 loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } };
+
+ ContentPrefTest.deleteDatabase();
+ var cp = new ContentPrefInstance(loadContext);
+ do_check_neq(cp, null, "Retrieving the content prefs service failed");
+
+ try {
+ const uri1 = ContentPrefTest.getURI("http://www.example.com/");
+ const uri2 = ContentPrefTest.getURI("http://www.anotherexample.com/");
+ const pref_name = "browser.content.full-zoom";
+ const zoomA = 1.5, zoomA_new = 0.8, zoomB = 1.3;
+ // save Zoom-A
+ cp.setPref(uri1, pref_name, zoomA);
+ // make sure Zoom-A is retrievable
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // enter private browsing mode
+ enterPBMode();
+ // make sure Zoom-A is retrievable
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // save Zoom-B
+ cp.setPref(uri2, pref_name, zoomB);
+ // make sure Zoom-B is retrievable
+ do_check_eq(cp.getPref(uri2, pref_name), zoomB);
+ // update Zoom-A
+ cp.setPref(uri1, pref_name, zoomA_new);
+ // make sure Zoom-A has changed
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA_new);
+ // exit private browsing mode
+ exitPBMode();
+ // make sure Zoom-A change has not persisted
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // make sure Zoom-B change has not persisted
+ do_check_eq(cp.hasPref(uri2, pref_name), false);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug503971.js b/toolkit/components/contentprefs/tests/unit/test_bug503971.js
new file mode 100644
index 000000000..ccfe1d02b
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug503971.js
@@ -0,0 +1,35 @@
+/* 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() {
+ var cps = new ContentPrefInstance(null);
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+ do_check_thrown(function () { cps.setPref(uri, null, 8); });
+ do_check_thrown(function () { cps.hasPref(uri, null); });
+ do_check_thrown(function () { cps.getPref(uri, null); });
+ do_check_thrown(function () { cps.removePref(uri, null); });
+ do_check_thrown(function () { cps.getPrefsByName(null); });
+ do_check_thrown(function () { cps.removePrefsByName(null); });
+
+ do_check_thrown(function () { cps.setPref(uri, "", 21); });
+ do_check_thrown(function () { cps.hasPref(uri, ""); });
+ do_check_thrown(function () { cps.getPref(uri, ""); });
+ do_check_thrown(function () { cps.removePref(uri, ""); });
+ do_check_thrown(function () { cps.getPrefsByName(""); });
+ do_check_thrown(function () { cps.removePrefsByName(""); });
+}
+
+function do_check_thrown (aCallback) {
+ var exThrown = false;
+ try {
+ aCallback();
+ do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+ } catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ exThrown = true;
+ }
+ do_check_true(exThrown);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug679784.js b/toolkit/components/contentprefs/tests/unit/test_bug679784.js
new file mode 100644
index 000000000..97251d87b
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug679784.js
@@ -0,0 +1,103 @@
+/* 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/. */
+
+
+var prefObserver = {
+ setCalledNum: 0,
+ onContentPrefSet: function(aGroup, aName, aValue) {
+ this.setCalledNum++;
+ },
+ removedCalledNum: 0,
+ onContentPrefRemoved: function(aGroup, aName) {
+ this.removedCalledNum++;
+ }
+};
+
+function run_test() {
+ let loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } };
+
+ var cps = new ContentPrefInstance(loadContext);
+ cps.removeGroupedPrefs();
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+ var group = cps.grouper.group(uri);
+
+ // first, set a pref in normal mode
+ cps.setPref(uri, "value", "foo");
+ cps.setPref(null, "value-global", "foo-global");
+
+ var num;
+ cps.addObserver("value", prefObserver);
+ cps.addObserver("value-global", prefObserver);
+
+ enterPBMode();
+
+ // test setPref
+ num = prefObserver.setCalledNum;
+ cps.setPref(uri, "value", "foo-private-browsing");
+ do_check_eq(cps.hasPref(uri, "value"), true);
+ do_check_eq(cps.getPref(uri, "value"), "foo-private-browsing");
+ do_check_eq(prefObserver.setCalledNum, num + 1);
+
+ num = prefObserver.setCalledNum;
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+ do_check_eq(prefObserver.setCalledNum, num + 1);
+
+ // test removePref
+ num = prefObserver.removedCalledNum;
+ cps.removePref(uri, "value");
+ do_check_eq(cps.hasPref(uri, "value"), true);
+ // fallback to non private mode value
+ do_check_eq(cps.getPref(uri, "value"), "foo");
+ do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+ num = prefObserver.removedCalledNum;
+ cps.removePref(null, "value-global");
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ // fallback to non private mode value
+ do_check_eq(cps.getPref(null, "value-global"), "foo-global") ;
+ do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+ // test removeGroupedPrefs
+ cps.setPref(uri, "value", "foo-private-browsing");
+ cps.removeGroupedPrefs();
+ do_check_eq(cps.hasPref(uri, "value"), false);
+ do_check_eq(cps.getPref(uri, "value"), undefined);
+
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ cps.removeGroupedPrefs();
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+
+ // test removePrefsByName
+ num = prefObserver.removedCalledNum;
+ cps.setPref(uri, "value", "foo-private-browsing");
+ cps.removePrefsByName("value");
+ do_check_eq(cps.hasPref(uri, "value"), false);
+ do_check_eq(cps.getPref(uri, "value"), undefined);
+ do_check_true(prefObserver.removedCalledNum > num);
+
+ num = prefObserver.removedCalledNum;
+ cps.setPref(null, "value-global", "foo-private-browsing");
+ cps.removePrefsByName("value-global");
+ do_check_eq(cps.hasPref(null, "value-global"), false);
+ do_check_eq(cps.getPref(null, "value-global"), undefined);
+ do_check_true(prefObserver.removedCalledNum > num);
+
+ // test getPrefs
+ cps.setPref(uri, "value", "foo-private-browsing");
+ do_check_eq(cps.getPrefs(uri).getProperty("value"), "foo-private-browsing");
+
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ do_check_eq(cps.getPrefs(null).getProperty("value-global"), "foo-private-browsing-global");
+
+ // test getPrefsByName
+ do_check_eq(cps.getPrefsByName("value").getProperty(group), "foo-private-browsing");
+ do_check_eq(cps.getPrefsByName("value-global").getProperty(null), "foo-private-browsing-global");
+
+ cps.removeObserver("value", prefObserver);
+ cps.removeObserver("value-global", prefObserver);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
new file mode 100644
index 000000000..f7e99ea9d
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
@@ -0,0 +1,463 @@
+/* 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() {
+ // Database Creation, Schema Migration, and Backup
+
+ // Note: in these tests we use createInstance instead of getService
+ // so we can instantiate the service multiple times and make it run
+ // its database initialization code each time.
+
+ // Create a new database.
+ {
+ ContentPrefTest.deleteDatabase();
+
+ // Get the service and make sure it has a ready database connection.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(cps.DBConnection.connectionReady);
+ cps.DBConnection.close();
+ }
+
+ // Open an existing database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ cps.DBConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it has a ready database connection.
+ cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(cps.DBConnection.connectionReady);
+ cps.DBConnection.close();
+ }
+
+ // Open an empty database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+
+ // Create an empty database.
+ let dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let dbConnection = dbService.openDatabase(dbFile);
+ do_check_eq(dbConnection.schemaVersion, 0);
+ dbConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it has created the schema.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_neq(cps.DBConnection.schemaVersion, 0);
+ cps.DBConnection.close();
+ }
+
+ // Open a corrupted database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+ let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+ // Create a corrupted database.
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foStream.init(dbFile, 0x02 | 0x08 | 0x20, 0o666, 0);
+ let garbageData = "garbage that makes SQLite think the file is corrupted";
+ foStream.write(garbageData, garbageData.length);
+ foStream.close();
+
+ // Get the service and make sure it backs up and recreates the database.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(backupDBFile.exists());
+ do_check_true(cps.DBConnection.connectionReady);
+
+ cps.DBConnection.close();
+ }
+
+ // Open a database with a corrupted schema.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+ let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+ // Create an empty database and set the schema version to a number
+ // that will trigger a schema migration that will fail.
+ let dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let dbConnection = dbService.openDatabase(dbFile);
+ dbConnection.schemaVersion = -1;
+ dbConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it backs up and recreates the database.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(backupDBFile.exists());
+ do_check_true(cps.DBConnection.connectionReady);
+
+ cps.DBConnection.close();
+ }
+
+
+ // Now get the content pref service for real for use by the rest of the tests.
+ let cps = new ContentPrefInstance(null);
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+ // Make sure disk synchronization checking is turned off by default.
+ var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+ statement.executeStep();
+ do_check_eq(0, statement.getInt32(0));
+
+ // Nonexistent Pref
+
+ do_check_eq(cps.getPref(uri, "test.nonexistent.getPref"), undefined);
+ do_check_eq(cps.setPref(uri, "test.nonexistent.setPref", 5), undefined);
+ do_check_false(cps.hasPref(uri, "test.nonexistent.hasPref"));
+ do_check_eq(cps.removePref(uri, "test.nonexistent.removePref"), undefined);
+
+
+ // Existing Pref
+
+ cps.setPref(uri, "test.existing", 5);
+
+ // getPref should return the pref value
+ do_check_eq(cps.getPref(uri, "test.existing"), 5);
+
+ // setPref should return undefined and change the value of the pref
+ do_check_eq(cps.setPref(uri, "test.existing", 6), undefined);
+ do_check_eq(cps.getPref(uri, "test.existing"), 6);
+
+ // hasPref should return true
+ do_check_true(cps.hasPref(uri, "test.existing"));
+
+ // removePref should return undefined and remove the pref
+ do_check_eq(cps.removePref(uri, "test.existing"), undefined);
+ do_check_false(cps.hasPref(uri, "test.existing"));
+
+
+ // Round-Trip Data Integrity
+
+ // Make sure pref values remain the same from setPref to getPref.
+
+ cps.setPref(uri, "test.data-integrity.integer", 5);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.integer"), 5);
+
+ cps.setPref(uri, "test.data-integrity.float", 5.5);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.float"), 5.5);
+
+ cps.setPref(uri, "test.data-integrity.boolean", true);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.boolean"), true);
+
+ cps.setPref(uri, "test.data-integrity.string", "test");
+ do_check_eq(cps.getPref(uri, "test.data-integrity.string"), "test");
+
+ cps.setPref(uri, "test.data-integrity.null", null);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.null"), null);
+
+ // XXX Test arbitrary binary data.
+
+ // Make sure hasPref and removePref work on all data types.
+
+ do_check_true(cps.hasPref(uri, "test.data-integrity.integer"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.float"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.boolean"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.string"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.null"));
+
+ do_check_eq(cps.removePref(uri, "test.data-integrity.integer"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.float"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.boolean"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.string"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.null"), undefined);
+
+ do_check_false(cps.hasPref(uri, "test.data-integrity.integer"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.float"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.boolean"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.string"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.null"));
+
+
+ // getPrefs
+
+ cps.setPref(uri, "test.getPrefs.a", 1);
+ cps.setPref(uri, "test.getPrefs.b", 2);
+ cps.setPref(uri, "test.getPrefs.c", 3);
+
+ var prefs = cps.getPrefs(uri);
+ do_check_true(prefs.hasKey("test.getPrefs.a"));
+ do_check_eq(prefs.get("test.getPrefs.a"), 1);
+ do_check_true(prefs.hasKey("test.getPrefs.b"));
+ do_check_eq(prefs.get("test.getPrefs.b"), 2);
+ do_check_true(prefs.hasKey("test.getPrefs.c"));
+ do_check_eq(prefs.get("test.getPrefs.c"), 3);
+
+
+ // Site-Specificity
+
+ {
+ // These are all different sites, and setting a pref for one of them
+ // shouldn't set it for the others.
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "test.site-specificity.uri1", 5);
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri1"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri1"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri1"));
+
+ cps.setPref(uri2, "test.site-specificity.uri2", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri2"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri2"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri2"));
+
+ cps.setPref(uri3, "test.site-specificity.uri3", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri3"));
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri3"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri3"));
+
+ cps.setPref(uri4, "test.site-specificity.uri4", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri4"));
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri4"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri4"));
+ }
+
+ // Observers
+
+ var specificObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ numTimesSetCalled: 0,
+ onContentPrefSet: function specificObserver_onContentPrefSet(group, name, value) {
+ ++this.numTimesSetCalled;
+ do_check_eq(group, "www.example.com");
+ do_check_eq(name, "test.observer.1");
+ do_check_eq(value, "test value");
+ },
+
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function specificObserver_onContentPrefRemoved(group, name) {
+ ++this.numTimesRemovedCalled;
+ do_check_eq(group, "www.example.com");
+ do_check_eq(name, "test.observer.1");
+ }
+
+ };
+
+ var genericObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ numTimesSetCalled: 0,
+ onContentPrefSet: function genericObserver_onContentPrefSet(group, name, value, isPrivate) {
+ ++this.numTimesSetCalled;
+ do_check_eq(group, "www.example.com");
+ if (name == "test.observer.private")
+ do_check_true(isPrivate);
+ else if (name == "test.observer.normal")
+ do_check_false(isPrivate);
+ else if (name != "test.observer.1" && name != "test.observer.2")
+ do_throw("genericObserver.onContentPrefSet: " +
+ "name not in (test.observer.(1|2|normal|private))");
+ do_check_eq(value, "test value");
+ },
+
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function genericObserver_onContentPrefRemoved(group, name, isPrivate) {
+ ++this.numTimesRemovedCalled;
+ do_check_eq(group, "www.example.com");
+ if (name == "test.observer.private")
+ do_check_true(isPrivate);
+ else if (name == "test.observer.normal")
+ do_check_false(isPrivate);
+ if (name != "test.observer.1" && name != "test.observer.2" &&
+ name != "test.observer.normal" && name != "test.observer.private") {
+ do_throw("genericObserver.onContentPrefSet: " +
+ "name not in (test.observer.(1|2|normal|private))");
+ }
+ }
+
+ };
+
+ // Make sure we can add observers, observers get notified about changes,
+ // specific observers only get notified about changes to the specific setting,
+ // and generic observers get notified about changes to all settings.
+ cps.addObserver("test.observer.1", specificObserver);
+ cps.addObserver(null, genericObserver);
+ cps.setPref(uri, "test.observer.1", "test value");
+ cps.setPref(uri, "test.observer.2", "test value");
+ cps.removePref(uri, "test.observer.1");
+ cps.removePref(uri, "test.observer.2");
+ do_check_eq(specificObserver.numTimesSetCalled, 1);
+ do_check_eq(genericObserver.numTimesSetCalled, 2);
+ do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+ do_check_eq(genericObserver.numTimesRemovedCalled, 2);
+
+ // Make sure information about private context is properly
+ // retrieved by the observer.
+ cps.setPref(uri, "test.observer.private", "test value", {usePrivateBrowsing: true});
+ cps.setPref(uri, "test.observer.normal", "test value", {usePrivateBrowsing: false});
+ cps.removePref(uri, "test.observer.private");
+ cps.removePref(uri, "test.observer.normal");
+
+ // Make sure we can remove observers and they don't get notified
+ // about changes anymore.
+ cps.removeObserver("test.observer.1", specificObserver);
+ cps.removeObserver(null, genericObserver);
+ cps.setPref(uri, "test.observer.1", "test value");
+ cps.removePref(uri, "test.observer.1", "test value");
+ do_check_eq(specificObserver.numTimesSetCalled, 1);
+ do_check_eq(genericObserver.numTimesSetCalled, 4);
+ do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+ do_check_eq(genericObserver.numTimesRemovedCalled, 3);
+
+
+ // Get/Remove Prefs By Name
+
+ {
+ var anObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onContentPrefSet: function anObserver_onContentPrefSet(group, name, value) {
+ },
+
+ expectedDomains: [],
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function anObserver_onContentPrefRemoved(group, name) {
+ ++this.numTimesRemovedCalled;
+
+ // remove the domain from the list of expected domains
+ var index = this.expectedDomains.indexOf(group);
+ do_check_true(index >= 0);
+ this.expectedDomains.splice(index, 1);
+ }
+ };
+
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "test.byname.1", 1);
+ cps.setPref(uri1, "test.byname.2", 2);
+ cps.setPref(uri2, "test.byname.1", 4);
+ cps.setPref(uri3, "test.byname.3", 8);
+ cps.setPref(uri4, "test.byname.1", 16);
+ cps.setPref(null, "test.byname.1", 32);
+ cps.setPref(null, "test.byname.2", false);
+
+ function enumerateAndCheck(testName, expectedSum, expectedDomains) {
+ var prefsByName = cps.getPrefsByName(testName);
+ var enumerator = prefsByName.enumerator;
+ var sum = 0;
+ while (enumerator.hasMoreElements()) {
+ var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ sum += parseInt(property.value);
+
+ // remove the domain from the list of expected domains
+ var index = expectedDomains.indexOf(property.name);
+ do_check_true(index >= 0);
+ expectedDomains.splice(index, 1);
+ }
+ do_check_eq(sum, expectedSum);
+ // check all domains have been removed from the array
+ do_check_eq(expectedDomains.length, 0);
+ }
+
+ enumerateAndCheck("test.byname.1", 53,
+ ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"]);
+ enumerateAndCheck("test.byname.2", 2, ["www.domain1.com", null]);
+ enumerateAndCheck("test.byname.3", 8, ["domain1.com"]);
+
+ cps.addObserver("test.byname.1", anObserver);
+ anObserver.expectedDomains = ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"];
+
+ cps.removePrefsByName("test.byname.1");
+ do_check_false(cps.hasPref(uri1, "test.byname.1"));
+ do_check_false(cps.hasPref(uri2, "test.byname.1"));
+ do_check_false(cps.hasPref(uri3, "test.byname.1"));
+ do_check_false(cps.hasPref(uri4, "test.byname.1"));
+ do_check_false(cps.hasPref(null, "test.byname.1"));
+ do_check_true(cps.hasPref(uri1, "test.byname.2"));
+ do_check_true(cps.hasPref(uri3, "test.byname.3"));
+
+ do_check_eq(anObserver.numTimesRemovedCalled, 4);
+ do_check_eq(anObserver.expectedDomains.length, 0);
+
+ cps.removeObserver("test.byname.1", anObserver);
+
+ // Clean up after ourselves
+ cps.removePref(uri1, "test.byname.2");
+ cps.removePref(uri3, "test.byname.3");
+ cps.removePref(null, "test.byname.2");
+ }
+
+
+ // Clear Private Data Pref Removal
+
+ {
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://www.domain2.com/");
+ let uri3 = ContentPrefTest.getURI("http://www.domain3.com/");
+
+ let dbConnection = cps.DBConnection;
+
+ let prefCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM prefs");
+
+ let groupCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM groups");
+
+ // Add some prefs for multiple domains.
+ cps.setPref(uri1, "test.removeAllGroups", 1);
+ cps.setPref(uri2, "test.removeAllGroups", 2);
+ cps.setPref(uri3, "test.removeAllGroups", 3);
+
+ // Add a global pref.
+ cps.setPref(null, "test.removeAllGroups", 1);
+
+ // Make sure there are some prefs and groups in the database.
+ prefCount.executeStep();
+ do_check_true(prefCount.row.count > 0);
+ prefCount.reset();
+ groupCount.executeStep();
+ do_check_true(groupCount.row.count > 0);
+ groupCount.reset();
+
+ // Remove all prefs and groups from the database using the same routine
+ // the Clear Private Data dialog uses.
+ cps.removeGroupedPrefs();
+
+ // Make sure there are no longer any groups in the database and the only pref
+ // is the global one.
+ prefCount.executeStep();
+ do_check_true(prefCount.row.count == 1);
+ prefCount.reset();
+ groupCount.executeStep();
+ do_check_true(groupCount.row.count == 0);
+ groupCount.reset();
+ let globalPref = dbConnection.createStatement("SELECT groupID FROM prefs");
+ globalPref.executeStep();
+ do_check_true(globalPref.row.groupID == null);
+ globalPref.reset();
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
new file mode 100644
index 000000000..38a2faddc
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
@@ -0,0 +1,244 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+ testCacheWorks("test1.example.com", "test-pref1");
+ testHasCachedPrefFunction("test2.example.com", "test-pref2");
+ testSetCaches("test3.example.com", "test-pref3");
+ testGetCaches("test4.example.com", "test-pref4");
+ testRemovePrefs("test5.example.com", "test-pref5");
+ testTypeConversions("test6.example.com", "test-pref6");
+ testNonExistingPrefCachesAsUndefined("test7.example.com", "test-pref7");
+ testCacheEviction("test8.example.com", "test-pref8");
+}
+
+function testCacheWorks(uri, prefName) {
+ const CACHED_VALUE = 3;
+ const NEW_VALUE = 5;
+
+ cps.setPref(uri, prefName, CACHED_VALUE);
+ do_check_eq(cps.getPref(uri, prefName), CACHED_VALUE);
+
+ // Now change the value directly through the DB and check
+ // that the cached value is different
+
+ let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+ let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+ let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+ "id", groupId, settingId);
+
+ let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+ stmt.params.value = NEW_VALUE;
+ stmt.params.id = prefId;
+ stmt.execute();
+
+ let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ let cacheValue = cps.getPref(uri, prefName);
+
+ do_check_eq(dbValue, NEW_VALUE);
+ do_check_eq(cacheValue, CACHED_VALUE);
+ do_check_neq(cacheValue, dbValue);
+
+ do_test_pending();
+ cps.getPref(uri, prefName, function (value) {
+ do_check_eq(dbValue, NEW_VALUE);
+ do_check_eq(value, CACHED_VALUE);
+ do_check_neq(value, dbValue);
+ do_test_finished();
+ });
+}
+
+function testHasCachedPrefFunction(uri, prefName) {
+ const STARTING_VALUE = 3;
+ const NEW_VALUE = 5;
+
+ do_check_false(isCached(uri, prefName));
+
+ cps.setPref(uri, prefName, STARTING_VALUE);
+
+ let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+ let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+ let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+ "id", groupId, settingId);
+
+ do_check_neq(prefId, undefined);
+
+ let originalValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ do_check_eq(originalValue, STARTING_VALUE);
+
+ let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+ stmt.params.value = NEW_VALUE;
+ stmt.params.id = prefId;
+ stmt.execute();
+
+ let newValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ do_check_eq(newValue, NEW_VALUE);
+
+ let cachedValue = cps.getPref(uri, prefName);
+ do_check_eq(cachedValue, STARTING_VALUE);
+ do_check_true(isCached(uri, prefName));
+}
+
+function testSetCaches(uri, prefName) {
+ cps.setPref(uri, prefName, 0);
+ do_check_true(isCached(uri, prefName));
+}
+
+function testRemovePrefs(uri, prefName) {
+
+ /* removePref */
+ cps.setPref("www1." + uri, prefName, 1);
+
+ do_check_eq(cps.getPref("www1." + uri, prefName), 1);
+
+ cps.removePref("www1." + uri, prefName);
+
+ do_check_false(isCached("www1." + uri, prefName));
+ do_check_false(cps.hasPref("www1." + uri, prefName));
+ do_check_neq(cps.getPref("www1." + uri, prefName), 1);
+
+ /* removeGroupedPrefs */
+ cps.setPref("www2." + uri, prefName, 2);
+ cps.setPref("www3." + uri, prefName, 3);
+
+ do_check_eq(cps.getPref("www2." + uri, prefName), 2);
+ do_check_eq(cps.getPref("www3." + uri, prefName), 3);
+
+ cps.removeGroupedPrefs();
+
+ do_check_false(isCached("www2." + uri, prefName));
+ do_check_false(isCached("www3." + uri, prefName));
+ do_check_false(cps.hasPref("www2." + uri, prefName));
+ do_check_false(cps.hasPref("www3." + uri, prefName));
+ do_check_neq(cps.getPref("www2." + uri, prefName), 2);
+ do_check_neq(cps.getPref("www3." + uri, prefName), 3);
+
+ /* removePrefsByName */
+ cps.setPref("www4." + uri, prefName, 4);
+ cps.setPref("www5." + uri, prefName, 5);
+
+ do_check_eq(cps.getPref("www4." + uri, prefName), 4);
+ do_check_eq(cps.getPref("www5." + uri, prefName), 5);
+
+ cps.removePrefsByName(prefName);
+
+ do_check_false(isCached("www4." + uri, prefName));
+ do_check_false(isCached("www5." + uri, prefName));
+ do_check_false(cps.hasPref("www4." + uri, prefName));
+ do_check_false(cps.hasPref("www5." + uri, prefName));
+ do_check_neq(cps.getPref("www4." + uri, prefName), 4);
+ do_check_neq(cps.getPref("www5." + uri, prefName), 5);
+}
+
+function testGetCaches(uri, prefName) {
+ const VALUE = 4;
+
+ let insertGroup = cps.DBConnection.createStatement("INSERT INTO groups (name) VALUES (:name)");
+ insertGroup.params.name = uri;
+ insertGroup.execute();
+ let groupId = cps.DBConnection.lastInsertRowID;
+
+ let insertSetting = cps.DBConnection.createStatement("INSERT INTO settings (name) VALUES (:name)");
+ insertSetting.params.name = prefName;
+ insertSetting.execute();
+ let settingId = cps.DBConnection.lastInsertRowID;
+
+ let insertPref = cps.DBConnection.createStatement(`
+ INSERT INTO prefs (groupID, settingID, value)
+ VALUES (:groupId, :settingId, :value)
+ `);
+ insertPref.params.groupId = groupId;
+ insertPref.params.settingId = settingId;
+ insertPref.params.value = VALUE;
+ insertPref.execute();
+ let prefId = cps.DBConnection.lastInsertRowID;
+
+ let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+
+ // First access from service should hit the DB
+ let svcValue = cps.getPref(uri, prefName);
+
+ // Second time should get the value from cache
+ let cacheValue = cps.getPref(uri, prefName);
+
+ do_check_eq(VALUE, dbValue);
+ do_check_eq(VALUE, svcValue);
+ do_check_eq(VALUE, cacheValue);
+
+ do_check_true(isCached(uri, prefName));
+}
+
+function testTypeConversions(uri, prefName) {
+ let value;
+
+ cps.setPref(uri, prefName, true);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === 1);
+
+ cps.setPref(uri, prefName, false);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === 0);
+
+ cps.setPref(uri, prefName, null);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === null);
+
+ cps.setPref(uri, prefName, undefined);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === null);
+}
+
+function testNonExistingPrefCachesAsUndefined(uri, prefName) {
+
+ do_check_false(isCached(uri, prefName));
+
+ // Cache the pref
+ let value = cps.getPref(uri, prefName);
+ do_check_true(value === undefined);
+
+ do_check_true(isCached(uri, prefName));
+
+ // Cached pref
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === undefined);
+}
+
+function testCacheEviction(uri, prefName) {
+
+ cps.setPref(uri, prefName, 5);
+ do_check_eq(cps.getPref(uri, prefName), 5);
+ do_check_true(isCached(uri, prefName));
+
+ // try to evict value from cache by adding various other entries
+ const ENTRIES_TO_ADD = 200;
+ for (let i = 0; i < ENTRIES_TO_ADD; i++) {
+ let uriToAdd = "www" + i + uri;
+ cps.setPref(uriToAdd, prefName, 0);
+ }
+
+ do_check_false(isCached(uri, prefName));
+
+}
+
+function selectValue(stmt, columnName, param1, param2) {
+ stmt = cps.DBConnection.createStatement(stmt);
+ if (param1)
+ stmt.params.param1 = param1;
+
+ if (param2)
+ stmt.params.param2 = param2;
+
+ stmt.executeStep();
+ let val = stmt.row[columnName];
+ stmt.reset();
+ stmt.finalize();
+ return val;
+}
+
+function isCached(uri, prefName) {
+ return cps.hasCachedPref(uri, prefName);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js
new file mode 100644
index 000000000..27d239f79
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js
@@ -0,0 +1,34 @@
+/* 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/. */
+var cps = new ContentPrefInstance(null);
+var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+function run_test() {
+ do_test_pending();
+
+ cps.setPref(uri, "asynctest", "pie");
+ do_check_eq(cps.getPref(uri, "asynctest"), "pie");
+
+ cps.getPref(uri, "asynctest", function(aValue) {
+ do_check_eq(aValue, "pie");
+ testCallbackObj();
+ });
+}
+
+function testCallbackObj() {
+ cps.getPref(uri, "asynctest", {
+ onResult: function(aValue) {
+ do_check_eq(aValue, "pie");
+ cps.removePref(uri, "asynctest");
+ testNoResult();
+ }
+ });
+}
+
+function testNoResult() {
+ cps.getPref(uri, "asynctest", function(aValue) {
+ do_check_eq(aValue, undefined);
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_stringGroups.js b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js
new file mode 100644
index 000000000..afce3b64a
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js
@@ -0,0 +1,128 @@
+/* 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() {
+
+ var cps = new ContentPrefInstance(null);
+
+ // Make sure disk synchronization checking is turned off by default.
+ var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+ statement.executeStep();
+ do_check_eq(0, statement.getInt32(0));
+
+ // These are the different types of aGroup arguments we'll test.
+ var anObject = {"foo":"bar"}; // a simple object
+ var uri = ContentPrefTest.getURI("http://www.example.com/"); // nsIURI
+ var stringURI = "www.example.com"; // typeof = "string"
+ var stringObjectURI = new String("www.example.com"); // typeof = "object"
+
+ {
+ // First check that all the methods work or don't work.
+ function simple_test_methods(aGroup, shouldThrow) {
+ var prefName = "test.pref.0";
+ var prefValue = Math.floor(Math.random() * 100);
+
+ if (shouldThrow) {
+ do_check_thrown(function () { cps.getPref(aGroup, prefName); });
+ do_check_thrown(function () { cps.setPref(aGroup, prefName, prefValue); });
+ do_check_thrown(function () { cps.hasPref(aGroup, prefName); });
+ do_check_thrown(function () { cps.removePref(aGroup, prefName); });
+ do_check_thrown(function () { cps.getPrefs(aGroup); });
+ } else {
+ do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+ do_check_true(cps.hasPref(aGroup, prefName));
+ do_check_eq(cps.getPref(aGroup, prefName), prefValue);
+ do_check_eq(cps.removePref(aGroup, prefName), undefined);
+ do_check_false(cps.hasPref(aGroup, prefName));
+ }
+ }
+
+ simple_test_methods(cps, true); // arbitrary nsISupports object, should throw too
+ simple_test_methods(anObject, true);
+ simple_test_methods(uri, false);
+ simple_test_methods(stringURI, false);
+ simple_test_methods(stringObjectURI, false);
+ }
+
+ {
+ // Now we'll check that each argument produces the same result.
+ function complex_test_methods(aGroup) {
+ var prefName = "test.pref.1";
+ var prefValue = Math.floor(Math.random() * 100);
+
+ do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+
+ do_check_true(cps.hasPref(uri, prefName));
+ do_check_true(cps.hasPref(stringURI, prefName));
+ do_check_true(cps.hasPref(stringObjectURI, prefName));
+
+ do_check_eq(cps.getPref(uri, prefName), prefValue);
+ do_check_eq(cps.getPref(stringURI, prefName), prefValue);
+ do_check_eq(cps.getPref(stringObjectURI, prefName), prefValue);
+
+ do_check_eq(cps.removePref(aGroup, prefName), undefined);
+
+ do_check_false(cps.hasPref(uri, prefName));
+ do_check_false(cps.hasPref(stringURI, prefName));
+ do_check_false(cps.hasPref(stringObjectURI, prefName));
+ }
+
+ complex_test_methods(uri);
+ complex_test_methods(stringURI);
+ complex_test_methods(stringObjectURI);
+ }
+
+ {
+ // test getPrefs returns the same prefs
+ do_check_eq(cps.setPref(stringObjectURI, "test.5", 5), undefined);
+ do_check_eq(cps.setPref(stringURI, "test.2", 2), undefined);
+ do_check_eq(cps.setPref(uri, "test.1", 1), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 8, ["test.1", "test.2", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 8, ["test.1", "test.2", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 8, ["test.1", "test.2", "test.5"]);
+
+ do_check_eq(cps.setPref(uri, "test.4", 4), undefined);
+ do_check_eq(cps.setPref(stringObjectURI, "test.0", 0), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+
+ do_check_eq(cps.setPref(stringURI, "test.3", 3), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ }
+}
+
+function do_check_thrown (aCallback) {
+ var exThrown = false;
+ try {
+ aCallback();
+ do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+ } catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ exThrown = true;
+ }
+ do_check_true(exThrown);
+}
+
+function enumerateAndCheck(prefs, expectedSum, expectedNames) {
+ var enumerator = prefs.enumerator;
+ var sum = 0;
+ while (enumerator.hasMoreElements()) {
+ var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ sum += parseInt(property.value);
+
+ // remove the pref name from the list of expected names
+ var index = expectedNames.indexOf(property.name);
+ do_check_true(index >= 0);
+ expectedNames.splice(index, 1);
+ }
+ do_check_eq(sum, expectedSum);
+ // check all pref names have been removed from the array
+ do_check_eq(expectedNames.length, 0);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js
new file mode 100644
index 000000000..24a86bcc0
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+ var uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ var uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ var uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ var uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "one", 1);
+ cps.setPref(uri1, "two", 2);
+ cps.setPref(uri2, "one", 4);
+ cps.setPref(uri3, "three", 8);
+ cps.setPref(uri4, "two", 16);
+
+ cps.removePref(uri3, "three"); // uri3 should be removed now
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+
+ cps.removePrefsByName("two"); // uri4 should be removed now
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+
+ cps.removeGroupedPrefs();
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+}
+
+function checkForUnusedGroups() {
+ var stmt = cps.DBConnection.createStatement(`
+ SELECT COUNT(*) AS count FROM groups
+ WHERE id NOT IN (SELECT DISTINCT groupID FROM prefs)
+ `);
+ stmt.executeStep();
+ do_check_eq(0, stmt.row.count);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function checkForUnusedSettings() {
+ var stmt = cps.DBConnection.createStatement(`
+ SELECT COUNT(*) AS count FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+ stmt.executeStep();
+ do_check_eq(0, stmt.row.count);
+ stmt.reset();
+ stmt.finalize();
+}
diff --git a/toolkit/components/contentprefs/tests/unit/xpcshell.ini b/toolkit/components/contentprefs/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..cbae178b1
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head_contentPrefs.js
+tail = tail_contentPrefs.js
+
+[test_bug248970.js]
+[test_bug503971.js]
+[test_bug679784.js]
+[test_contentPrefs.js]
+[test_contentPrefsCache.js]
+[test_getPrefAsync.js]
+[test_stringGroups.js]
+[test_unusedGroupsAndSettings.js]
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
new file mode 100644
index 000000000..ac878c28c
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
@@ -0,0 +1,69 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = [
+ "AsyncRunner",
+];
+
+const { interfaces: Ci, classes: Cc } = Components;
+
+function AsyncRunner(callbacks) {
+ this._callbacks = callbacks;
+ this._iteratorQueue = [];
+
+ // This catches errors reported to the console, e.g., via Cu.reportError.
+ Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService).
+ 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() {
+ Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService).
+ 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 000000000..b86abe208
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js
@@ -0,0 +1,401 @@
+/* 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/. */
+
+var { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var cps;
+var asyncRunner;
+var next;
+
+(function init() {
+ // There has to be a profile directory before the CPS service is gotten.
+ do_get_profile();
+})();
+
+function runAsyncTests(tests, dontResetBefore = false) {
+ do_test_pending();
+
+ cps = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+
+ let s = {};
+ Cu.import("resource://test/AsyncRunner.jsm", s);
+ asyncRunner = new s.AsyncRunner({
+ done: do_test_finished,
+ error: function (err) {
+ // xpcshell test functions like equal throw NS_ERROR_ABORT on
+ // failure. Ignore those and catch only uncaught exceptions.
+ if (err !== Cr.NS_ERROR_ABORT) {
+ if (err.stack) {
+ err = err + "\n\nTraceback (most recent call first):\n" + err.stack +
+ "\nUseless do_throw stack:";
+ }
+ do_throw(err);
+ }
+ },
+ consoleError: function (scriptErr) {
+ // Previously, this code checked for console errors related to the test,
+ // and treated them as failures. This was problematic, because our current
+ // very-broken exception reporting machinery in XPCWrappedJSClass reports
+ // errors to the console even if there's actually JS on the stack above
+ // that will catch them. And a lot of the tests here intentionally trigger
+ // error conditions on the JS-implemented XPCOM component (see erroneous()
+ // in test_getSubdomains.js, for example). In the old world, we got lucky,
+ // and the errors were never reported to the console due to happenstantial
+ // JSContext reasons that aren't really worth going into.
+ //
+ // So. We make sure to dump this stuff so that it shows up in the logs, but
+ // don't turn them into duplicate failures of the exception that was already
+ // propagated to the caller.
+ dump("AsyncRunner.jsm observed console error: " + scriptErr + "\n");
+ }
+ });
+
+ next = asyncRunner.next.bind(asyncRunner);
+
+ do_register_cleanup(function () {
+ asyncRunner.destroy();
+ asyncRunner = null;
+ });
+
+ tests.forEach(function (test) {
+ function* gen() {
+ do_print("Running " + test.name);
+ yield test();
+ yield reset();
+ }
+ asyncRunner.appendIterator(gen());
+ });
+
+ // reset() ends up calling asyncRunner.next(), starting the tests.
+ if (dontResetBefore) {
+ next();
+ } else {
+ reset();
+ }
+}
+
+function makeCallback(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 {
+ next();
+ }
+ };
+ return callbacks;
+}
+
+function do_check_throws(fn) {
+ let threw = false;
+ try {
+ 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() {
+ sendMessage("reset", next);
+}
+
+function setWithDate(group, name, val, timestamp, context) {
+ function updateDate() {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ UPDATE prefs SET timestamp = :timestamp
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `);
+ stmt.params.timestamp = timestamp / 1000;
+ stmt.params.name = name;
+ stmt.params.group = group;
+
+ stmt.executeAsync({
+ handleCompletion: function (reason) {
+ next();
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+ }
+
+ cps.set(group, name, val, context, makeCallback(null, updateDate));
+}
+
+function getDate(group, name, context) {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ SELECT timestamp FROM prefs
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `);
+ stmt.params.name = name;
+ stmt.params.group = group;
+
+ let res;
+ stmt.executeAsync({
+ handleResult: function (results) {
+ let row = results.getNextRow();
+ res = row.getResultByName("timestamp");
+ },
+ handleCompletion: function (reason) {
+ next(res * 1000);
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+}
+
+function set(group, name, val, context) {
+ cps.set(group, name, val, context, makeCallback());
+}
+
+function setGlobal(name, val, context) {
+ cps.setGlobal(name, val, context, makeCallback());
+}
+
+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);
+}
+
+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 }];
+ yield getOKEx("getByDomainAndName", args, expectedPrefs, strict);
+}
+
+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 };
+ });
+ yield getOKEx("getBySubdomainAndName", args, expectedPrefs);
+}
+
+function* getGlobalOK(args, expectedVal) {
+ if (args.length == 1)
+ args.push(undefined);
+ let expectedPrefs = expectedVal === undefined ? [] :
+ [{ domain: null, name: args[0], value: expectedVal }];
+ yield getOKEx("getGlobal", args, expectedPrefs);
+}
+
+function* getOKEx(methodName, args, expectedPrefs, strict, context) {
+ let actualPrefs = [];
+ args.push(makeCallback({
+ handleResult: pref => actualPrefs.push(pref)
+ }));
+ yield 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 len = {};
+ args.push(len);
+ let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args);
+ actualPrefs = actualPrefs.sort(function (a, b) {
+ return a.domain.localeCompare(b.domain);
+ });
+ equal(actualPrefs.length, len.value);
+ 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)
+ });
+}
+
+function dbOK(expectedRows) {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ 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 actualRows = [];
+ let cols = ["grp", "name", "value"];
+
+ db.executeAsync([stmt], 1, {
+ handleCompletion: function (reason) {
+ arraysOfArraysOK(actualRows, expectedRows);
+ next();
+ },
+ handleResult: function (results) {
+ let row = null;
+ while (row = results.getNextRow()) {
+ actualRows.push(cols.map(c => row.getResultByName(c)));
+ }
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+}
+
+function on(event, names, dontRemove) {
+ let args = {
+ reset: function () {
+ for (let prop in this) {
+ if (Array.isArray(this[prop]))
+ this[prop].splice(0, this[prop].length);
+ }
+ },
+ };
+
+ let observers = {};
+
+ names.forEach(function (name) {
+ let obs = {};
+ ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) {
+ obs[meth] = () => do_throw(meth + " should not be called");
+ });
+ obs["onContentPref" + event] = function () {
+ args[name].push(Array.slice(arguments));
+ };
+ observers[name] = obs;
+ args[name] = [];
+ args[name].observer = obs;
+ cps.addObserverForName(name, obs);
+ });
+
+ do_execute_soon(function () {
+ if (!dontRemove)
+ names.forEach(n => cps.removeObserverForName(n, observers[n]));
+ next(args);
+ });
+}
+
+function schemaVersionIs(expectedVersion) {
+ let db = sendMessage("db");
+ equal(db.schemaVersion, expectedVersion);
+}
+
+function wait() {
+ do_execute_soon(next);
+}
+
+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 000000000..2ec3d6878
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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) {
+ do_check_eq(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 000000000..33a965b7f
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
@@ -0,0 +1,95 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ getCachedOK(["a.com", "foo"], false, undefined);
+ getCachedGlobalOK(["foo"], false, undefined);
+ yield true;
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com");
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+
+ yield set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+
+ yield setGlobal("foo", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ yield setGlobal("bar", 4);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.a.com", "foo"], true, 2);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield 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);
+ },
+
+ 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));
+ yield true;
+ },
+
+ 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.
+
+ yield set("a.com", "foo", false);
+ yield getOK(["a.com", "foo"], 0, "a.com", true);
+ getCachedOK(["a.com", "foo"], true, 0, "a.com", true);
+
+ yield set("a.com", "bar", true);
+ yield getOK(["a.com", "bar"], 1, "a.com", true);
+ getCachedOK(["a.com", "bar"], true, 1, "a.com", true);
+ },
+];
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 000000000..9f2599708
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
@@ -0,0 +1,186 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield true;
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ yield set("a.com", "bar", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ yield setGlobal("foo", 3);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ yield 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);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield 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]]);
+ },
+
+ function* populateViaGet() {
+ yield cps.getByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ yield cps.getGlobal("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ },
+
+ function* populateViaGetSubdomains() {
+ yield cps.getBySubdomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ },
+
+ function* populateViaRemove() {
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ yield cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+
+ yield cps.removeGlobal("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ yield set("a.com", "foo", 2);
+ yield set("b.a.com", "foo", 3);
+ yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ 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]]);
+
+ yield setGlobal("foo", 4);
+ yield cps.removeGlobal("foo", null, makeCallback());
+ 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]]);
+ },
+
+ function* populateViaRemoveByDomain() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.a.com", "foo", 3);
+ yield set("b.a.com", "bar", 4);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", 3]]);
+ getCachedSubdomainsOK(["a.com", "bar"],
+ [["a.com", undefined], ["b.a.com", 4]]);
+
+ yield set("a.com", "foo", 5);
+ yield set("a.com", "bar", 6);
+ yield cps.removeBySubdomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+
+ yield setGlobal("foo", 7);
+ yield setGlobal("bar", 8);
+ yield cps.removeAllGlobals(null, makeCallback());
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ },
+
+ function* populateViaRemoveAllDomains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.com", "foo", 3);
+ yield set("b.com", "bar", 4);
+ yield cps.removeAllDomains(null, makeCallback());
+ 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]]);
+ },
+
+ function* populateViaRemoveByName() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield cps.removeByName("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, 4);
+
+ yield cps.removeByName("bar", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield 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]]);
+ },
+
+ 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));
+ yield true;
+ },
+];
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 000000000..d08d6fe69
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
@@ -0,0 +1,68 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* get_nonexistent() {
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ yield set("a.com", "bar", 2);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ yield setGlobal("foo", 3);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+ yield getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+ yield getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ yield getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+ },
+
+ 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));
+ yield true;
+ },
+];
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 000000000..85d23e355
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js
@@ -0,0 +1,82 @@
+/* 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 dirService = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+ var dbFile = dirService.get("ProfD", Ci.nsIFile);
+ dbFile.append("content-prefs.sqlite");
+
+ var dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ ok(!dbFile.exists(), "Db should not exist yet.");
+
+ var dbConnection = dbService.openDatabase(dbFile);
+ equal(dbConnection.schemaVersion, 0);
+
+ dbConnection.executeSimpleSQL(schema_version3);
+ dbConnection.schemaVersion = 3;
+
+ dbConnection.close();
+}
+
+function run_test() {
+ prepareVersion3Schema();
+ runAsyncTests(tests, true);
+}
+
+
+// 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.
+var tests = [
+ function* testMigration() {
+ // Test migrated db content.
+ schemaVersionIs(4);
+ 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"],
+ ];
+ yield dbOK(dbExpectedState);
+
+ // Migrated fields should have timestamp set to 0.
+ yield cps.removeAllDomainsSince(1000, null, makeCallback());
+ yield dbOK(dbExpectedState);
+
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield dbOK([[null, "zoom-setting", 0.1]]);
+
+ // Test that dates are present after migration (column is added).
+ const timestamp = 1234;
+ yield setWithDate("a.com", "pref-name", "val", timestamp);
+ let actualTimestamp = yield getDate("a.com", "pref-name");
+ equal(actualTimestamp, timestamp);
+ }
+];
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 000000000..c48918cd9
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
@@ -0,0 +1,178 @@
+/* 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 global = this;
+
+function run_test() {
+ var allTests = [];
+ for (var i = 0; i < tests.length; i++) {
+ // Generate two wrappers of each test function that invoke the original test with an
+ // appropriate privacy context.
+ var pub = eval('var f = function* ' + tests[i].name + '() { yield tests[' + i + ']({ usePrivateBrowsing: false }); }; f');
+ var priv = eval('var f = function* ' + tests[i].name + '_private() { yield tests[' + i + ']({ usePrivateBrowsing: true }); }; f');
+ allTests.push(pub);
+ allTests.push(priv);
+ }
+ allTests = allTests.concat(specialTests);
+ runAsyncTests(allTests);
+}
+
+var tests = [
+
+ function* observerForName_set(context) {
+ yield set("a.com", "foo", 1, context);
+ let args = yield on("Set", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+
+ yield setGlobal("foo", 2, context);
+ args = yield on("Set", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_remove(context) {
+ yield set("a.com", "foo", 1, context);
+ yield setGlobal("foo", 2, context);
+
+ yield cps.removeByDomainAndName("a.com", "bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeGlobal("foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_removeByDomain(context) {
+ yield set("a.com", "foo", 1, context);
+ yield set("b.a.com", "bar", 2, context);
+ yield setGlobal("foo", 3, context);
+
+ yield cps.removeByDomain("bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeBySubdomain("a.com", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ 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]]);
+
+ yield cps.removeAllGlobals(context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_removeAllDomains(context) {
+ yield set("a.com", "foo", 1, context);
+ yield setGlobal("foo", 2, context);
+ yield set("b.com", "bar", 3, context);
+
+ yield cps.removeAllDomains(context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ 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]]);
+ },
+
+ function* observerForName_removeByName(context) {
+ yield set("a.com", "foo", 1, context);
+ yield set("a.com", "bar", 2, context);
+ yield setGlobal("foo", 3, context);
+
+ yield cps.removeByName("bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeByName("foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ 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, []);
+ },
+
+ function* removeObserverForName(context) {
+ let args = yield on("Set", ["foo", null, "bar"], true);
+
+ cps.removeObserverForName("foo", args.foo.observer);
+ yield set("a.com", "foo", 1, context);
+ yield wait();
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ args.reset();
+
+ cps.removeObserverForName(null, args.null.observer);
+ yield set("a.com", "foo", 2, context);
+ yield wait();
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+ args.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 = [
+ function* observerForName_removeAllDomainsSince() {
+ yield setWithDate("a.com", "foo", 1, 100, null);
+ yield setWithDate("b.com", "foo", 2, 200, null);
+ yield setWithDate("c.com", "foo", 3, 300, null);
+
+ yield setWithDate("a.com", "bar", 1, 0, null);
+ yield setWithDate("b.com", "bar", 2, 100, null);
+ yield setWithDate("c.com", "bar", 3, 200, null);
+ yield setGlobal("foo", 2, null);
+
+ yield cps.removeAllDomainsSince(200, null, makeCallback());
+
+ let args = yield on("Removed", ["foo", "bar", null]);
+
+ 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]]);
+ },
+
+ function* observerForName_removeAllDomainsSince_private() {
+ let context = {usePrivateBrowsing: true};
+ yield setWithDate("a.com", "foo", 1, 100, context);
+ yield setWithDate("b.com", "foo", 2, 200, context);
+ yield setWithDate("c.com", "foo", 3, 300, context);
+
+ yield setWithDate("a.com", "bar", 1, 0, context);
+ yield setWithDate("b.com", "bar", 2, 100, context);
+ yield setWithDate("c.com", "bar", 3, 200, context);
+ yield setGlobal("foo", 2, context);
+
+ yield cps.removeAllDomainsSince(200, context, makeCallback());
+
+ let args = yield on("Removed", ["foo", "bar", null]);
+
+ 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]]);
+ },
+];
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 000000000..9853293fc
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
@@ -0,0 +1,222 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByDomainAndName("a.com", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeGlobal("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeByDomainAndName("bogus", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+
+ yield set("a.com", "foo", 2);
+ yield cps.removeByDomainAndName("http://a.com/huh", "foo", null,
+ makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeGlobal("foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeByDomainAndName("a.com", "bar", null, makeCallback());
+ yield dbOK([
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeGlobal("bar", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ yield set("a.com", "foo", 3);
+ yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("a.com", "foo", 4);
+ yield set("b.a.com", "foo", 5);
+ yield cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("b.a.com", "foo", 6);
+ yield cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setGlobal("qux", 5);
+ yield set("b.com", "foo", 6);
+ yield set("b.com", "bar", 7);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 8, context);
+ yield setGlobal("foo", 9, context);
+ yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback());
+ yield cps.removeGlobal("foo", context, makeCallback());
+ yield cps.removeGlobal("qux", context, makeCallback());
+ yield cps.removeByDomainAndName("b.com", "foo", context, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 7],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], 2);
+ yield getGlobalOK(["foo", context], undefined);
+ yield getGlobalOK(["bar", context], 4);
+ yield getGlobalOK(["qux", context], undefined);
+ yield getOK(["b.com", "foo", context], undefined);
+ yield getOK(["b.com", "bar", context], 7);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+ yield getGlobalOK(["qux"], undefined);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], 7);
+ },
+
+ 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"));
+ yield true;
+ },
+
+ function* removeByDomainAndName_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ yield;
+ },
+
+ function* removeBySubdomainAndName_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]);
+ cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield;
+ },
+
+ function* removeGlobal_invalidateCache() {
+ yield setGlobal("foo", 1);
+ getCachedGlobalOK(["foo"], true, 1);
+ cps.removeGlobal("foo", null, makeCallback());
+ getCachedGlobalOK(["foo"], false);
+ yield;
+ },
+];
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 000000000..63e1b0552
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
@@ -0,0 +1,87 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield setGlobal("foo", 1);
+ yield cps.removeAllDomains(null, makeCallback());
+ yield dbOK([
+ [null, "foo", 1],
+ ]);
+ yield getGlobalOK(["foo"], 1);
+ },
+
+ function* domains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeAllDomains(null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield cps.removeAllDomains(context, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], undefined);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeAllDomains(null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.com", "bar", 2);
+ yield setGlobal("baz", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.com", "bar"], true, 2);
+ getCachedGlobalOK(["baz"], true, 3);
+ cps.removeAllDomains(null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["b.com", "bar"], false);
+ getCachedGlobalOK(["baz"], true, 3);
+ yield;
+ },
+];
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 000000000..fa0bf31c3
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js
@@ -0,0 +1,111 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield setGlobal("foo", 1);
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield getGlobalOK(["foo"], 1);
+ },
+
+ function* domainsAll() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* domainsWithDate() {
+ yield setWithDate("a.com", "foobar", 0, 0);
+ yield setWithDate("a.com", "foo", 1, 1000);
+ yield setWithDate("a.com", "bar", 2, 4000);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setWithDate("b.com", "foo", 5, 2000);
+ yield setWithDate("b.com", "bar", 6, 3000);
+ yield setWithDate("b.com", "foobar", 7, 1000);
+
+ yield cps.removeAllDomainsSince(2000, null, makeCallback());
+ yield dbOK([
+ ["a.com", "foobar", 0],
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foobar", 7],
+ ]);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield cps.removeAllDomainsSince(0, context, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], undefined);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeAllDomainsSince(null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield setWithDate("a.com", "foobar", 0, 0);
+ yield setWithDate("a.com", "foo", 1, 1000);
+ yield setWithDate("a.com", "bar", 2, 4000);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setWithDate("b.com", "foo", 5, 2000);
+ yield setWithDate("b.com", "bar", 6, 3000);
+ yield setWithDate("b.com", "foobar", 7, 1000);
+ cps.removeAllDomainsSince(0, null, makeCallback());
+ 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);
+ yield true;
+ },
+];
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 000000000..1cf6bd8f2
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
@@ -0,0 +1,199 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByDomain("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeBySubdomain("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+
+ yield set("a.com", "foo", 2);
+ yield cps.removeByDomain("http://a.com/huh", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+ },
+
+ function* domains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], 6);
+
+ yield cps.removeAllGlobals(null, makeCallback());
+ yield dbOK([
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], 6);
+
+ yield cps.removeByDomain("b.com", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ yield set("a.com", "foo", 3);
+ yield cps.removeBySubdomain("a.com", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("a.com", "foo", 4);
+ yield set("b.a.com", "foo", 5);
+ yield cps.removeByDomain("b.a.com", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("b.a.com", "foo", 6);
+ yield cps.removeBySubdomain("b.a.com", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield set("b.com", "foo", 7, context);
+ yield setGlobal("foo", 8, context);
+ yield cps.removeByDomain("a.com", context, makeCallback());
+ yield getOK(["b.com", "foo", context], 7);
+ yield getGlobalOK(["foo", context], 8);
+ yield cps.removeAllGlobals(context, makeCallback());
+ yield dbOK([
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], undefined);
+ yield getGlobalOK(["bar", context], undefined);
+ yield getOK(["b.com", "foo", context], 5);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ },
+
+ 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"));
+ yield true;
+ },
+
+ function* removeByDomain_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ cps.removeByDomain("a.com", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["a.com", "bar"], false);
+ yield;
+ },
+
+ function* removeBySubdomain_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]);
+ cps.removeBySubdomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield;
+ },
+
+ function* removeAllGlobals_invalidateCache() {
+ yield setGlobal("foo", 1);
+ yield setGlobal("bar", 2);
+ getCachedGlobalOK(["foo"], true, 1);
+ getCachedGlobalOK(["bar"], true, 2);
+ cps.removeAllGlobals(null, makeCallback());
+ getCachedGlobalOK(["foo"], false);
+ getCachedGlobalOK(["bar"], false);
+ yield;
+ },
+];
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 000000000..fa04656e2
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
@@ -0,0 +1,96 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByName("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeByName("foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], 6);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 7, context);
+ yield setGlobal("foo", 8, context);
+ yield set("b.com", "bar", 9, context);
+ yield cps.removeByName("bar", context, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], 7);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 8);
+ yield getGlobalOK(["bar", context], undefined);
+ yield getOK(["b.com", "foo", context], 5);
+ yield getOK(["b.com", "bar", context], undefined);
+
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeByName("", null));
+ do_check_throws(() => cps.removeByName(null, null));
+ do_check_throws(() => cps.removeByName("foo", null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield 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);
+ yield;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_service.js b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
new file mode 100644
index 000000000..75292063e
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
@@ -0,0 +1,12 @@
+/* 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 serv = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+ do_check_eq(serv.QueryInterface(Ci.nsIContentPrefService2), serv);
+ do_check_eq(serv.QueryInterface(Ci.nsISupports), serv);
+ let val = serv.QueryInterface(Ci.nsIContentPrefService);
+ do_check_true(val instanceof Ci.nsIContentPrefService);
+}
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 000000000..b10a05bbc
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
@@ -0,0 +1,206 @@
+/* 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() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* get_nonexistent() {
+ yield getOK(["a.com", "foo"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["http://a.com/huh", "foo"], 1, "a.com");
+
+ yield set("http://a.com/huh", "foo", 2);
+ yield dbOK([
+ ["a.com", "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 2);
+ yield getOK(["http://a.com/yeah", "foo"], 2, "a.com");
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+
+ yield set("a.com", "bar", 2);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+
+ yield setGlobal("foo", 3);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+
+ yield setGlobal("bar", 4);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["b.a.com", "foo"], 2);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], 6, "a.com");
+ yield getOK(["a.com", "bar", context], 2);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], 5);
+
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], 5);
+ },
+
+ 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"));
+ yield true;
+ },
+
+ 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));
+ yield true;
+ },
+
+ function* set_invalidateCache() {
+ // (1) Set a pref and wait for it to finish.
+ yield 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: function () {
+ // (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.
+ var fetchedPref;
+ cps.getByDomainAndName("a.com", "foo", null, {
+ handleResult: function (pref) {
+ fetchedPref = pref;
+ },
+ handleCompletion: function () {
+ // (7) Finally, this callback should be called after set's above.
+ do_check_true(!!fetchedPref);
+ do_check_eq(fetchedPref.value, 2);
+ next();
+ },
+ });
+
+ yield;
+ },
+
+ function* get_nameOnly() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.com", "foo", 3);
+ yield setGlobal("foo", 4);
+
+ yield 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 = { usePrivateBrowsing: true };
+ yield set("b.com", "foo", 5, context);
+
+ yield getOKEx("getByName", ["foo", context], [
+ {"domain": "a.com", "name": "foo", "value": 1},
+ {"domain": null, "name": "foo", "value": 4},
+ {"domain": "b.com", "name": "foo", "value": 5}
+ ]);
+ },
+
+ 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;
+ yield set("a.com", "foo", 1);
+ let timestamp = yield getDate("a.com", "foo");
+ ok(start <= timestamp, "Timestamp is not too early (" + start + "<=" + timestamp + ").");
+ ok(timestamp <= end, "Timestamp is not too late (" + timestamp + "<=" + end + ").");
+ },
+];
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 000000000..bdbcaf8fd
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+support-files = AsyncRunner.jsm
+
+[test_service.js]
+[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_removeAllDomainsSince.js]