diff -r 000000000000 -r 6474c204b198 toolkit/components/contentprefs/ContentPrefService2.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/components/contentprefs/ContentPrefService2.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,887 @@ +/* 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 + +let 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/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); + +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] in 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] in 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] in + 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] in 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 ($)" + ) : + 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) { + let stmt = this._stmt(joinArgs(Array.slice(arguments, 2)).replace("$", + "SELECT id " + + "FROM groups " + + "WHERE name = :group OR " + + "(:includeSubdomains AND name LIKE :pattern ESCAPE '/')" + )); + 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] in store.match(group, name, includeSubdomains)) { + outStore.set(sgroup, name, val); + } + }); + + let prefs = []; + for (let [sgroup, sname, val] in 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); + }); + 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)", + "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", + ")" + ); + stmt.params.group = group; + } + else { + stmt = this._stmt( + "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", + "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", + ")" + ); + } + stmt.params.name = name; + stmt.params.value = value; + 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); + }, + 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 in 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 ($)", + "END" + ); + stmt.params.name = name; + stmts.push(stmt); + + // Delete settings and groups that are no longer used. 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. + stmts.push(this._stmt( + "DELETE FROM settings", + "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" + )); + stmts.push(this._stmt( + "DELETE FROM groups WHERE id NOT IN (", + "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", + ")" + )); + + let prefs = new ContentPrefStore(); + + 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 (context && context.usePrivateBrowsing) { + for (let [sgroup, ] in + this._pbStore.match(group, name, includeSubdomains)) { + prefs.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); + } + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, , ] in prefs) { + this._cps._notifyPrefRemoved(sgroup, name); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + 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 in 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 ($)" + )); + stmts.push(this._stmtWithGroupClause(group, includeSubdomains, + "DELETE FROM groups WHERE id IN ($)" + )); + 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(); + + 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 && context && context.usePrivateBrowsing) { + for (let [sgroup, sname, ] in this._pbStore) { + prefs.set(sgroup, sname, undefined); + this._pbStore.remove(sgroup, sname); + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, sname, ] in prefs) { + this._cps._notifyPrefRemoved(sgroup, sname); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + removeAllDomains: function CPS2_removeAllDomains(context, callback) { + checkCallbackArg(callback, false); + + // Invalidate the cached values so consumers accessing the cache between now + // and when the operation finishes don't get old data. + this._cache.removeAllGroups(); + + let stmts = []; + + // First get the matching prefs. + stmts.push(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" + )); + + stmts.push(this._stmt( + "DELETE FROM prefs WHERE groupID NOTNULL" + )); + stmts.push(this._stmt( + "DELETE FROM groups" + )); + stmts.push(this._stmt( + "DELETE FROM settings", + "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" + )); + + let prefs = new ContentPrefStore(); + + 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 && context && context.usePrivateBrowsing) { + for (let [sgroup, sname, ] in this._pbStore) { + prefs.set(sgroup, sname, undefined); + } + this._pbStore.removeAllGroups(); + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, sname, ] in prefs) { + this._cps._notifyPrefRemoved(sgroup, sname); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + 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, ] in 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(); + + 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 && context && context.usePrivateBrowsing) { + for (let [sgroup, sname, ] in this._pbStore) { + if (sname === name) { + prefs.set(sgroup, name, undefined); + this._pbStore.remove(sgroup, name); + } + } + } + cbHandleCompletion(callback, reason); + if (ok) { + for (let [sgroup, , ] in prefs) { + this._cps._notifyPrefRemoved(sgroup, name); + } + } + }, + onError: function onError(nsresult) { + cbHandleError(callback, nsresult); + } + }); + }, + + destroy: function CPS2_destroy() { + for each (let stmt in this._statements) { + 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. If more than one string is given, then + * all are concatenated. The concatenation process inserts + * spaces where appropriate and removes unnecessary contiguous + * spaces. Call like _stmt("SELECT *", "FROM foo"). + * @return The cached, possibly new, statement. + */ + _stmt: function CPS2__stmt(sql /*, sql2, sql3, ... */) { + let sql = joinArgs(arguments); + 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(function (t) this._stmt("DELETE FROM", t), this); + this._execStmts(stmts, { onDone: function () callback() }); + }, + + QueryInterface: function CPS2_QueryInterface(iid) { + let supportedIIDs = [ + Ci.nsIContentPrefService2, + Ci.nsIObserver, + Ci.nsISupports, + ]; + if (supportedIIDs.some(function (i) iid.equals(i))) + return this; + if (iid.equals(Ci.nsIContentPrefService)) + return this._cps; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +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); + } +} + +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); +} + +function joinArgs(args) { + return Array.join(args, " ").trim().replace(/\s{2,}/g, " "); +}