1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/contentprefs/ContentPrefService2.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,887 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +// This file is an XPCOM component that implements nsIContentPrefService2. 1.9 +// Although it's a JSM, it's not intended to be imported by consumers like JSMs 1.10 +// are usually imported. It's only a JSM so that nsContentPrefService.js can 1.11 +// easily use it. Consumers should access this component with the usual XPCOM 1.12 +// rigmarole: 1.13 +// 1.14 +// Cc["@mozilla.org/content-pref/service;1"]. 1.15 +// getService(Ci.nsIContentPrefService2); 1.16 +// 1.17 +// That contract ID actually belongs to nsContentPrefService.js, which, when 1.18 +// QI'ed to nsIContentPrefService2, returns an instance of this component. 1.19 +// 1.20 +// The plan is to eventually remove nsIContentPrefService and its 1.21 +// implementation, nsContentPrefService.js. At such time this file can stop 1.22 +// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and 1.23 +// NSGetFactory and all the other XPCOM initialization goop in 1.24 +// nsContentPrefService.js can be moved here. 1.25 +// 1.26 +// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859 1.27 + 1.28 +let EXPORTED_SYMBOLS = [ 1.29 + "ContentPrefService2", 1.30 +]; 1.31 + 1.32 +const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; 1.33 + 1.34 +Cu.import("resource://gre/modules/Services.jsm"); 1.35 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.36 +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); 1.37 + 1.38 +function ContentPrefService2(cps) { 1.39 + this._cps = cps; 1.40 + this._cache = cps._cache; 1.41 + this._pbStore = cps._privModeStorage; 1.42 +} 1.43 + 1.44 +ContentPrefService2.prototype = { 1.45 + 1.46 + getByName: function CPS2_getByName(name, context, callback) { 1.47 + checkNameArg(name); 1.48 + checkCallbackArg(callback, true); 1.49 + 1.50 + // Some prefs may be in both the database and the private browsing store. 1.51 + // Notify the caller of such prefs only once, using the values from private 1.52 + // browsing. 1.53 + let pbPrefs = new ContentPrefStore(); 1.54 + if (context && context.usePrivateBrowsing) { 1.55 + for (let [sgroup, sname, val] in this._pbStore) { 1.56 + if (sname == name) { 1.57 + pbPrefs.set(sgroup, sname, val); 1.58 + } 1.59 + } 1.60 + } 1.61 + 1.62 + let stmt1 = this._stmt( 1.63 + "SELECT groups.name AS grp, prefs.value AS value", 1.64 + "FROM prefs", 1.65 + "JOIN settings ON settings.id = prefs.settingID", 1.66 + "JOIN groups ON groups.id = prefs.groupID", 1.67 + "WHERE settings.name = :name" 1.68 + ); 1.69 + stmt1.params.name = name; 1.70 + 1.71 + let stmt2 = this._stmt( 1.72 + "SELECT NULL AS grp, prefs.value AS value", 1.73 + "FROM prefs", 1.74 + "JOIN settings ON settings.id = prefs.settingID", 1.75 + "WHERE settings.name = :name AND prefs.groupID ISNULL" 1.76 + ); 1.77 + stmt2.params.name = name; 1.78 + 1.79 + this._execStmts([stmt1, stmt2], { 1.80 + onRow: function onRow(row) { 1.81 + let grp = row.getResultByName("grp"); 1.82 + let val = row.getResultByName("value"); 1.83 + this._cache.set(grp, name, val); 1.84 + if (!pbPrefs.has(grp, name)) 1.85 + cbHandleResult(callback, new ContentPref(grp, name, val)); 1.86 + }, 1.87 + onDone: function onDone(reason, ok, gotRow) { 1.88 + if (ok) { 1.89 + for (let [pbGroup, pbName, pbVal] in pbPrefs) { 1.90 + cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); 1.91 + } 1.92 + } 1.93 + cbHandleCompletion(callback, reason); 1.94 + }, 1.95 + onError: function onError(nsresult) { 1.96 + cbHandleError(callback, nsresult); 1.97 + } 1.98 + }); 1.99 + }, 1.100 + 1.101 + getByDomainAndName: function CPS2_getByDomainAndName(group, name, context, 1.102 + callback) { 1.103 + checkGroupArg(group); 1.104 + this._get(group, name, false, context, callback); 1.105 + }, 1.106 + 1.107 + getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name, 1.108 + context, 1.109 + callback) { 1.110 + checkGroupArg(group); 1.111 + this._get(group, name, true, context, callback); 1.112 + }, 1.113 + 1.114 + getGlobal: function CPS2_getGlobal(name, context, callback) { 1.115 + this._get(null, name, false, context, callback); 1.116 + }, 1.117 + 1.118 + _get: function CPS2__get(group, name, includeSubdomains, context, callback) { 1.119 + group = this._parseGroup(group); 1.120 + checkNameArg(name); 1.121 + checkCallbackArg(callback, true); 1.122 + 1.123 + // Some prefs may be in both the database and the private browsing store. 1.124 + // Notify the caller of such prefs only once, using the values from private 1.125 + // browsing. 1.126 + let pbPrefs = new ContentPrefStore(); 1.127 + if (context && context.usePrivateBrowsing) { 1.128 + for (let [sgroup, val] in 1.129 + this._pbStore.match(group, name, includeSubdomains)) { 1.130 + pbPrefs.set(sgroup, name, val); 1.131 + } 1.132 + } 1.133 + 1.134 + this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], { 1.135 + onRow: function onRow(row) { 1.136 + let grp = row.getResultByName("grp"); 1.137 + let val = row.getResultByName("value"); 1.138 + this._cache.set(grp, name, val); 1.139 + if (!pbPrefs.has(group, name)) 1.140 + cbHandleResult(callback, new ContentPref(grp, name, val)); 1.141 + }, 1.142 + onDone: function onDone(reason, ok, gotRow) { 1.143 + if (ok) { 1.144 + if (!gotRow) 1.145 + this._cache.set(group, name, undefined); 1.146 + for (let [pbGroup, pbName, pbVal] in pbPrefs) { 1.147 + cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); 1.148 + } 1.149 + } 1.150 + cbHandleCompletion(callback, reason); 1.151 + }, 1.152 + onError: function onError(nsresult) { 1.153 + cbHandleError(callback, nsresult); 1.154 + } 1.155 + }); 1.156 + }, 1.157 + 1.158 + _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) { 1.159 + let stmt = group ? 1.160 + this._stmtWithGroupClause(group, includeSubdomains, 1.161 + "SELECT groups.name AS grp, prefs.value AS value", 1.162 + "FROM prefs", 1.163 + "JOIN settings ON settings.id = prefs.settingID", 1.164 + "JOIN groups ON groups.id = prefs.groupID", 1.165 + "WHERE settings.name = :name AND prefs.groupID IN ($)" 1.166 + ) : 1.167 + this._stmt( 1.168 + "SELECT NULL AS grp, prefs.value AS value", 1.169 + "FROM prefs", 1.170 + "JOIN settings ON settings.id = prefs.settingID", 1.171 + "WHERE settings.name = :name AND prefs.groupID ISNULL" 1.172 + ); 1.173 + stmt.params.name = name; 1.174 + return stmt; 1.175 + }, 1.176 + 1.177 + _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group, 1.178 + includeSubdomains) { 1.179 + let stmt = this._stmt(joinArgs(Array.slice(arguments, 2)).replace("$", 1.180 + "SELECT id " + 1.181 + "FROM groups " + 1.182 + "WHERE name = :group OR " + 1.183 + "(:includeSubdomains AND name LIKE :pattern ESCAPE '/')" 1.184 + )); 1.185 + stmt.params.group = group; 1.186 + stmt.params.includeSubdomains = includeSubdomains || false; 1.187 + stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/"); 1.188 + return stmt; 1.189 + }, 1.190 + 1.191 + getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group, 1.192 + name, 1.193 + context) { 1.194 + checkGroupArg(group); 1.195 + let prefs = this._getCached(group, name, false, context); 1.196 + return prefs[0] || null; 1.197 + }, 1.198 + 1.199 + getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group, 1.200 + name, 1.201 + context, 1.202 + len) { 1.203 + checkGroupArg(group); 1.204 + let prefs = this._getCached(group, name, true, context); 1.205 + if (len) 1.206 + len.value = prefs.length; 1.207 + return prefs; 1.208 + }, 1.209 + 1.210 + getCachedGlobal: function CPS2_getCachedGlobal(name, context) { 1.211 + let prefs = this._getCached(null, name, false, context); 1.212 + return prefs[0] || null; 1.213 + }, 1.214 + 1.215 + _getCached: function CPS2__getCached(group, name, includeSubdomains, 1.216 + context) { 1.217 + group = this._parseGroup(group); 1.218 + checkNameArg(name); 1.219 + 1.220 + let storesToCheck = [this._cache]; 1.221 + if (context && context.usePrivateBrowsing) 1.222 + storesToCheck.push(this._pbStore); 1.223 + 1.224 + let outStore = new ContentPrefStore(); 1.225 + storesToCheck.forEach(function (store) { 1.226 + for (let [sgroup, val] in store.match(group, name, includeSubdomains)) { 1.227 + outStore.set(sgroup, name, val); 1.228 + } 1.229 + }); 1.230 + 1.231 + let prefs = []; 1.232 + for (let [sgroup, sname, val] in outStore) { 1.233 + prefs.push(new ContentPref(sgroup, sname, val)); 1.234 + } 1.235 + return prefs; 1.236 + }, 1.237 + 1.238 + set: function CPS2_set(group, name, value, context, callback) { 1.239 + checkGroupArg(group); 1.240 + this._set(group, name, value, context, callback); 1.241 + }, 1.242 + 1.243 + setGlobal: function CPS2_setGlobal(name, value, context, callback) { 1.244 + this._set(null, name, value, context, callback); 1.245 + }, 1.246 + 1.247 + _set: function CPS2__set(group, name, value, context, callback) { 1.248 + group = this._parseGroup(group); 1.249 + checkNameArg(name); 1.250 + checkValueArg(value); 1.251 + checkCallbackArg(callback, false); 1.252 + 1.253 + if (context && context.usePrivateBrowsing) { 1.254 + this._pbStore.set(group, name, value); 1.255 + this._schedule(function () { 1.256 + cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK); 1.257 + this._cps._notifyPrefSet(group, name, value); 1.258 + }); 1.259 + return; 1.260 + } 1.261 + 1.262 + // Invalidate the cached value so consumers accessing the cache between now 1.263 + // and when the operation finishes don't get old data. 1.264 + this._cache.remove(group, name); 1.265 + 1.266 + let stmts = []; 1.267 + 1.268 + // Create the setting if it doesn't exist. 1.269 + let stmt = this._stmt( 1.270 + "INSERT OR IGNORE INTO settings (id, name)", 1.271 + "VALUES((SELECT id FROM settings WHERE name = :name), :name)" 1.272 + ); 1.273 + stmt.params.name = name; 1.274 + stmts.push(stmt); 1.275 + 1.276 + // Create the group if it doesn't exist. 1.277 + if (group) { 1.278 + stmt = this._stmt( 1.279 + "INSERT OR IGNORE INTO groups (id, name)", 1.280 + "VALUES((SELECT id FROM groups WHERE name = :group), :group)" 1.281 + ); 1.282 + stmt.params.group = group; 1.283 + stmts.push(stmt); 1.284 + } 1.285 + 1.286 + // Finally create or update the pref. 1.287 + if (group) { 1.288 + stmt = this._stmt( 1.289 + "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", 1.290 + "VALUES(", 1.291 + "(SELECT prefs.id", 1.292 + "FROM prefs", 1.293 + "JOIN groups ON groups.id = prefs.groupID", 1.294 + "JOIN settings ON settings.id = prefs.settingID", 1.295 + "WHERE groups.name = :group AND settings.name = :name),", 1.296 + "(SELECT id FROM groups WHERE name = :group),", 1.297 + "(SELECT id FROM settings WHERE name = :name),", 1.298 + ":value", 1.299 + ")" 1.300 + ); 1.301 + stmt.params.group = group; 1.302 + } 1.303 + else { 1.304 + stmt = this._stmt( 1.305 + "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", 1.306 + "VALUES(", 1.307 + "(SELECT prefs.id", 1.308 + "FROM prefs", 1.309 + "JOIN settings ON settings.id = prefs.settingID", 1.310 + "WHERE prefs.groupID IS NULL AND settings.name = :name),", 1.311 + "NULL,", 1.312 + "(SELECT id FROM settings WHERE name = :name),", 1.313 + ":value", 1.314 + ")" 1.315 + ); 1.316 + } 1.317 + stmt.params.name = name; 1.318 + stmt.params.value = value; 1.319 + stmts.push(stmt); 1.320 + 1.321 + this._execStmts(stmts, { 1.322 + onDone: function onDone(reason, ok) { 1.323 + if (ok) 1.324 + this._cache.setWithCast(group, name, value); 1.325 + cbHandleCompletion(callback, reason); 1.326 + if (ok) 1.327 + this._cps._notifyPrefSet(group, name, value); 1.328 + }, 1.329 + onError: function onError(nsresult) { 1.330 + cbHandleError(callback, nsresult); 1.331 + } 1.332 + }); 1.333 + }, 1.334 + 1.335 + removeByDomainAndName: function CPS2_removeByDomainAndName(group, name, 1.336 + context, 1.337 + callback) { 1.338 + checkGroupArg(group); 1.339 + this._remove(group, name, false, context, callback); 1.340 + }, 1.341 + 1.342 + removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name, 1.343 + context, 1.344 + callback) { 1.345 + checkGroupArg(group); 1.346 + this._remove(group, name, true, context, callback); 1.347 + }, 1.348 + 1.349 + removeGlobal: function CPS2_removeGlobal(name, context,callback) { 1.350 + this._remove(null, name, false, context, callback); 1.351 + }, 1.352 + 1.353 + _remove: function CPS2__remove(group, name, includeSubdomains, context, 1.354 + callback) { 1.355 + group = this._parseGroup(group); 1.356 + checkNameArg(name); 1.357 + checkCallbackArg(callback, false); 1.358 + 1.359 + // Invalidate the cached values so consumers accessing the cache between now 1.360 + // and when the operation finishes don't get old data. 1.361 + for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { 1.362 + this._cache.remove(sgroup, name); 1.363 + } 1.364 + 1.365 + let stmts = []; 1.366 + 1.367 + // First get the matching prefs. 1.368 + stmts.push(this._commonGetStmt(group, name, includeSubdomains)); 1.369 + 1.370 + // Delete the matching prefs. 1.371 + let stmt = this._stmtWithGroupClause(group, includeSubdomains, 1.372 + "DELETE FROM prefs", 1.373 + "WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND", 1.374 + "CASE typeof(:group)", 1.375 + "WHEN 'null' THEN prefs.groupID IS NULL", 1.376 + "ELSE prefs.groupID IN ($)", 1.377 + "END" 1.378 + ); 1.379 + stmt.params.name = name; 1.380 + stmts.push(stmt); 1.381 + 1.382 + // Delete settings and groups that are no longer used. The NOTNULL term in 1.383 + // the subquery of the second statment is needed because of SQLite's weird 1.384 + // IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html. 1.385 + stmts.push(this._stmt( 1.386 + "DELETE FROM settings", 1.387 + "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" 1.388 + )); 1.389 + stmts.push(this._stmt( 1.390 + "DELETE FROM groups WHERE id NOT IN (", 1.391 + "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", 1.392 + ")" 1.393 + )); 1.394 + 1.395 + let prefs = new ContentPrefStore(); 1.396 + 1.397 + this._execStmts(stmts, { 1.398 + onRow: function onRow(row) { 1.399 + let grp = row.getResultByName("grp"); 1.400 + prefs.set(grp, name, undefined); 1.401 + this._cache.set(grp, name, undefined); 1.402 + }, 1.403 + onDone: function onDone(reason, ok) { 1.404 + if (ok) { 1.405 + this._cache.set(group, name, undefined); 1.406 + if (context && context.usePrivateBrowsing) { 1.407 + for (let [sgroup, ] in 1.408 + this._pbStore.match(group, name, includeSubdomains)) { 1.409 + prefs.set(sgroup, name, undefined); 1.410 + this._pbStore.remove(sgroup, name); 1.411 + } 1.412 + } 1.413 + } 1.414 + cbHandleCompletion(callback, reason); 1.415 + if (ok) { 1.416 + for (let [sgroup, , ] in prefs) { 1.417 + this._cps._notifyPrefRemoved(sgroup, name); 1.418 + } 1.419 + } 1.420 + }, 1.421 + onError: function onError(nsresult) { 1.422 + cbHandleError(callback, nsresult); 1.423 + } 1.424 + }); 1.425 + }, 1.426 + 1.427 + removeByDomain: function CPS2_removeByDomain(group, context, callback) { 1.428 + checkGroupArg(group); 1.429 + this._removeByDomain(group, false, context, callback); 1.430 + }, 1.431 + 1.432 + removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) { 1.433 + checkGroupArg(group); 1.434 + this._removeByDomain(group, true, context, callback); 1.435 + }, 1.436 + 1.437 + removeAllGlobals: function CPS2_removeAllGlobals(context, callback) { 1.438 + this._removeByDomain(null, false, context, callback); 1.439 + }, 1.440 + 1.441 + _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains, 1.442 + context, callback) { 1.443 + group = this._parseGroup(group); 1.444 + checkCallbackArg(callback, false); 1.445 + 1.446 + // Invalidate the cached values so consumers accessing the cache between now 1.447 + // and when the operation finishes don't get old data. 1.448 + for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { 1.449 + this._cache.removeGroup(sgroup); 1.450 + } 1.451 + 1.452 + let stmts = []; 1.453 + 1.454 + // First get the matching prefs, then delete groups and prefs that reference 1.455 + // deleted groups. 1.456 + if (group) { 1.457 + stmts.push(this._stmtWithGroupClause(group, includeSubdomains, 1.458 + "SELECT groups.name AS grp, settings.name AS name", 1.459 + "FROM prefs", 1.460 + "JOIN settings ON settings.id = prefs.settingID", 1.461 + "JOIN groups ON groups.id = prefs.groupID", 1.462 + "WHERE prefs.groupID IN ($)" 1.463 + )); 1.464 + stmts.push(this._stmtWithGroupClause(group, includeSubdomains, 1.465 + "DELETE FROM groups WHERE id IN ($)" 1.466 + )); 1.467 + stmts.push(this._stmt( 1.468 + "DELETE FROM prefs", 1.469 + "WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)" 1.470 + )); 1.471 + } 1.472 + else { 1.473 + stmts.push(this._stmt( 1.474 + "SELECT NULL AS grp, settings.name AS name", 1.475 + "FROM prefs", 1.476 + "JOIN settings ON settings.id = prefs.settingID", 1.477 + "WHERE prefs.groupID IS NULL" 1.478 + )); 1.479 + stmts.push(this._stmt( 1.480 + "DELETE FROM prefs WHERE groupID IS NULL" 1.481 + )); 1.482 + } 1.483 + 1.484 + // Finally delete settings that are no longer referenced. 1.485 + stmts.push(this._stmt( 1.486 + "DELETE FROM settings", 1.487 + "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" 1.488 + )); 1.489 + 1.490 + let prefs = new ContentPrefStore(); 1.491 + 1.492 + this._execStmts(stmts, { 1.493 + onRow: function onRow(row) { 1.494 + let grp = row.getResultByName("grp"); 1.495 + let name = row.getResultByName("name"); 1.496 + prefs.set(grp, name, undefined); 1.497 + this._cache.set(grp, name, undefined); 1.498 + }, 1.499 + onDone: function onDone(reason, ok) { 1.500 + if (ok && context && context.usePrivateBrowsing) { 1.501 + for (let [sgroup, sname, ] in this._pbStore) { 1.502 + prefs.set(sgroup, sname, undefined); 1.503 + this._pbStore.remove(sgroup, sname); 1.504 + } 1.505 + } 1.506 + cbHandleCompletion(callback, reason); 1.507 + if (ok) { 1.508 + for (let [sgroup, sname, ] in prefs) { 1.509 + this._cps._notifyPrefRemoved(sgroup, sname); 1.510 + } 1.511 + } 1.512 + }, 1.513 + onError: function onError(nsresult) { 1.514 + cbHandleError(callback, nsresult); 1.515 + } 1.516 + }); 1.517 + }, 1.518 + 1.519 + removeAllDomains: function CPS2_removeAllDomains(context, callback) { 1.520 + checkCallbackArg(callback, false); 1.521 + 1.522 + // Invalidate the cached values so consumers accessing the cache between now 1.523 + // and when the operation finishes don't get old data. 1.524 + this._cache.removeAllGroups(); 1.525 + 1.526 + let stmts = []; 1.527 + 1.528 + // First get the matching prefs. 1.529 + stmts.push(this._stmt( 1.530 + "SELECT groups.name AS grp, settings.name AS name", 1.531 + "FROM prefs", 1.532 + "JOIN settings ON settings.id = prefs.settingID", 1.533 + "JOIN groups ON groups.id = prefs.groupID" 1.534 + )); 1.535 + 1.536 + stmts.push(this._stmt( 1.537 + "DELETE FROM prefs WHERE groupID NOTNULL" 1.538 + )); 1.539 + stmts.push(this._stmt( 1.540 + "DELETE FROM groups" 1.541 + )); 1.542 + stmts.push(this._stmt( 1.543 + "DELETE FROM settings", 1.544 + "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" 1.545 + )); 1.546 + 1.547 + let prefs = new ContentPrefStore(); 1.548 + 1.549 + this._execStmts(stmts, { 1.550 + onRow: function onRow(row) { 1.551 + let grp = row.getResultByName("grp"); 1.552 + let name = row.getResultByName("name"); 1.553 + prefs.set(grp, name, undefined); 1.554 + this._cache.set(grp, name, undefined); 1.555 + }, 1.556 + onDone: function onDone(reason, ok) { 1.557 + if (ok && context && context.usePrivateBrowsing) { 1.558 + for (let [sgroup, sname, ] in this._pbStore) { 1.559 + prefs.set(sgroup, sname, undefined); 1.560 + } 1.561 + this._pbStore.removeAllGroups(); 1.562 + } 1.563 + cbHandleCompletion(callback, reason); 1.564 + if (ok) { 1.565 + for (let [sgroup, sname, ] in prefs) { 1.566 + this._cps._notifyPrefRemoved(sgroup, sname); 1.567 + } 1.568 + } 1.569 + }, 1.570 + onError: function onError(nsresult) { 1.571 + cbHandleError(callback, nsresult); 1.572 + } 1.573 + }); 1.574 + }, 1.575 + 1.576 + removeByName: function CPS2_removeByName(name, context, callback) { 1.577 + checkNameArg(name); 1.578 + checkCallbackArg(callback, false); 1.579 + 1.580 + // Invalidate the cached values so consumers accessing the cache between now 1.581 + // and when the operation finishes don't get old data. 1.582 + for (let [group, sname, ] in this._cache) { 1.583 + if (sname == name) 1.584 + this._cache.remove(group, name); 1.585 + } 1.586 + 1.587 + let stmts = []; 1.588 + 1.589 + // First get the matching prefs. Include null if any of those prefs are 1.590 + // global. 1.591 + let stmt = this._stmt( 1.592 + "SELECT groups.name AS grp", 1.593 + "FROM prefs", 1.594 + "JOIN settings ON settings.id = prefs.settingID", 1.595 + "JOIN groups ON groups.id = prefs.groupID", 1.596 + "WHERE settings.name = :name", 1.597 + "UNION", 1.598 + "SELECT NULL AS grp", 1.599 + "WHERE EXISTS (", 1.600 + "SELECT prefs.id", 1.601 + "FROM prefs", 1.602 + "JOIN settings ON settings.id = prefs.settingID", 1.603 + "WHERE settings.name = :name AND prefs.groupID IS NULL", 1.604 + ")" 1.605 + ); 1.606 + stmt.params.name = name; 1.607 + stmts.push(stmt); 1.608 + 1.609 + // Delete the target settings. 1.610 + stmt = this._stmt( 1.611 + "DELETE FROM settings WHERE name = :name" 1.612 + ); 1.613 + stmt.params.name = name; 1.614 + stmts.push(stmt); 1.615 + 1.616 + // Delete prefs and groups that are no longer used. 1.617 + stmts.push(this._stmt( 1.618 + "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" 1.619 + )); 1.620 + stmts.push(this._stmt( 1.621 + "DELETE FROM groups WHERE id NOT IN (", 1.622 + "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", 1.623 + ")" 1.624 + )); 1.625 + 1.626 + let prefs = new ContentPrefStore(); 1.627 + 1.628 + this._execStmts(stmts, { 1.629 + onRow: function onRow(row) { 1.630 + let grp = row.getResultByName("grp"); 1.631 + prefs.set(grp, name, undefined); 1.632 + this._cache.set(grp, name, undefined); 1.633 + }, 1.634 + onDone: function onDone(reason, ok) { 1.635 + if (ok && context && context.usePrivateBrowsing) { 1.636 + for (let [sgroup, sname, ] in this._pbStore) { 1.637 + if (sname === name) { 1.638 + prefs.set(sgroup, name, undefined); 1.639 + this._pbStore.remove(sgroup, name); 1.640 + } 1.641 + } 1.642 + } 1.643 + cbHandleCompletion(callback, reason); 1.644 + if (ok) { 1.645 + for (let [sgroup, , ] in prefs) { 1.646 + this._cps._notifyPrefRemoved(sgroup, name); 1.647 + } 1.648 + } 1.649 + }, 1.650 + onError: function onError(nsresult) { 1.651 + cbHandleError(callback, nsresult); 1.652 + } 1.653 + }); 1.654 + }, 1.655 + 1.656 + destroy: function CPS2_destroy() { 1.657 + for each (let stmt in this._statements) { 1.658 + stmt.finalize(); 1.659 + } 1.660 + }, 1.661 + 1.662 + /** 1.663 + * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such 1.664 + * statement is cached, one is created and cached. 1.665 + * 1.666 + * @param sql The SQL query string. If more than one string is given, then 1.667 + * all are concatenated. The concatenation process inserts 1.668 + * spaces where appropriate and removes unnecessary contiguous 1.669 + * spaces. Call like _stmt("SELECT *", "FROM foo"). 1.670 + * @return The cached, possibly new, statement. 1.671 + */ 1.672 + _stmt: function CPS2__stmt(sql /*, sql2, sql3, ... */) { 1.673 + let sql = joinArgs(arguments); 1.674 + if (!this._statements) 1.675 + this._statements = {}; 1.676 + if (!this._statements[sql]) 1.677 + this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql); 1.678 + return this._statements[sql]; 1.679 + }, 1.680 + 1.681 + /** 1.682 + * Executes some async statements. 1.683 + * 1.684 + * @param stmts An array of mozIStorageAsyncStatements. 1.685 + * @param callbacks An object with the following methods: 1.686 + * onRow(row) (optional) 1.687 + * Called once for each result row. 1.688 + * row: A mozIStorageRow. 1.689 + * onDone(reason, reasonOK, didGetRow) (required) 1.690 + * Called when done. 1.691 + * reason: A nsIContentPrefService2.COMPLETE_* value. 1.692 + * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK. 1.693 + * didGetRow: True if onRow was ever called. 1.694 + * onError(nsresult) (optional) 1.695 + * Called on error. 1.696 + * nsresult: The error code. 1.697 + */ 1.698 + _execStmts: function CPS2__execStmts(stmts, callbacks) { 1.699 + let self = this; 1.700 + let gotRow = false; 1.701 + this._cps._dbConnection.executeAsync(stmts, stmts.length, { 1.702 + handleResult: function handleResult(results) { 1.703 + try { 1.704 + let row = null; 1.705 + while ((row = results.getNextRow())) { 1.706 + gotRow = true; 1.707 + if (callbacks.onRow) 1.708 + callbacks.onRow.call(self, row); 1.709 + } 1.710 + } 1.711 + catch (err) { 1.712 + Cu.reportError(err); 1.713 + } 1.714 + }, 1.715 + handleCompletion: function handleCompletion(reason) { 1.716 + try { 1.717 + let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED; 1.718 + callbacks.onDone.call(self, 1.719 + ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK : 1.720 + Ci.nsIContentPrefCallback2.COMPLETE_ERROR, 1.721 + ok, gotRow); 1.722 + } 1.723 + catch (err) { 1.724 + Cu.reportError(err); 1.725 + } 1.726 + }, 1.727 + handleError: function handleError(error) { 1.728 + try { 1.729 + if (callbacks.onError) 1.730 + callbacks.onError.call(self, Cr.NS_ERROR_FAILURE); 1.731 + } 1.732 + catch (err) { 1.733 + Cu.reportError(err); 1.734 + } 1.735 + } 1.736 + }); 1.737 + }, 1.738 + 1.739 + /** 1.740 + * Parses the domain (the "group", to use the database's term) from the given 1.741 + * string. 1.742 + * 1.743 + * @param groupStr Assumed to be either a string or falsey. 1.744 + * @return If groupStr is a valid URL string, returns the domain of 1.745 + * that URL. If groupStr is some other nonempty string, 1.746 + * returns groupStr itself. Otherwise returns null. 1.747 + */ 1.748 + _parseGroup: function CPS2__parseGroup(groupStr) { 1.749 + if (!groupStr) 1.750 + return null; 1.751 + try { 1.752 + var groupURI = Services.io.newURI(groupStr, null, null); 1.753 + } 1.754 + catch (err) { 1.755 + return groupStr; 1.756 + } 1.757 + return this._cps._grouper.group(groupURI); 1.758 + }, 1.759 + 1.760 + _schedule: function CPS2__schedule(fn) { 1.761 + Services.tm.mainThread.dispatch(fn.bind(this), 1.762 + Ci.nsIThread.DISPATCH_NORMAL); 1.763 + }, 1.764 + 1.765 + addObserverForName: function CPS2_addObserverForName(name, observer) { 1.766 + this._cps._addObserver(name, observer); 1.767 + }, 1.768 + 1.769 + removeObserverForName: function CPS2_removeObserverForName(name, observer) { 1.770 + this._cps._removeObserver(name, observer); 1.771 + }, 1.772 + 1.773 + extractDomain: function CPS2_extractDomain(str) { 1.774 + return this._parseGroup(str); 1.775 + }, 1.776 + 1.777 + /** 1.778 + * Tests use this as a backchannel by calling it directly. 1.779 + * 1.780 + * @param subj This value depends on topic. 1.781 + * @param topic The backchannel "method" name. 1.782 + * @param data This value depends on topic. 1.783 + */ 1.784 + observe: function CPS2_observe(subj, topic, data) { 1.785 + switch (topic) { 1.786 + case "test:reset": 1.787 + let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); 1.788 + this._reset(fn); 1.789 + break; 1.790 + case "test:db": 1.791 + let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); 1.792 + obj.value = this._cps._dbConnection; 1.793 + break; 1.794 + } 1.795 + }, 1.796 + 1.797 + /** 1.798 + * Removes all state from the service. Used by tests. 1.799 + * 1.800 + * @param callback A function that will be called when done. 1.801 + */ 1.802 + _reset: function CPS2__reset(callback) { 1.803 + this._pbStore.removeAll(); 1.804 + this._cache.removeAll(); 1.805 + 1.806 + let cps = this._cps; 1.807 + cps._observers = {}; 1.808 + cps._genericObservers = []; 1.809 + 1.810 + let tables = ["prefs", "groups", "settings"]; 1.811 + let stmts = tables.map(function (t) this._stmt("DELETE FROM", t), this); 1.812 + this._execStmts(stmts, { onDone: function () callback() }); 1.813 + }, 1.814 + 1.815 + QueryInterface: function CPS2_QueryInterface(iid) { 1.816 + let supportedIIDs = [ 1.817 + Ci.nsIContentPrefService2, 1.818 + Ci.nsIObserver, 1.819 + Ci.nsISupports, 1.820 + ]; 1.821 + if (supportedIIDs.some(function (i) iid.equals(i))) 1.822 + return this; 1.823 + if (iid.equals(Ci.nsIContentPrefService)) 1.824 + return this._cps; 1.825 + throw Cr.NS_ERROR_NO_INTERFACE; 1.826 + }, 1.827 +}; 1.828 + 1.829 +function ContentPref(domain, name, value) { 1.830 + this.domain = domain; 1.831 + this.name = name; 1.832 + this.value = value; 1.833 +} 1.834 + 1.835 +ContentPref.prototype = { 1.836 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]), 1.837 +}; 1.838 + 1.839 +function cbHandleResult(callback, pref) { 1.840 + safeCallback(callback, "handleResult", [pref]); 1.841 +} 1.842 + 1.843 +function cbHandleCompletion(callback, reason) { 1.844 + safeCallback(callback, "handleCompletion", [reason]); 1.845 +} 1.846 + 1.847 +function cbHandleError(callback, nsresult) { 1.848 + safeCallback(callback, "handleError", [nsresult]); 1.849 +} 1.850 + 1.851 +function safeCallback(callbackObj, methodName, args) { 1.852 + if (!callbackObj || typeof(callbackObj[methodName]) != "function") 1.853 + return; 1.854 + try { 1.855 + callbackObj[methodName].apply(callbackObj, args); 1.856 + } 1.857 + catch (err) { 1.858 + Cu.reportError(err); 1.859 + } 1.860 +} 1.861 + 1.862 +function checkGroupArg(group) { 1.863 + if (!group || typeof(group) != "string") 1.864 + throw invalidArg("domain must be nonempty string."); 1.865 +} 1.866 + 1.867 +function checkNameArg(name) { 1.868 + if (!name || typeof(name) != "string") 1.869 + throw invalidArg("name must be nonempty string."); 1.870 +} 1.871 + 1.872 +function checkValueArg(value) { 1.873 + if (value === undefined) 1.874 + throw invalidArg("value must not be undefined."); 1.875 +} 1.876 + 1.877 +function checkCallbackArg(callback, required) { 1.878 + if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) 1.879 + throw invalidArg("callback must be an nsIContentPrefCallback2."); 1.880 + if (!callback && required) 1.881 + throw invalidArg("callback must be given."); 1.882 +} 1.883 + 1.884 +function invalidArg(msg) { 1.885 + return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG); 1.886 +} 1.887 + 1.888 +function joinArgs(args) { 1.889 + return Array.join(args, " ").trim().replace(/\s{2,}/g, " "); 1.890 +}