michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // This file is an XPCOM component that implements nsIContentPrefService2. michael@0: // Although it's a JSM, it's not intended to be imported by consumers like JSMs michael@0: // are usually imported. It's only a JSM so that nsContentPrefService.js can michael@0: // easily use it. Consumers should access this component with the usual XPCOM michael@0: // rigmarole: michael@0: // michael@0: // Cc["@mozilla.org/content-pref/service;1"]. michael@0: // getService(Ci.nsIContentPrefService2); michael@0: // michael@0: // That contract ID actually belongs to nsContentPrefService.js, which, when michael@0: // QI'ed to nsIContentPrefService2, returns an instance of this component. michael@0: // michael@0: // The plan is to eventually remove nsIContentPrefService and its michael@0: // implementation, nsContentPrefService.js. At such time this file can stop michael@0: // being a JSM, and the "_cps" parts that ContentPrefService2 relies on and michael@0: // NSGetFactory and all the other XPCOM initialization goop in michael@0: // nsContentPrefService.js can be moved here. michael@0: // michael@0: // See https://bugzilla.mozilla.org/show_bug.cgi?id=699859 michael@0: michael@0: let EXPORTED_SYMBOLS = [ michael@0: "ContentPrefService2", michael@0: ]; michael@0: michael@0: const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/ContentPrefStore.jsm"); michael@0: michael@0: function ContentPrefService2(cps) { michael@0: this._cps = cps; michael@0: this._cache = cps._cache; michael@0: this._pbStore = cps._privModeStorage; michael@0: } michael@0: michael@0: ContentPrefService2.prototype = { michael@0: michael@0: getByName: function CPS2_getByName(name, context, callback) { michael@0: checkNameArg(name); michael@0: checkCallbackArg(callback, true); michael@0: michael@0: // Some prefs may be in both the database and the private browsing store. michael@0: // Notify the caller of such prefs only once, using the values from private michael@0: // browsing. michael@0: let pbPrefs = new ContentPrefStore(); michael@0: if (context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, sname, val] in this._pbStore) { michael@0: if (sname == name) { michael@0: pbPrefs.set(sgroup, sname, val); michael@0: } michael@0: } michael@0: } michael@0: michael@0: let stmt1 = this._stmt( michael@0: "SELECT groups.name AS grp, prefs.value AS value", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "JOIN groups ON groups.id = prefs.groupID", michael@0: "WHERE settings.name = :name" michael@0: ); michael@0: stmt1.params.name = name; michael@0: michael@0: let stmt2 = this._stmt( michael@0: "SELECT NULL AS grp, prefs.value AS value", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE settings.name = :name AND prefs.groupID ISNULL" michael@0: ); michael@0: stmt2.params.name = name; michael@0: michael@0: this._execStmts([stmt1, stmt2], { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: let val = row.getResultByName("value"); michael@0: this._cache.set(grp, name, val); michael@0: if (!pbPrefs.has(grp, name)) michael@0: cbHandleResult(callback, new ContentPref(grp, name, val)); michael@0: }, michael@0: onDone: function onDone(reason, ok, gotRow) { michael@0: if (ok) { michael@0: for (let [pbGroup, pbName, pbVal] in pbPrefs) { michael@0: cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); michael@0: } michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: getByDomainAndName: function CPS2_getByDomainAndName(group, name, context, michael@0: callback) { michael@0: checkGroupArg(group); michael@0: this._get(group, name, false, context, callback); michael@0: }, michael@0: michael@0: getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name, michael@0: context, michael@0: callback) { michael@0: checkGroupArg(group); michael@0: this._get(group, name, true, context, callback); michael@0: }, michael@0: michael@0: getGlobal: function CPS2_getGlobal(name, context, callback) { michael@0: this._get(null, name, false, context, callback); michael@0: }, michael@0: michael@0: _get: function CPS2__get(group, name, includeSubdomains, context, callback) { michael@0: group = this._parseGroup(group); michael@0: checkNameArg(name); michael@0: checkCallbackArg(callback, true); michael@0: michael@0: // Some prefs may be in both the database and the private browsing store. michael@0: // Notify the caller of such prefs only once, using the values from private michael@0: // browsing. michael@0: let pbPrefs = new ContentPrefStore(); michael@0: if (context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, val] in michael@0: this._pbStore.match(group, name, includeSubdomains)) { michael@0: pbPrefs.set(sgroup, name, val); michael@0: } michael@0: } michael@0: michael@0: this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: let val = row.getResultByName("value"); michael@0: this._cache.set(grp, name, val); michael@0: if (!pbPrefs.has(group, name)) michael@0: cbHandleResult(callback, new ContentPref(grp, name, val)); michael@0: }, michael@0: onDone: function onDone(reason, ok, gotRow) { michael@0: if (ok) { michael@0: if (!gotRow) michael@0: this._cache.set(group, name, undefined); michael@0: for (let [pbGroup, pbName, pbVal] in pbPrefs) { michael@0: cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); michael@0: } michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) { michael@0: let stmt = group ? michael@0: this._stmtWithGroupClause(group, includeSubdomains, michael@0: "SELECT groups.name AS grp, prefs.value AS value", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "JOIN groups ON groups.id = prefs.groupID", michael@0: "WHERE settings.name = :name AND prefs.groupID IN ($)" michael@0: ) : michael@0: this._stmt( michael@0: "SELECT NULL AS grp, prefs.value AS value", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE settings.name = :name AND prefs.groupID ISNULL" michael@0: ); michael@0: stmt.params.name = name; michael@0: return stmt; michael@0: }, michael@0: michael@0: _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group, michael@0: includeSubdomains) { michael@0: let stmt = this._stmt(joinArgs(Array.slice(arguments, 2)).replace("$", michael@0: "SELECT id " + michael@0: "FROM groups " + michael@0: "WHERE name = :group OR " + michael@0: "(:includeSubdomains AND name LIKE :pattern ESCAPE '/')" michael@0: )); michael@0: stmt.params.group = group; michael@0: stmt.params.includeSubdomains = includeSubdomains || false; michael@0: stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/"); michael@0: return stmt; michael@0: }, michael@0: michael@0: getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group, michael@0: name, michael@0: context) { michael@0: checkGroupArg(group); michael@0: let prefs = this._getCached(group, name, false, context); michael@0: return prefs[0] || null; michael@0: }, michael@0: michael@0: getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group, michael@0: name, michael@0: context, michael@0: len) { michael@0: checkGroupArg(group); michael@0: let prefs = this._getCached(group, name, true, context); michael@0: if (len) michael@0: len.value = prefs.length; michael@0: return prefs; michael@0: }, michael@0: michael@0: getCachedGlobal: function CPS2_getCachedGlobal(name, context) { michael@0: let prefs = this._getCached(null, name, false, context); michael@0: return prefs[0] || null; michael@0: }, michael@0: michael@0: _getCached: function CPS2__getCached(group, name, includeSubdomains, michael@0: context) { michael@0: group = this._parseGroup(group); michael@0: checkNameArg(name); michael@0: michael@0: let storesToCheck = [this._cache]; michael@0: if (context && context.usePrivateBrowsing) michael@0: storesToCheck.push(this._pbStore); michael@0: michael@0: let outStore = new ContentPrefStore(); michael@0: storesToCheck.forEach(function (store) { michael@0: for (let [sgroup, val] in store.match(group, name, includeSubdomains)) { michael@0: outStore.set(sgroup, name, val); michael@0: } michael@0: }); michael@0: michael@0: let prefs = []; michael@0: for (let [sgroup, sname, val] in outStore) { michael@0: prefs.push(new ContentPref(sgroup, sname, val)); michael@0: } michael@0: return prefs; michael@0: }, michael@0: michael@0: set: function CPS2_set(group, name, value, context, callback) { michael@0: checkGroupArg(group); michael@0: this._set(group, name, value, context, callback); michael@0: }, michael@0: michael@0: setGlobal: function CPS2_setGlobal(name, value, context, callback) { michael@0: this._set(null, name, value, context, callback); michael@0: }, michael@0: michael@0: _set: function CPS2__set(group, name, value, context, callback) { michael@0: group = this._parseGroup(group); michael@0: checkNameArg(name); michael@0: checkValueArg(value); michael@0: checkCallbackArg(callback, false); michael@0: michael@0: if (context && context.usePrivateBrowsing) { michael@0: this._pbStore.set(group, name, value); michael@0: this._schedule(function () { michael@0: cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK); michael@0: this._cps._notifyPrefSet(group, name, value); michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: // Invalidate the cached value so consumers accessing the cache between now michael@0: // and when the operation finishes don't get old data. michael@0: this._cache.remove(group, name); michael@0: michael@0: let stmts = []; michael@0: michael@0: // Create the setting if it doesn't exist. michael@0: let stmt = this._stmt( michael@0: "INSERT OR IGNORE INTO settings (id, name)", michael@0: "VALUES((SELECT id FROM settings WHERE name = :name), :name)" michael@0: ); michael@0: stmt.params.name = name; michael@0: stmts.push(stmt); michael@0: michael@0: // Create the group if it doesn't exist. michael@0: if (group) { michael@0: stmt = this._stmt( michael@0: "INSERT OR IGNORE INTO groups (id, name)", michael@0: "VALUES((SELECT id FROM groups WHERE name = :group), :group)" michael@0: ); michael@0: stmt.params.group = group; michael@0: stmts.push(stmt); michael@0: } michael@0: michael@0: // Finally create or update the pref. michael@0: if (group) { michael@0: stmt = this._stmt( michael@0: "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", michael@0: "VALUES(", michael@0: "(SELECT prefs.id", michael@0: "FROM prefs", michael@0: "JOIN groups ON groups.id = prefs.groupID", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE groups.name = :group AND settings.name = :name),", michael@0: "(SELECT id FROM groups WHERE name = :group),", michael@0: "(SELECT id FROM settings WHERE name = :name),", michael@0: ":value", michael@0: ")" michael@0: ); michael@0: stmt.params.group = group; michael@0: } michael@0: else { michael@0: stmt = this._stmt( michael@0: "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", michael@0: "VALUES(", michael@0: "(SELECT prefs.id", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE prefs.groupID IS NULL AND settings.name = :name),", michael@0: "NULL,", michael@0: "(SELECT id FROM settings WHERE name = :name),", michael@0: ":value", michael@0: ")" michael@0: ); michael@0: } michael@0: stmt.params.name = name; michael@0: stmt.params.value = value; michael@0: stmts.push(stmt); michael@0: michael@0: this._execStmts(stmts, { michael@0: onDone: function onDone(reason, ok) { michael@0: if (ok) michael@0: this._cache.setWithCast(group, name, value); michael@0: cbHandleCompletion(callback, reason); michael@0: if (ok) michael@0: this._cps._notifyPrefSet(group, name, value); michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: removeByDomainAndName: function CPS2_removeByDomainAndName(group, name, michael@0: context, michael@0: callback) { michael@0: checkGroupArg(group); michael@0: this._remove(group, name, false, context, callback); michael@0: }, michael@0: michael@0: removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name, michael@0: context, michael@0: callback) { michael@0: checkGroupArg(group); michael@0: this._remove(group, name, true, context, callback); michael@0: }, michael@0: michael@0: removeGlobal: function CPS2_removeGlobal(name, context,callback) { michael@0: this._remove(null, name, false, context, callback); michael@0: }, michael@0: michael@0: _remove: function CPS2__remove(group, name, includeSubdomains, context, michael@0: callback) { michael@0: group = this._parseGroup(group); michael@0: checkNameArg(name); michael@0: checkCallbackArg(callback, false); michael@0: michael@0: // Invalidate the cached values so consumers accessing the cache between now michael@0: // and when the operation finishes don't get old data. michael@0: for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { michael@0: this._cache.remove(sgroup, name); michael@0: } michael@0: michael@0: let stmts = []; michael@0: michael@0: // First get the matching prefs. michael@0: stmts.push(this._commonGetStmt(group, name, includeSubdomains)); michael@0: michael@0: // Delete the matching prefs. michael@0: let stmt = this._stmtWithGroupClause(group, includeSubdomains, michael@0: "DELETE FROM prefs", michael@0: "WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND", michael@0: "CASE typeof(:group)", michael@0: "WHEN 'null' THEN prefs.groupID IS NULL", michael@0: "ELSE prefs.groupID IN ($)", michael@0: "END" michael@0: ); michael@0: stmt.params.name = name; michael@0: stmts.push(stmt); michael@0: michael@0: // Delete settings and groups that are no longer used. The NOTNULL term in michael@0: // the subquery of the second statment is needed because of SQLite's weird michael@0: // IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html. michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM settings", michael@0: "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM groups WHERE id NOT IN (", michael@0: "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", michael@0: ")" michael@0: )); michael@0: michael@0: let prefs = new ContentPrefStore(); michael@0: michael@0: this._execStmts(stmts, { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: prefs.set(grp, name, undefined); michael@0: this._cache.set(grp, name, undefined); michael@0: }, michael@0: onDone: function onDone(reason, ok) { michael@0: if (ok) { michael@0: this._cache.set(group, name, undefined); michael@0: if (context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, ] in michael@0: this._pbStore.match(group, name, includeSubdomains)) { michael@0: prefs.set(sgroup, name, undefined); michael@0: this._pbStore.remove(sgroup, name); michael@0: } michael@0: } michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: if (ok) { michael@0: for (let [sgroup, , ] in prefs) { michael@0: this._cps._notifyPrefRemoved(sgroup, name); michael@0: } michael@0: } michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: removeByDomain: function CPS2_removeByDomain(group, context, callback) { michael@0: checkGroupArg(group); michael@0: this._removeByDomain(group, false, context, callback); michael@0: }, michael@0: michael@0: removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) { michael@0: checkGroupArg(group); michael@0: this._removeByDomain(group, true, context, callback); michael@0: }, michael@0: michael@0: removeAllGlobals: function CPS2_removeAllGlobals(context, callback) { michael@0: this._removeByDomain(null, false, context, callback); michael@0: }, michael@0: michael@0: _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains, michael@0: context, callback) { michael@0: group = this._parseGroup(group); michael@0: checkCallbackArg(callback, false); michael@0: michael@0: // Invalidate the cached values so consumers accessing the cache between now michael@0: // and when the operation finishes don't get old data. michael@0: for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { michael@0: this._cache.removeGroup(sgroup); michael@0: } michael@0: michael@0: let stmts = []; michael@0: michael@0: // First get the matching prefs, then delete groups and prefs that reference michael@0: // deleted groups. michael@0: if (group) { michael@0: stmts.push(this._stmtWithGroupClause(group, includeSubdomains, michael@0: "SELECT groups.name AS grp, settings.name AS name", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "JOIN groups ON groups.id = prefs.groupID", michael@0: "WHERE prefs.groupID IN ($)" michael@0: )); michael@0: stmts.push(this._stmtWithGroupClause(group, includeSubdomains, michael@0: "DELETE FROM groups WHERE id IN ($)" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM prefs", michael@0: "WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)" michael@0: )); michael@0: } michael@0: else { michael@0: stmts.push(this._stmt( michael@0: "SELECT NULL AS grp, settings.name AS name", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE prefs.groupID IS NULL" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM prefs WHERE groupID IS NULL" michael@0: )); michael@0: } michael@0: michael@0: // Finally delete settings that are no longer referenced. michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM settings", michael@0: "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" michael@0: )); michael@0: michael@0: let prefs = new ContentPrefStore(); michael@0: michael@0: this._execStmts(stmts, { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: let name = row.getResultByName("name"); michael@0: prefs.set(grp, name, undefined); michael@0: this._cache.set(grp, name, undefined); michael@0: }, michael@0: onDone: function onDone(reason, ok) { michael@0: if (ok && context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, sname, ] in this._pbStore) { michael@0: prefs.set(sgroup, sname, undefined); michael@0: this._pbStore.remove(sgroup, sname); michael@0: } michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: if (ok) { michael@0: for (let [sgroup, sname, ] in prefs) { michael@0: this._cps._notifyPrefRemoved(sgroup, sname); michael@0: } michael@0: } michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: removeAllDomains: function CPS2_removeAllDomains(context, callback) { michael@0: checkCallbackArg(callback, false); michael@0: michael@0: // Invalidate the cached values so consumers accessing the cache between now michael@0: // and when the operation finishes don't get old data. michael@0: this._cache.removeAllGroups(); michael@0: michael@0: let stmts = []; michael@0: michael@0: // First get the matching prefs. michael@0: stmts.push(this._stmt( michael@0: "SELECT groups.name AS grp, settings.name AS name", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "JOIN groups ON groups.id = prefs.groupID" michael@0: )); michael@0: michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM prefs WHERE groupID NOTNULL" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM groups" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM settings", michael@0: "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" michael@0: )); michael@0: michael@0: let prefs = new ContentPrefStore(); michael@0: michael@0: this._execStmts(stmts, { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: let name = row.getResultByName("name"); michael@0: prefs.set(grp, name, undefined); michael@0: this._cache.set(grp, name, undefined); michael@0: }, michael@0: onDone: function onDone(reason, ok) { michael@0: if (ok && context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, sname, ] in this._pbStore) { michael@0: prefs.set(sgroup, sname, undefined); michael@0: } michael@0: this._pbStore.removeAllGroups(); michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: if (ok) { michael@0: for (let [sgroup, sname, ] in prefs) { michael@0: this._cps._notifyPrefRemoved(sgroup, sname); michael@0: } michael@0: } michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: removeByName: function CPS2_removeByName(name, context, callback) { michael@0: checkNameArg(name); michael@0: checkCallbackArg(callback, false); michael@0: michael@0: // Invalidate the cached values so consumers accessing the cache between now michael@0: // and when the operation finishes don't get old data. michael@0: for (let [group, sname, ] in this._cache) { michael@0: if (sname == name) michael@0: this._cache.remove(group, name); michael@0: } michael@0: michael@0: let stmts = []; michael@0: michael@0: // First get the matching prefs. Include null if any of those prefs are michael@0: // global. michael@0: let stmt = this._stmt( michael@0: "SELECT groups.name AS grp", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "JOIN groups ON groups.id = prefs.groupID", michael@0: "WHERE settings.name = :name", michael@0: "UNION", michael@0: "SELECT NULL AS grp", michael@0: "WHERE EXISTS (", michael@0: "SELECT prefs.id", michael@0: "FROM prefs", michael@0: "JOIN settings ON settings.id = prefs.settingID", michael@0: "WHERE settings.name = :name AND prefs.groupID IS NULL", michael@0: ")" michael@0: ); michael@0: stmt.params.name = name; michael@0: stmts.push(stmt); michael@0: michael@0: // Delete the target settings. michael@0: stmt = this._stmt( michael@0: "DELETE FROM settings WHERE name = :name" michael@0: ); michael@0: stmt.params.name = name; michael@0: stmts.push(stmt); michael@0: michael@0: // Delete prefs and groups that are no longer used. michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" michael@0: )); michael@0: stmts.push(this._stmt( michael@0: "DELETE FROM groups WHERE id NOT IN (", michael@0: "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", michael@0: ")" michael@0: )); michael@0: michael@0: let prefs = new ContentPrefStore(); michael@0: michael@0: this._execStmts(stmts, { michael@0: onRow: function onRow(row) { michael@0: let grp = row.getResultByName("grp"); michael@0: prefs.set(grp, name, undefined); michael@0: this._cache.set(grp, name, undefined); michael@0: }, michael@0: onDone: function onDone(reason, ok) { michael@0: if (ok && context && context.usePrivateBrowsing) { michael@0: for (let [sgroup, sname, ] in this._pbStore) { michael@0: if (sname === name) { michael@0: prefs.set(sgroup, name, undefined); michael@0: this._pbStore.remove(sgroup, name); michael@0: } michael@0: } michael@0: } michael@0: cbHandleCompletion(callback, reason); michael@0: if (ok) { michael@0: for (let [sgroup, , ] in prefs) { michael@0: this._cps._notifyPrefRemoved(sgroup, name); michael@0: } michael@0: } michael@0: }, michael@0: onError: function onError(nsresult) { michael@0: cbHandleError(callback, nsresult); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: destroy: function CPS2_destroy() { michael@0: for each (let stmt in this._statements) { michael@0: stmt.finalize(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such michael@0: * statement is cached, one is created and cached. michael@0: * michael@0: * @param sql The SQL query string. If more than one string is given, then michael@0: * all are concatenated. The concatenation process inserts michael@0: * spaces where appropriate and removes unnecessary contiguous michael@0: * spaces. Call like _stmt("SELECT *", "FROM foo"). michael@0: * @return The cached, possibly new, statement. michael@0: */ michael@0: _stmt: function CPS2__stmt(sql /*, sql2, sql3, ... */) { michael@0: let sql = joinArgs(arguments); michael@0: if (!this._statements) michael@0: this._statements = {}; michael@0: if (!this._statements[sql]) michael@0: this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql); michael@0: return this._statements[sql]; michael@0: }, michael@0: michael@0: /** michael@0: * Executes some async statements. michael@0: * michael@0: * @param stmts An array of mozIStorageAsyncStatements. michael@0: * @param callbacks An object with the following methods: michael@0: * onRow(row) (optional) michael@0: * Called once for each result row. michael@0: * row: A mozIStorageRow. michael@0: * onDone(reason, reasonOK, didGetRow) (required) michael@0: * Called when done. michael@0: * reason: A nsIContentPrefService2.COMPLETE_* value. michael@0: * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK. michael@0: * didGetRow: True if onRow was ever called. michael@0: * onError(nsresult) (optional) michael@0: * Called on error. michael@0: * nsresult: The error code. michael@0: */ michael@0: _execStmts: function CPS2__execStmts(stmts, callbacks) { michael@0: let self = this; michael@0: let gotRow = false; michael@0: this._cps._dbConnection.executeAsync(stmts, stmts.length, { michael@0: handleResult: function handleResult(results) { michael@0: try { michael@0: let row = null; michael@0: while ((row = results.getNextRow())) { michael@0: gotRow = true; michael@0: if (callbacks.onRow) michael@0: callbacks.onRow.call(self, row); michael@0: } michael@0: } michael@0: catch (err) { michael@0: Cu.reportError(err); michael@0: } michael@0: }, michael@0: handleCompletion: function handleCompletion(reason) { michael@0: try { michael@0: let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED; michael@0: callbacks.onDone.call(self, michael@0: ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK : michael@0: Ci.nsIContentPrefCallback2.COMPLETE_ERROR, michael@0: ok, gotRow); michael@0: } michael@0: catch (err) { michael@0: Cu.reportError(err); michael@0: } michael@0: }, michael@0: handleError: function handleError(error) { michael@0: try { michael@0: if (callbacks.onError) michael@0: callbacks.onError.call(self, Cr.NS_ERROR_FAILURE); michael@0: } michael@0: catch (err) { michael@0: Cu.reportError(err); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Parses the domain (the "group", to use the database's term) from the given michael@0: * string. michael@0: * michael@0: * @param groupStr Assumed to be either a string or falsey. michael@0: * @return If groupStr is a valid URL string, returns the domain of michael@0: * that URL. If groupStr is some other nonempty string, michael@0: * returns groupStr itself. Otherwise returns null. michael@0: */ michael@0: _parseGroup: function CPS2__parseGroup(groupStr) { michael@0: if (!groupStr) michael@0: return null; michael@0: try { michael@0: var groupURI = Services.io.newURI(groupStr, null, null); michael@0: } michael@0: catch (err) { michael@0: return groupStr; michael@0: } michael@0: return this._cps._grouper.group(groupURI); michael@0: }, michael@0: michael@0: _schedule: function CPS2__schedule(fn) { michael@0: Services.tm.mainThread.dispatch(fn.bind(this), michael@0: Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: addObserverForName: function CPS2_addObserverForName(name, observer) { michael@0: this._cps._addObserver(name, observer); michael@0: }, michael@0: michael@0: removeObserverForName: function CPS2_removeObserverForName(name, observer) { michael@0: this._cps._removeObserver(name, observer); michael@0: }, michael@0: michael@0: extractDomain: function CPS2_extractDomain(str) { michael@0: return this._parseGroup(str); michael@0: }, michael@0: michael@0: /** michael@0: * Tests use this as a backchannel by calling it directly. michael@0: * michael@0: * @param subj This value depends on topic. michael@0: * @param topic The backchannel "method" name. michael@0: * @param data This value depends on topic. michael@0: */ michael@0: observe: function CPS2_observe(subj, topic, data) { michael@0: switch (topic) { michael@0: case "test:reset": michael@0: let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); michael@0: this._reset(fn); michael@0: break; michael@0: case "test:db": michael@0: let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); michael@0: obj.value = this._cps._dbConnection; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes all state from the service. Used by tests. michael@0: * michael@0: * @param callback A function that will be called when done. michael@0: */ michael@0: _reset: function CPS2__reset(callback) { michael@0: this._pbStore.removeAll(); michael@0: this._cache.removeAll(); michael@0: michael@0: let cps = this._cps; michael@0: cps._observers = {}; michael@0: cps._genericObservers = []; michael@0: michael@0: let tables = ["prefs", "groups", "settings"]; michael@0: let stmts = tables.map(function (t) this._stmt("DELETE FROM", t), this); michael@0: this._execStmts(stmts, { onDone: function () callback() }); michael@0: }, michael@0: michael@0: QueryInterface: function CPS2_QueryInterface(iid) { michael@0: let supportedIIDs = [ michael@0: Ci.nsIContentPrefService2, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupports, michael@0: ]; michael@0: if (supportedIIDs.some(function (i) iid.equals(i))) michael@0: return this; michael@0: if (iid.equals(Ci.nsIContentPrefService)) michael@0: return this._cps; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: }; michael@0: michael@0: function ContentPref(domain, name, value) { michael@0: this.domain = domain; michael@0: this.name = name; michael@0: this.value = value; michael@0: } michael@0: michael@0: ContentPref.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]), michael@0: }; michael@0: michael@0: function cbHandleResult(callback, pref) { michael@0: safeCallback(callback, "handleResult", [pref]); michael@0: } michael@0: michael@0: function cbHandleCompletion(callback, reason) { michael@0: safeCallback(callback, "handleCompletion", [reason]); michael@0: } michael@0: michael@0: function cbHandleError(callback, nsresult) { michael@0: safeCallback(callback, "handleError", [nsresult]); michael@0: } michael@0: michael@0: function safeCallback(callbackObj, methodName, args) { michael@0: if (!callbackObj || typeof(callbackObj[methodName]) != "function") michael@0: return; michael@0: try { michael@0: callbackObj[methodName].apply(callbackObj, args); michael@0: } michael@0: catch (err) { michael@0: Cu.reportError(err); michael@0: } michael@0: } michael@0: michael@0: function checkGroupArg(group) { michael@0: if (!group || typeof(group) != "string") michael@0: throw invalidArg("domain must be nonempty string."); michael@0: } michael@0: michael@0: function checkNameArg(name) { michael@0: if (!name || typeof(name) != "string") michael@0: throw invalidArg("name must be nonempty string."); michael@0: } michael@0: michael@0: function checkValueArg(value) { michael@0: if (value === undefined) michael@0: throw invalidArg("value must not be undefined."); michael@0: } michael@0: michael@0: function checkCallbackArg(callback, required) { michael@0: if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) michael@0: throw invalidArg("callback must be an nsIContentPrefCallback2."); michael@0: if (!callback && required) michael@0: throw invalidArg("callback must be given."); michael@0: } michael@0: michael@0: function invalidArg(msg) { michael@0: return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: function joinArgs(args) { michael@0: return Array.join(args, " ").trim().replace(/\s{2,}/g, " "); michael@0: }