|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 // This file is an XPCOM component that implements nsIContentPrefService2. |
|
6 // Although it's a JSM, it's not intended to be imported by consumers like JSMs |
|
7 // are usually imported. It's only a JSM so that nsContentPrefService.js can |
|
8 // easily use it. Consumers should access this component with the usual XPCOM |
|
9 // rigmarole: |
|
10 // |
|
11 // Cc["@mozilla.org/content-pref/service;1"]. |
|
12 // getService(Ci.nsIContentPrefService2); |
|
13 // |
|
14 // That contract ID actually belongs to nsContentPrefService.js, which, when |
|
15 // QI'ed to nsIContentPrefService2, returns an instance of this component. |
|
16 // |
|
17 // The plan is to eventually remove nsIContentPrefService and its |
|
18 // implementation, nsContentPrefService.js. At such time this file can stop |
|
19 // being a JSM, and the "_cps" parts that ContentPrefService2 relies on and |
|
20 // NSGetFactory and all the other XPCOM initialization goop in |
|
21 // nsContentPrefService.js can be moved here. |
|
22 // |
|
23 // See https://bugzilla.mozilla.org/show_bug.cgi?id=699859 |
|
24 |
|
25 let EXPORTED_SYMBOLS = [ |
|
26 "ContentPrefService2", |
|
27 ]; |
|
28 |
|
29 const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components; |
|
30 |
|
31 Cu.import("resource://gre/modules/Services.jsm"); |
|
32 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
33 Cu.import("resource://gre/modules/ContentPrefStore.jsm"); |
|
34 |
|
35 function ContentPrefService2(cps) { |
|
36 this._cps = cps; |
|
37 this._cache = cps._cache; |
|
38 this._pbStore = cps._privModeStorage; |
|
39 } |
|
40 |
|
41 ContentPrefService2.prototype = { |
|
42 |
|
43 getByName: function CPS2_getByName(name, context, callback) { |
|
44 checkNameArg(name); |
|
45 checkCallbackArg(callback, true); |
|
46 |
|
47 // Some prefs may be in both the database and the private browsing store. |
|
48 // Notify the caller of such prefs only once, using the values from private |
|
49 // browsing. |
|
50 let pbPrefs = new ContentPrefStore(); |
|
51 if (context && context.usePrivateBrowsing) { |
|
52 for (let [sgroup, sname, val] in this._pbStore) { |
|
53 if (sname == name) { |
|
54 pbPrefs.set(sgroup, sname, val); |
|
55 } |
|
56 } |
|
57 } |
|
58 |
|
59 let stmt1 = this._stmt( |
|
60 "SELECT groups.name AS grp, prefs.value AS value", |
|
61 "FROM prefs", |
|
62 "JOIN settings ON settings.id = prefs.settingID", |
|
63 "JOIN groups ON groups.id = prefs.groupID", |
|
64 "WHERE settings.name = :name" |
|
65 ); |
|
66 stmt1.params.name = name; |
|
67 |
|
68 let stmt2 = this._stmt( |
|
69 "SELECT NULL AS grp, prefs.value AS value", |
|
70 "FROM prefs", |
|
71 "JOIN settings ON settings.id = prefs.settingID", |
|
72 "WHERE settings.name = :name AND prefs.groupID ISNULL" |
|
73 ); |
|
74 stmt2.params.name = name; |
|
75 |
|
76 this._execStmts([stmt1, stmt2], { |
|
77 onRow: function onRow(row) { |
|
78 let grp = row.getResultByName("grp"); |
|
79 let val = row.getResultByName("value"); |
|
80 this._cache.set(grp, name, val); |
|
81 if (!pbPrefs.has(grp, name)) |
|
82 cbHandleResult(callback, new ContentPref(grp, name, val)); |
|
83 }, |
|
84 onDone: function onDone(reason, ok, gotRow) { |
|
85 if (ok) { |
|
86 for (let [pbGroup, pbName, pbVal] in pbPrefs) { |
|
87 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); |
|
88 } |
|
89 } |
|
90 cbHandleCompletion(callback, reason); |
|
91 }, |
|
92 onError: function onError(nsresult) { |
|
93 cbHandleError(callback, nsresult); |
|
94 } |
|
95 }); |
|
96 }, |
|
97 |
|
98 getByDomainAndName: function CPS2_getByDomainAndName(group, name, context, |
|
99 callback) { |
|
100 checkGroupArg(group); |
|
101 this._get(group, name, false, context, callback); |
|
102 }, |
|
103 |
|
104 getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name, |
|
105 context, |
|
106 callback) { |
|
107 checkGroupArg(group); |
|
108 this._get(group, name, true, context, callback); |
|
109 }, |
|
110 |
|
111 getGlobal: function CPS2_getGlobal(name, context, callback) { |
|
112 this._get(null, name, false, context, callback); |
|
113 }, |
|
114 |
|
115 _get: function CPS2__get(group, name, includeSubdomains, context, callback) { |
|
116 group = this._parseGroup(group); |
|
117 checkNameArg(name); |
|
118 checkCallbackArg(callback, true); |
|
119 |
|
120 // Some prefs may be in both the database and the private browsing store. |
|
121 // Notify the caller of such prefs only once, using the values from private |
|
122 // browsing. |
|
123 let pbPrefs = new ContentPrefStore(); |
|
124 if (context && context.usePrivateBrowsing) { |
|
125 for (let [sgroup, val] in |
|
126 this._pbStore.match(group, name, includeSubdomains)) { |
|
127 pbPrefs.set(sgroup, name, val); |
|
128 } |
|
129 } |
|
130 |
|
131 this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], { |
|
132 onRow: function onRow(row) { |
|
133 let grp = row.getResultByName("grp"); |
|
134 let val = row.getResultByName("value"); |
|
135 this._cache.set(grp, name, val); |
|
136 if (!pbPrefs.has(group, name)) |
|
137 cbHandleResult(callback, new ContentPref(grp, name, val)); |
|
138 }, |
|
139 onDone: function onDone(reason, ok, gotRow) { |
|
140 if (ok) { |
|
141 if (!gotRow) |
|
142 this._cache.set(group, name, undefined); |
|
143 for (let [pbGroup, pbName, pbVal] in pbPrefs) { |
|
144 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); |
|
145 } |
|
146 } |
|
147 cbHandleCompletion(callback, reason); |
|
148 }, |
|
149 onError: function onError(nsresult) { |
|
150 cbHandleError(callback, nsresult); |
|
151 } |
|
152 }); |
|
153 }, |
|
154 |
|
155 _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) { |
|
156 let stmt = group ? |
|
157 this._stmtWithGroupClause(group, includeSubdomains, |
|
158 "SELECT groups.name AS grp, prefs.value AS value", |
|
159 "FROM prefs", |
|
160 "JOIN settings ON settings.id = prefs.settingID", |
|
161 "JOIN groups ON groups.id = prefs.groupID", |
|
162 "WHERE settings.name = :name AND prefs.groupID IN ($)" |
|
163 ) : |
|
164 this._stmt( |
|
165 "SELECT NULL AS grp, prefs.value AS value", |
|
166 "FROM prefs", |
|
167 "JOIN settings ON settings.id = prefs.settingID", |
|
168 "WHERE settings.name = :name AND prefs.groupID ISNULL" |
|
169 ); |
|
170 stmt.params.name = name; |
|
171 return stmt; |
|
172 }, |
|
173 |
|
174 _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group, |
|
175 includeSubdomains) { |
|
176 let stmt = this._stmt(joinArgs(Array.slice(arguments, 2)).replace("$", |
|
177 "SELECT id " + |
|
178 "FROM groups " + |
|
179 "WHERE name = :group OR " + |
|
180 "(:includeSubdomains AND name LIKE :pattern ESCAPE '/')" |
|
181 )); |
|
182 stmt.params.group = group; |
|
183 stmt.params.includeSubdomains = includeSubdomains || false; |
|
184 stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/"); |
|
185 return stmt; |
|
186 }, |
|
187 |
|
188 getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group, |
|
189 name, |
|
190 context) { |
|
191 checkGroupArg(group); |
|
192 let prefs = this._getCached(group, name, false, context); |
|
193 return prefs[0] || null; |
|
194 }, |
|
195 |
|
196 getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group, |
|
197 name, |
|
198 context, |
|
199 len) { |
|
200 checkGroupArg(group); |
|
201 let prefs = this._getCached(group, name, true, context); |
|
202 if (len) |
|
203 len.value = prefs.length; |
|
204 return prefs; |
|
205 }, |
|
206 |
|
207 getCachedGlobal: function CPS2_getCachedGlobal(name, context) { |
|
208 let prefs = this._getCached(null, name, false, context); |
|
209 return prefs[0] || null; |
|
210 }, |
|
211 |
|
212 _getCached: function CPS2__getCached(group, name, includeSubdomains, |
|
213 context) { |
|
214 group = this._parseGroup(group); |
|
215 checkNameArg(name); |
|
216 |
|
217 let storesToCheck = [this._cache]; |
|
218 if (context && context.usePrivateBrowsing) |
|
219 storesToCheck.push(this._pbStore); |
|
220 |
|
221 let outStore = new ContentPrefStore(); |
|
222 storesToCheck.forEach(function (store) { |
|
223 for (let [sgroup, val] in store.match(group, name, includeSubdomains)) { |
|
224 outStore.set(sgroup, name, val); |
|
225 } |
|
226 }); |
|
227 |
|
228 let prefs = []; |
|
229 for (let [sgroup, sname, val] in outStore) { |
|
230 prefs.push(new ContentPref(sgroup, sname, val)); |
|
231 } |
|
232 return prefs; |
|
233 }, |
|
234 |
|
235 set: function CPS2_set(group, name, value, context, callback) { |
|
236 checkGroupArg(group); |
|
237 this._set(group, name, value, context, callback); |
|
238 }, |
|
239 |
|
240 setGlobal: function CPS2_setGlobal(name, value, context, callback) { |
|
241 this._set(null, name, value, context, callback); |
|
242 }, |
|
243 |
|
244 _set: function CPS2__set(group, name, value, context, callback) { |
|
245 group = this._parseGroup(group); |
|
246 checkNameArg(name); |
|
247 checkValueArg(value); |
|
248 checkCallbackArg(callback, false); |
|
249 |
|
250 if (context && context.usePrivateBrowsing) { |
|
251 this._pbStore.set(group, name, value); |
|
252 this._schedule(function () { |
|
253 cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK); |
|
254 this._cps._notifyPrefSet(group, name, value); |
|
255 }); |
|
256 return; |
|
257 } |
|
258 |
|
259 // Invalidate the cached value so consumers accessing the cache between now |
|
260 // and when the operation finishes don't get old data. |
|
261 this._cache.remove(group, name); |
|
262 |
|
263 let stmts = []; |
|
264 |
|
265 // Create the setting if it doesn't exist. |
|
266 let stmt = this._stmt( |
|
267 "INSERT OR IGNORE INTO settings (id, name)", |
|
268 "VALUES((SELECT id FROM settings WHERE name = :name), :name)" |
|
269 ); |
|
270 stmt.params.name = name; |
|
271 stmts.push(stmt); |
|
272 |
|
273 // Create the group if it doesn't exist. |
|
274 if (group) { |
|
275 stmt = this._stmt( |
|
276 "INSERT OR IGNORE INTO groups (id, name)", |
|
277 "VALUES((SELECT id FROM groups WHERE name = :group), :group)" |
|
278 ); |
|
279 stmt.params.group = group; |
|
280 stmts.push(stmt); |
|
281 } |
|
282 |
|
283 // Finally create or update the pref. |
|
284 if (group) { |
|
285 stmt = this._stmt( |
|
286 "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", |
|
287 "VALUES(", |
|
288 "(SELECT prefs.id", |
|
289 "FROM prefs", |
|
290 "JOIN groups ON groups.id = prefs.groupID", |
|
291 "JOIN settings ON settings.id = prefs.settingID", |
|
292 "WHERE groups.name = :group AND settings.name = :name),", |
|
293 "(SELECT id FROM groups WHERE name = :group),", |
|
294 "(SELECT id FROM settings WHERE name = :name),", |
|
295 ":value", |
|
296 ")" |
|
297 ); |
|
298 stmt.params.group = group; |
|
299 } |
|
300 else { |
|
301 stmt = this._stmt( |
|
302 "INSERT OR REPLACE INTO prefs (id, groupID, settingID, value)", |
|
303 "VALUES(", |
|
304 "(SELECT prefs.id", |
|
305 "FROM prefs", |
|
306 "JOIN settings ON settings.id = prefs.settingID", |
|
307 "WHERE prefs.groupID IS NULL AND settings.name = :name),", |
|
308 "NULL,", |
|
309 "(SELECT id FROM settings WHERE name = :name),", |
|
310 ":value", |
|
311 ")" |
|
312 ); |
|
313 } |
|
314 stmt.params.name = name; |
|
315 stmt.params.value = value; |
|
316 stmts.push(stmt); |
|
317 |
|
318 this._execStmts(stmts, { |
|
319 onDone: function onDone(reason, ok) { |
|
320 if (ok) |
|
321 this._cache.setWithCast(group, name, value); |
|
322 cbHandleCompletion(callback, reason); |
|
323 if (ok) |
|
324 this._cps._notifyPrefSet(group, name, value); |
|
325 }, |
|
326 onError: function onError(nsresult) { |
|
327 cbHandleError(callback, nsresult); |
|
328 } |
|
329 }); |
|
330 }, |
|
331 |
|
332 removeByDomainAndName: function CPS2_removeByDomainAndName(group, name, |
|
333 context, |
|
334 callback) { |
|
335 checkGroupArg(group); |
|
336 this._remove(group, name, false, context, callback); |
|
337 }, |
|
338 |
|
339 removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name, |
|
340 context, |
|
341 callback) { |
|
342 checkGroupArg(group); |
|
343 this._remove(group, name, true, context, callback); |
|
344 }, |
|
345 |
|
346 removeGlobal: function CPS2_removeGlobal(name, context,callback) { |
|
347 this._remove(null, name, false, context, callback); |
|
348 }, |
|
349 |
|
350 _remove: function CPS2__remove(group, name, includeSubdomains, context, |
|
351 callback) { |
|
352 group = this._parseGroup(group); |
|
353 checkNameArg(name); |
|
354 checkCallbackArg(callback, false); |
|
355 |
|
356 // Invalidate the cached values so consumers accessing the cache between now |
|
357 // and when the operation finishes don't get old data. |
|
358 for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { |
|
359 this._cache.remove(sgroup, name); |
|
360 } |
|
361 |
|
362 let stmts = []; |
|
363 |
|
364 // First get the matching prefs. |
|
365 stmts.push(this._commonGetStmt(group, name, includeSubdomains)); |
|
366 |
|
367 // Delete the matching prefs. |
|
368 let stmt = this._stmtWithGroupClause(group, includeSubdomains, |
|
369 "DELETE FROM prefs", |
|
370 "WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND", |
|
371 "CASE typeof(:group)", |
|
372 "WHEN 'null' THEN prefs.groupID IS NULL", |
|
373 "ELSE prefs.groupID IN ($)", |
|
374 "END" |
|
375 ); |
|
376 stmt.params.name = name; |
|
377 stmts.push(stmt); |
|
378 |
|
379 // Delete settings and groups that are no longer used. The NOTNULL term in |
|
380 // the subquery of the second statment is needed because of SQLite's weird |
|
381 // IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html. |
|
382 stmts.push(this._stmt( |
|
383 "DELETE FROM settings", |
|
384 "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" |
|
385 )); |
|
386 stmts.push(this._stmt( |
|
387 "DELETE FROM groups WHERE id NOT IN (", |
|
388 "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", |
|
389 ")" |
|
390 )); |
|
391 |
|
392 let prefs = new ContentPrefStore(); |
|
393 |
|
394 this._execStmts(stmts, { |
|
395 onRow: function onRow(row) { |
|
396 let grp = row.getResultByName("grp"); |
|
397 prefs.set(grp, name, undefined); |
|
398 this._cache.set(grp, name, undefined); |
|
399 }, |
|
400 onDone: function onDone(reason, ok) { |
|
401 if (ok) { |
|
402 this._cache.set(group, name, undefined); |
|
403 if (context && context.usePrivateBrowsing) { |
|
404 for (let [sgroup, ] in |
|
405 this._pbStore.match(group, name, includeSubdomains)) { |
|
406 prefs.set(sgroup, name, undefined); |
|
407 this._pbStore.remove(sgroup, name); |
|
408 } |
|
409 } |
|
410 } |
|
411 cbHandleCompletion(callback, reason); |
|
412 if (ok) { |
|
413 for (let [sgroup, , ] in prefs) { |
|
414 this._cps._notifyPrefRemoved(sgroup, name); |
|
415 } |
|
416 } |
|
417 }, |
|
418 onError: function onError(nsresult) { |
|
419 cbHandleError(callback, nsresult); |
|
420 } |
|
421 }); |
|
422 }, |
|
423 |
|
424 removeByDomain: function CPS2_removeByDomain(group, context, callback) { |
|
425 checkGroupArg(group); |
|
426 this._removeByDomain(group, false, context, callback); |
|
427 }, |
|
428 |
|
429 removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) { |
|
430 checkGroupArg(group); |
|
431 this._removeByDomain(group, true, context, callback); |
|
432 }, |
|
433 |
|
434 removeAllGlobals: function CPS2_removeAllGlobals(context, callback) { |
|
435 this._removeByDomain(null, false, context, callback); |
|
436 }, |
|
437 |
|
438 _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains, |
|
439 context, callback) { |
|
440 group = this._parseGroup(group); |
|
441 checkCallbackArg(callback, false); |
|
442 |
|
443 // Invalidate the cached values so consumers accessing the cache between now |
|
444 // and when the operation finishes don't get old data. |
|
445 for (let sgroup in this._cache.matchGroups(group, includeSubdomains)) { |
|
446 this._cache.removeGroup(sgroup); |
|
447 } |
|
448 |
|
449 let stmts = []; |
|
450 |
|
451 // First get the matching prefs, then delete groups and prefs that reference |
|
452 // deleted groups. |
|
453 if (group) { |
|
454 stmts.push(this._stmtWithGroupClause(group, includeSubdomains, |
|
455 "SELECT groups.name AS grp, settings.name AS name", |
|
456 "FROM prefs", |
|
457 "JOIN settings ON settings.id = prefs.settingID", |
|
458 "JOIN groups ON groups.id = prefs.groupID", |
|
459 "WHERE prefs.groupID IN ($)" |
|
460 )); |
|
461 stmts.push(this._stmtWithGroupClause(group, includeSubdomains, |
|
462 "DELETE FROM groups WHERE id IN ($)" |
|
463 )); |
|
464 stmts.push(this._stmt( |
|
465 "DELETE FROM prefs", |
|
466 "WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)" |
|
467 )); |
|
468 } |
|
469 else { |
|
470 stmts.push(this._stmt( |
|
471 "SELECT NULL AS grp, settings.name AS name", |
|
472 "FROM prefs", |
|
473 "JOIN settings ON settings.id = prefs.settingID", |
|
474 "WHERE prefs.groupID IS NULL" |
|
475 )); |
|
476 stmts.push(this._stmt( |
|
477 "DELETE FROM prefs WHERE groupID IS NULL" |
|
478 )); |
|
479 } |
|
480 |
|
481 // Finally delete settings that are no longer referenced. |
|
482 stmts.push(this._stmt( |
|
483 "DELETE FROM settings", |
|
484 "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" |
|
485 )); |
|
486 |
|
487 let prefs = new ContentPrefStore(); |
|
488 |
|
489 this._execStmts(stmts, { |
|
490 onRow: function onRow(row) { |
|
491 let grp = row.getResultByName("grp"); |
|
492 let name = row.getResultByName("name"); |
|
493 prefs.set(grp, name, undefined); |
|
494 this._cache.set(grp, name, undefined); |
|
495 }, |
|
496 onDone: function onDone(reason, ok) { |
|
497 if (ok && context && context.usePrivateBrowsing) { |
|
498 for (let [sgroup, sname, ] in this._pbStore) { |
|
499 prefs.set(sgroup, sname, undefined); |
|
500 this._pbStore.remove(sgroup, sname); |
|
501 } |
|
502 } |
|
503 cbHandleCompletion(callback, reason); |
|
504 if (ok) { |
|
505 for (let [sgroup, sname, ] in prefs) { |
|
506 this._cps._notifyPrefRemoved(sgroup, sname); |
|
507 } |
|
508 } |
|
509 }, |
|
510 onError: function onError(nsresult) { |
|
511 cbHandleError(callback, nsresult); |
|
512 } |
|
513 }); |
|
514 }, |
|
515 |
|
516 removeAllDomains: function CPS2_removeAllDomains(context, callback) { |
|
517 checkCallbackArg(callback, false); |
|
518 |
|
519 // Invalidate the cached values so consumers accessing the cache between now |
|
520 // and when the operation finishes don't get old data. |
|
521 this._cache.removeAllGroups(); |
|
522 |
|
523 let stmts = []; |
|
524 |
|
525 // First get the matching prefs. |
|
526 stmts.push(this._stmt( |
|
527 "SELECT groups.name AS grp, settings.name AS name", |
|
528 "FROM prefs", |
|
529 "JOIN settings ON settings.id = prefs.settingID", |
|
530 "JOIN groups ON groups.id = prefs.groupID" |
|
531 )); |
|
532 |
|
533 stmts.push(this._stmt( |
|
534 "DELETE FROM prefs WHERE groupID NOTNULL" |
|
535 )); |
|
536 stmts.push(this._stmt( |
|
537 "DELETE FROM groups" |
|
538 )); |
|
539 stmts.push(this._stmt( |
|
540 "DELETE FROM settings", |
|
541 "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)" |
|
542 )); |
|
543 |
|
544 let prefs = new ContentPrefStore(); |
|
545 |
|
546 this._execStmts(stmts, { |
|
547 onRow: function onRow(row) { |
|
548 let grp = row.getResultByName("grp"); |
|
549 let name = row.getResultByName("name"); |
|
550 prefs.set(grp, name, undefined); |
|
551 this._cache.set(grp, name, undefined); |
|
552 }, |
|
553 onDone: function onDone(reason, ok) { |
|
554 if (ok && context && context.usePrivateBrowsing) { |
|
555 for (let [sgroup, sname, ] in this._pbStore) { |
|
556 prefs.set(sgroup, sname, undefined); |
|
557 } |
|
558 this._pbStore.removeAllGroups(); |
|
559 } |
|
560 cbHandleCompletion(callback, reason); |
|
561 if (ok) { |
|
562 for (let [sgroup, sname, ] in prefs) { |
|
563 this._cps._notifyPrefRemoved(sgroup, sname); |
|
564 } |
|
565 } |
|
566 }, |
|
567 onError: function onError(nsresult) { |
|
568 cbHandleError(callback, nsresult); |
|
569 } |
|
570 }); |
|
571 }, |
|
572 |
|
573 removeByName: function CPS2_removeByName(name, context, callback) { |
|
574 checkNameArg(name); |
|
575 checkCallbackArg(callback, false); |
|
576 |
|
577 // Invalidate the cached values so consumers accessing the cache between now |
|
578 // and when the operation finishes don't get old data. |
|
579 for (let [group, sname, ] in this._cache) { |
|
580 if (sname == name) |
|
581 this._cache.remove(group, name); |
|
582 } |
|
583 |
|
584 let stmts = []; |
|
585 |
|
586 // First get the matching prefs. Include null if any of those prefs are |
|
587 // global. |
|
588 let stmt = this._stmt( |
|
589 "SELECT groups.name AS grp", |
|
590 "FROM prefs", |
|
591 "JOIN settings ON settings.id = prefs.settingID", |
|
592 "JOIN groups ON groups.id = prefs.groupID", |
|
593 "WHERE settings.name = :name", |
|
594 "UNION", |
|
595 "SELECT NULL AS grp", |
|
596 "WHERE EXISTS (", |
|
597 "SELECT prefs.id", |
|
598 "FROM prefs", |
|
599 "JOIN settings ON settings.id = prefs.settingID", |
|
600 "WHERE settings.name = :name AND prefs.groupID IS NULL", |
|
601 ")" |
|
602 ); |
|
603 stmt.params.name = name; |
|
604 stmts.push(stmt); |
|
605 |
|
606 // Delete the target settings. |
|
607 stmt = this._stmt( |
|
608 "DELETE FROM settings WHERE name = :name" |
|
609 ); |
|
610 stmt.params.name = name; |
|
611 stmts.push(stmt); |
|
612 |
|
613 // Delete prefs and groups that are no longer used. |
|
614 stmts.push(this._stmt( |
|
615 "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" |
|
616 )); |
|
617 stmts.push(this._stmt( |
|
618 "DELETE FROM groups WHERE id NOT IN (", |
|
619 "SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL", |
|
620 ")" |
|
621 )); |
|
622 |
|
623 let prefs = new ContentPrefStore(); |
|
624 |
|
625 this._execStmts(stmts, { |
|
626 onRow: function onRow(row) { |
|
627 let grp = row.getResultByName("grp"); |
|
628 prefs.set(grp, name, undefined); |
|
629 this._cache.set(grp, name, undefined); |
|
630 }, |
|
631 onDone: function onDone(reason, ok) { |
|
632 if (ok && context && context.usePrivateBrowsing) { |
|
633 for (let [sgroup, sname, ] in this._pbStore) { |
|
634 if (sname === name) { |
|
635 prefs.set(sgroup, name, undefined); |
|
636 this._pbStore.remove(sgroup, name); |
|
637 } |
|
638 } |
|
639 } |
|
640 cbHandleCompletion(callback, reason); |
|
641 if (ok) { |
|
642 for (let [sgroup, , ] in prefs) { |
|
643 this._cps._notifyPrefRemoved(sgroup, name); |
|
644 } |
|
645 } |
|
646 }, |
|
647 onError: function onError(nsresult) { |
|
648 cbHandleError(callback, nsresult); |
|
649 } |
|
650 }); |
|
651 }, |
|
652 |
|
653 destroy: function CPS2_destroy() { |
|
654 for each (let stmt in this._statements) { |
|
655 stmt.finalize(); |
|
656 } |
|
657 }, |
|
658 |
|
659 /** |
|
660 * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such |
|
661 * statement is cached, one is created and cached. |
|
662 * |
|
663 * @param sql The SQL query string. If more than one string is given, then |
|
664 * all are concatenated. The concatenation process inserts |
|
665 * spaces where appropriate and removes unnecessary contiguous |
|
666 * spaces. Call like _stmt("SELECT *", "FROM foo"). |
|
667 * @return The cached, possibly new, statement. |
|
668 */ |
|
669 _stmt: function CPS2__stmt(sql /*, sql2, sql3, ... */) { |
|
670 let sql = joinArgs(arguments); |
|
671 if (!this._statements) |
|
672 this._statements = {}; |
|
673 if (!this._statements[sql]) |
|
674 this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql); |
|
675 return this._statements[sql]; |
|
676 }, |
|
677 |
|
678 /** |
|
679 * Executes some async statements. |
|
680 * |
|
681 * @param stmts An array of mozIStorageAsyncStatements. |
|
682 * @param callbacks An object with the following methods: |
|
683 * onRow(row) (optional) |
|
684 * Called once for each result row. |
|
685 * row: A mozIStorageRow. |
|
686 * onDone(reason, reasonOK, didGetRow) (required) |
|
687 * Called when done. |
|
688 * reason: A nsIContentPrefService2.COMPLETE_* value. |
|
689 * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK. |
|
690 * didGetRow: True if onRow was ever called. |
|
691 * onError(nsresult) (optional) |
|
692 * Called on error. |
|
693 * nsresult: The error code. |
|
694 */ |
|
695 _execStmts: function CPS2__execStmts(stmts, callbacks) { |
|
696 let self = this; |
|
697 let gotRow = false; |
|
698 this._cps._dbConnection.executeAsync(stmts, stmts.length, { |
|
699 handleResult: function handleResult(results) { |
|
700 try { |
|
701 let row = null; |
|
702 while ((row = results.getNextRow())) { |
|
703 gotRow = true; |
|
704 if (callbacks.onRow) |
|
705 callbacks.onRow.call(self, row); |
|
706 } |
|
707 } |
|
708 catch (err) { |
|
709 Cu.reportError(err); |
|
710 } |
|
711 }, |
|
712 handleCompletion: function handleCompletion(reason) { |
|
713 try { |
|
714 let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED; |
|
715 callbacks.onDone.call(self, |
|
716 ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK : |
|
717 Ci.nsIContentPrefCallback2.COMPLETE_ERROR, |
|
718 ok, gotRow); |
|
719 } |
|
720 catch (err) { |
|
721 Cu.reportError(err); |
|
722 } |
|
723 }, |
|
724 handleError: function handleError(error) { |
|
725 try { |
|
726 if (callbacks.onError) |
|
727 callbacks.onError.call(self, Cr.NS_ERROR_FAILURE); |
|
728 } |
|
729 catch (err) { |
|
730 Cu.reportError(err); |
|
731 } |
|
732 } |
|
733 }); |
|
734 }, |
|
735 |
|
736 /** |
|
737 * Parses the domain (the "group", to use the database's term) from the given |
|
738 * string. |
|
739 * |
|
740 * @param groupStr Assumed to be either a string or falsey. |
|
741 * @return If groupStr is a valid URL string, returns the domain of |
|
742 * that URL. If groupStr is some other nonempty string, |
|
743 * returns groupStr itself. Otherwise returns null. |
|
744 */ |
|
745 _parseGroup: function CPS2__parseGroup(groupStr) { |
|
746 if (!groupStr) |
|
747 return null; |
|
748 try { |
|
749 var groupURI = Services.io.newURI(groupStr, null, null); |
|
750 } |
|
751 catch (err) { |
|
752 return groupStr; |
|
753 } |
|
754 return this._cps._grouper.group(groupURI); |
|
755 }, |
|
756 |
|
757 _schedule: function CPS2__schedule(fn) { |
|
758 Services.tm.mainThread.dispatch(fn.bind(this), |
|
759 Ci.nsIThread.DISPATCH_NORMAL); |
|
760 }, |
|
761 |
|
762 addObserverForName: function CPS2_addObserverForName(name, observer) { |
|
763 this._cps._addObserver(name, observer); |
|
764 }, |
|
765 |
|
766 removeObserverForName: function CPS2_removeObserverForName(name, observer) { |
|
767 this._cps._removeObserver(name, observer); |
|
768 }, |
|
769 |
|
770 extractDomain: function CPS2_extractDomain(str) { |
|
771 return this._parseGroup(str); |
|
772 }, |
|
773 |
|
774 /** |
|
775 * Tests use this as a backchannel by calling it directly. |
|
776 * |
|
777 * @param subj This value depends on topic. |
|
778 * @param topic The backchannel "method" name. |
|
779 * @param data This value depends on topic. |
|
780 */ |
|
781 observe: function CPS2_observe(subj, topic, data) { |
|
782 switch (topic) { |
|
783 case "test:reset": |
|
784 let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); |
|
785 this._reset(fn); |
|
786 break; |
|
787 case "test:db": |
|
788 let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); |
|
789 obj.value = this._cps._dbConnection; |
|
790 break; |
|
791 } |
|
792 }, |
|
793 |
|
794 /** |
|
795 * Removes all state from the service. Used by tests. |
|
796 * |
|
797 * @param callback A function that will be called when done. |
|
798 */ |
|
799 _reset: function CPS2__reset(callback) { |
|
800 this._pbStore.removeAll(); |
|
801 this._cache.removeAll(); |
|
802 |
|
803 let cps = this._cps; |
|
804 cps._observers = {}; |
|
805 cps._genericObservers = []; |
|
806 |
|
807 let tables = ["prefs", "groups", "settings"]; |
|
808 let stmts = tables.map(function (t) this._stmt("DELETE FROM", t), this); |
|
809 this._execStmts(stmts, { onDone: function () callback() }); |
|
810 }, |
|
811 |
|
812 QueryInterface: function CPS2_QueryInterface(iid) { |
|
813 let supportedIIDs = [ |
|
814 Ci.nsIContentPrefService2, |
|
815 Ci.nsIObserver, |
|
816 Ci.nsISupports, |
|
817 ]; |
|
818 if (supportedIIDs.some(function (i) iid.equals(i))) |
|
819 return this; |
|
820 if (iid.equals(Ci.nsIContentPrefService)) |
|
821 return this._cps; |
|
822 throw Cr.NS_ERROR_NO_INTERFACE; |
|
823 }, |
|
824 }; |
|
825 |
|
826 function ContentPref(domain, name, value) { |
|
827 this.domain = domain; |
|
828 this.name = name; |
|
829 this.value = value; |
|
830 } |
|
831 |
|
832 ContentPref.prototype = { |
|
833 QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]), |
|
834 }; |
|
835 |
|
836 function cbHandleResult(callback, pref) { |
|
837 safeCallback(callback, "handleResult", [pref]); |
|
838 } |
|
839 |
|
840 function cbHandleCompletion(callback, reason) { |
|
841 safeCallback(callback, "handleCompletion", [reason]); |
|
842 } |
|
843 |
|
844 function cbHandleError(callback, nsresult) { |
|
845 safeCallback(callback, "handleError", [nsresult]); |
|
846 } |
|
847 |
|
848 function safeCallback(callbackObj, methodName, args) { |
|
849 if (!callbackObj || typeof(callbackObj[methodName]) != "function") |
|
850 return; |
|
851 try { |
|
852 callbackObj[methodName].apply(callbackObj, args); |
|
853 } |
|
854 catch (err) { |
|
855 Cu.reportError(err); |
|
856 } |
|
857 } |
|
858 |
|
859 function checkGroupArg(group) { |
|
860 if (!group || typeof(group) != "string") |
|
861 throw invalidArg("domain must be nonempty string."); |
|
862 } |
|
863 |
|
864 function checkNameArg(name) { |
|
865 if (!name || typeof(name) != "string") |
|
866 throw invalidArg("name must be nonempty string."); |
|
867 } |
|
868 |
|
869 function checkValueArg(value) { |
|
870 if (value === undefined) |
|
871 throw invalidArg("value must not be undefined."); |
|
872 } |
|
873 |
|
874 function checkCallbackArg(callback, required) { |
|
875 if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) |
|
876 throw invalidArg("callback must be an nsIContentPrefCallback2."); |
|
877 if (!callback && required) |
|
878 throw invalidArg("callback must be given."); |
|
879 } |
|
880 |
|
881 function invalidArg(msg) { |
|
882 return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG); |
|
883 } |
|
884 |
|
885 function joinArgs(args) { |
|
886 return Array.join(args, " ").trim().replace(/\s{2,}/g, " "); |
|
887 } |