summaryrefslogtreecommitdiff
path: root/components/contentprefs
diff options
context:
space:
mode:
Diffstat (limited to 'components/contentprefs')
-rw-r--r--components/contentprefs/ContentPrefInstance.jsm75
-rw-r--r--components/contentprefs/ContentPrefService2.jsm885
-rw-r--r--components/contentprefs/ContentPrefServiceChild.jsm181
-rw-r--r--components/contentprefs/ContentPrefServiceParent.jsm136
-rw-r--r--components/contentprefs/ContentPrefStore.jsm123
-rw-r--r--components/contentprefs/ContentPrefUtils.jsm69
-rw-r--r--components/contentprefs/moz.build18
-rw-r--r--components/contentprefs/nsContentPrefService.js1332
-rw-r--r--components/contentprefs/nsContentPrefService.manifest5
9 files changed, 2824 insertions, 0 deletions
diff --git a/components/contentprefs/ContentPrefInstance.jsm b/components/contentprefs/ContentPrefInstance.jsm
new file mode 100644
index 000000000..395569995
--- /dev/null
+++ b/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/components/contentprefs/ContentPrefService2.jsm b/components/contentprefs/ContentPrefService2.jsm
new file mode 100644
index 000000000..87063d170
--- /dev/null
+++ b/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/components/contentprefs/ContentPrefServiceChild.jsm b/components/contentprefs/ContentPrefServiceChild.jsm
new file mode 100644
index 000000000..af2a3c14b
--- /dev/null
+++ b/components/contentprefs/ContentPrefServiceChild.jsm
@@ -0,0 +1,181 @@
+/* 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/components/contentprefs/ContentPrefServiceParent.jsm b/components/contentprefs/ContentPrefServiceParent.jsm
new file mode 100644
index 000000000..2b425c42f
--- /dev/null
+++ b/components/contentprefs/ContentPrefServiceParent.jsm
@@ -0,0 +1,136 @@
+/* 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/components/contentprefs/ContentPrefStore.jsm b/components/contentprefs/ContentPrefStore.jsm
new file mode 100644
index 000000000..7a552662f
--- /dev/null
+++ b/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/components/contentprefs/ContentPrefUtils.jsm b/components/contentprefs/ContentPrefUtils.jsm
new file mode 100644
index 000000000..72c12e558
--- /dev/null
+++ b/components/contentprefs/ContentPrefUtils.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/. */
+
+"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/components/contentprefs/moz.build b/components/contentprefs/moz.build
new file mode 100644
index 000000000..0733f41b6
--- /dev/null
+++ b/components/contentprefs/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'nsContentPrefService.js',
+ 'nsContentPrefService.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'ContentPrefInstance.jsm',
+ 'ContentPrefService2.jsm',
+ 'ContentPrefServiceChild.jsm',
+ 'ContentPrefServiceParent.jsm',
+ 'ContentPrefStore.jsm',
+ 'ContentPrefUtils.jsm',
+]
diff --git a/components/contentprefs/nsContentPrefService.js b/components/contentprefs/nsContentPrefService.js
new file mode 100644
index 000000000..6360134a5
--- /dev/null
+++ b/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/components/contentprefs/nsContentPrefService.manifest b/components/contentprefs/nsContentPrefService.manifest
new file mode 100644
index 000000000..b6bc15721
--- /dev/null
+++ b/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}
+