diff options
Diffstat (limited to 'components/contentprefs')
-rw-r--r-- | components/contentprefs/ContentPrefInstance.jsm | 75 | ||||
-rw-r--r-- | components/contentprefs/ContentPrefService2.jsm | 885 | ||||
-rw-r--r-- | components/contentprefs/ContentPrefServiceChild.jsm | 181 | ||||
-rw-r--r-- | components/contentprefs/ContentPrefServiceParent.jsm | 136 | ||||
-rw-r--r-- | components/contentprefs/ContentPrefStore.jsm | 123 | ||||
-rw-r--r-- | components/contentprefs/ContentPrefUtils.jsm | 69 | ||||
-rw-r--r-- | components/contentprefs/moz.build | 18 | ||||
-rw-r--r-- | components/contentprefs/nsContentPrefService.js | 1332 | ||||
-rw-r--r-- | components/contentprefs/nsContentPrefService.manifest | 5 |
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} + |