Bug 1880550 - Propagate explicit heights to scrolled table cells as min-heights....
[gecko.git] / toolkit / components / contentprefs / ContentPrefService2.sys.mjs
blob43fa0c4cb6b5aeb1d239b6fc49340d365c17503a
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/. */
5 import {
6   ContentPref,
7   cbHandleCompletion,
8   cbHandleError,
9   cbHandleResult,
10 } from "resource://gre/modules/ContentPrefUtils.sys.mjs";
12 import { ContentPrefStore } from "resource://gre/modules/ContentPrefStore.sys.mjs";
14 const lazy = {};
15 ChromeUtils.defineESModuleGetters(lazy, {
16   Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
17 });
19 const CACHE_MAX_GROUP_ENTRIES = 100;
21 const GROUP_CLAUSE = `
22   SELECT id
23   FROM groups
24   WHERE name = :group OR
25         (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
28 export function ContentPrefService2() {
29   if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
30     return ChromeUtils.importESModule(
31       "resource://gre/modules/ContentPrefServiceChild.sys.mjs"
32     ).ContentPrefServiceChild;
33   }
35   Services.obs.addObserver(this, "last-pb-context-exited");
37   // Observe shutdown so we can shut down the database connection.
38   Services.obs.addObserver(this, "profile-before-change");
41 const cache = new ContentPrefStore();
42 cache.set = function CPS_cache_set(group, name, val) {
43   Object.getPrototypeOf(this).set.apply(this, arguments);
44   let groupCount = this._groups.size;
45   if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
46     // Clean half of the entries
47     for (let [group, name] of this) {
48       this.remove(group, name);
49       groupCount--;
50       if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) {
51         break;
52       }
53     }
54   }
57 const privModeStorage = new ContentPrefStore();
59 function executeStatementsInTransaction(conn, stmts) {
60   return conn.executeTransaction(async () => {
61     let rows = [];
62     for (let { sql, params, cachable } of stmts) {
63       let execute = cachable ? conn.executeCached : conn.execute;
64       let stmtRows = await execute.call(conn, sql, params);
65       rows = rows.concat(stmtRows);
66     }
67     return rows;
68   });
71 /**
72  * Helper function to extract a non-empty group from a URI.
73  * @param {nsIURI} uri The URI to extract from.
74  * @returns {string} a non-empty group.
75  * @throws if a non-empty group cannot be extracted.
76  */
77 function nonEmptyGroupFromURI(uri) {
78   if (uri.schemeIs("blob")) {
79     // blob: URLs are generated internally for a specific browser instance,
80     // thus storing settings for them would be pointless. Though in most cases
81     // it's possible to extract an origin from them and use it as the group.
82     let embeddedURL = new URL(URL.fromURI(uri).origin);
83     if (/^https?:$/.test(embeddedURL.protocol)) {
84       return embeddedURL.host;
85     }
86     if (embeddedURL.origin) {
87       // Keep the protocol if it's not http(s), to avoid mixing up settings
88       // for different protocols, e.g. resource://moz.com and https://moz.com.
89       return embeddedURL.origin;
90     }
91   }
92   if (uri.host) {
93     // Accessing the host property of the URI will throw an exception
94     // if the URI is of a type that doesn't have a host property.
95     // Otherwise, we manually throw an exception if the host is empty,
96     // since the effect is the same (we can't derive a group from it).
97     return uri.host;
98   }
99   // If reach this point, we'd have an empty group.
100   throw new Error(`Can't derive non-empty CPS group from ${uri.spec}`);
103 function HostnameGrouper_group(aURI) {
104   try {
105     return nonEmptyGroupFromURI(aURI);
106   } catch (ex) {
107     // If we don't have a host, then use the entire URI (minus the query,
108     // reference, and hash, if possible) as the group.  This means that URIs
109     // like about:mozilla and about:blank will be considered separate groups,
110     // but at least they'll be grouped somehow.
112     // This also means that each individual file: URL will be considered
113     // its own group.  This seems suboptimal, but so does treating the entire
114     // file: URL space as a single group (especially if folks start setting
115     // group-specific capabilities prefs).
117     // XXX Is there something better we can do here?
119     try {
120       return aURI.prePath + aURI.filePath;
121     } catch (ex) {
122       return aURI.spec;
123     }
124   }
127 ContentPrefService2.prototype = {
128   // XPCOM Plumbing
130   classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
132   // Destruction
134   _destroy() {
135     Services.obs.removeObserver(this, "profile-before-change");
136     Services.obs.removeObserver(this, "last-pb-context-exited");
138     // Delete references to XPCOM components to make sure we don't leak them
139     // (although we haven't observed leakage in tests).  Also delete references
140     // in _observers and _genericObservers to avoid cycles with those that
141     // refer to us and don't remove themselves from those observer pools.
142     delete this._observers;
143     delete this._genericObservers;
144   },
146   // in-memory cache and private-browsing stores
148   _cache: cache,
149   _pbStore: privModeStorage,
151   _connPromise: null,
153   get conn() {
154     if (this._connPromise) {
155       return this._connPromise;
156     }
158     return (this._connPromise = (async () => {
159       let conn;
160       try {
161         conn = await this._getConnection();
162       } catch (e) {
163         this.log("Failed to establish database connection: " + e);
164         throw e;
165       }
166       return conn;
167     })());
168   },
170   // nsIContentPrefService
172   getByName: function CPS2_getByName(name, context, callback) {
173     checkNameArg(name);
174     checkCallbackArg(callback, true);
176     // Some prefs may be in both the database and the private browsing store.
177     // Notify the caller of such prefs only once, using the values from private
178     // browsing.
179     let pbPrefs = new ContentPrefStore();
180     if (context && context.usePrivateBrowsing) {
181       for (let [sgroup, sname, val] of this._pbStore) {
182         if (sname == name) {
183           pbPrefs.set(sgroup, sname, val);
184         }
185       }
186     }
188     let stmt1 = this._stmt(`
189       SELECT groups.name AS grp, prefs.value AS value
190       FROM prefs
191       JOIN settings ON settings.id = prefs.settingID
192       JOIN groups ON groups.id = prefs.groupID
193       WHERE settings.name = :name
194     `);
195     stmt1.params.name = name;
197     let stmt2 = this._stmt(`
198       SELECT NULL AS grp, prefs.value AS value
199       FROM prefs
200       JOIN settings ON settings.id = prefs.settingID
201       WHERE settings.name = :name AND prefs.groupID ISNULL
202     `);
203     stmt2.params.name = name;
205     this._execStmts([stmt1, stmt2], {
206       onRow: row => {
207         let grp = row.getResultByName("grp");
208         let val = row.getResultByName("value");
209         this._cache.set(grp, name, val);
210         if (!pbPrefs.has(grp, name)) {
211           cbHandleResult(callback, new ContentPref(grp, name, val));
212         }
213       },
214       onDone: (reason, ok, gotRow) => {
215         if (ok) {
216           for (let [pbGroup, pbName, pbVal] of pbPrefs) {
217             cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
218           }
219         }
220         cbHandleCompletion(callback, reason);
221       },
222       onError: nsresult => {
223         cbHandleError(callback, nsresult);
224       },
225     });
226   },
228   getByDomainAndName: function CPS2_getByDomainAndName(
229     group,
230     name,
231     context,
232     callback
233   ) {
234     checkGroupArg(group);
235     this._get(group, name, false, context, callback);
236   },
238   getBySubdomainAndName: function CPS2_getBySubdomainAndName(
239     group,
240     name,
241     context,
242     callback
243   ) {
244     checkGroupArg(group);
245     this._get(group, name, true, context, callback);
246   },
248   getGlobal: function CPS2_getGlobal(name, context, callback) {
249     this._get(null, name, false, context, callback);
250   },
252   _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
253     group = this._parseGroup(group);
254     checkNameArg(name);
255     checkCallbackArg(callback, true);
257     // Some prefs may be in both the database and the private browsing store.
258     // Notify the caller of such prefs only once, using the values from private
259     // browsing.
260     let pbPrefs = new ContentPrefStore();
261     if (context && context.usePrivateBrowsing) {
262       for (let [sgroup, val] of this._pbStore.match(
263         group,
264         name,
265         includeSubdomains
266       )) {
267         pbPrefs.set(sgroup, name, val);
268       }
269     }
271     this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
272       onRow: row => {
273         let grp = row.getResultByName("grp");
274         let val = row.getResultByName("value");
275         this._cache.set(grp, name, val);
276         if (!pbPrefs.has(group, name)) {
277           cbHandleResult(callback, new ContentPref(grp, name, val));
278         }
279       },
280       onDone: (reason, ok, gotRow) => {
281         if (ok) {
282           if (!gotRow) {
283             this._cache.set(group, name, undefined);
284           }
285           for (let [pbGroup, pbName, pbVal] of pbPrefs) {
286             cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
287           }
288         }
289         cbHandleCompletion(callback, reason);
290       },
291       onError: nsresult => {
292         cbHandleError(callback, nsresult);
293       },
294     });
295   },
297   _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
298     let stmt = group
299       ? this._stmtWithGroupClause(
300           group,
301           includeSubdomains,
302           `
303         SELECT groups.name AS grp, prefs.value AS value
304         FROM prefs
305         JOIN settings ON settings.id = prefs.settingID
306         JOIN groups ON groups.id = prefs.groupID
307         WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
308       `
309         )
310       : this._stmt(`
311         SELECT NULL AS grp, prefs.value AS value
312         FROM prefs
313         JOIN settings ON settings.id = prefs.settingID
314         WHERE settings.name = :name AND prefs.groupID ISNULL
315       `);
316     stmt.params.name = name;
317     return stmt;
318   },
320   _stmtWithGroupClause: function CPS2__stmtWithGroupClause(
321     group,
322     includeSubdomains,
323     sql
324   ) {
325     let stmt = this._stmt(sql, false);
326     stmt.params.group = group;
327     stmt.params.includeSubdomains = includeSubdomains || false;
328     stmt.params.pattern =
329       "%." + (group == null ? null : group.replace(/\/|%|_/g, "/$&"));
330     return stmt;
331   },
333   getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(
334     group,
335     name,
336     context
337   ) {
338     checkGroupArg(group);
339     let prefs = this._getCached(group, name, false, context);
340     return prefs[0] || null;
341   },
343   getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(
344     group,
345     name,
346     context
347   ) {
348     checkGroupArg(group);
349     return this._getCached(group, name, true, context);
350   },
352   getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
353     let prefs = this._getCached(null, name, false, context);
354     return prefs[0] || null;
355   },
357   _getCached: function CPS2__getCached(
358     group,
359     name,
360     includeSubdomains,
361     context
362   ) {
363     group = this._parseGroup(group);
364     checkNameArg(name);
366     let storesToCheck = [this._cache];
367     if (context && context.usePrivateBrowsing) {
368       storesToCheck.push(this._pbStore);
369     }
371     let outStore = new ContentPrefStore();
372     storesToCheck.forEach(function (store) {
373       for (let [sgroup, val] of store.match(group, name, includeSubdomains)) {
374         outStore.set(sgroup, name, val);
375       }
376     });
378     let prefs = [];
379     for (let [sgroup, sname, val] of outStore) {
380       prefs.push(new ContentPref(sgroup, sname, val));
381     }
382     return prefs;
383   },
385   set: function CPS2_set(group, name, value, context, callback) {
386     checkGroupArg(group);
387     this._set(group, name, value, context, callback);
388   },
390   setGlobal: function CPS2_setGlobal(name, value, context, callback) {
391     this._set(null, name, value, context, callback);
392   },
394   _set: function CPS2__set(group, name, value, context, callback) {
395     group = this._parseGroup(group);
396     checkNameArg(name);
397     checkValueArg(value);
398     checkCallbackArg(callback, false);
400     if (context && context.usePrivateBrowsing) {
401       this._pbStore.set(group, name, value);
402       this._schedule(function () {
403         cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
404         this._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
405       });
406       return;
407     }
409     // Invalidate the cached value so consumers accessing the cache between now
410     // and when the operation finishes don't get old data.
411     this._cache.remove(group, name);
413     let stmts = [];
415     // Create the setting if it doesn't exist.
416     let stmt = this._stmt(`
417       INSERT OR IGNORE INTO settings (id, name)
418       VALUES((SELECT id FROM settings WHERE name = :name), :name)
419     `);
420     stmt.params.name = name;
421     stmts.push(stmt);
423     // Create the group if it doesn't exist.
424     if (group) {
425       stmt = this._stmt(`
426         INSERT OR IGNORE INTO groups (id, name)
427         VALUES((SELECT id FROM groups WHERE name = :group), :group)
428       `);
429       stmt.params.group = group;
430       stmts.push(stmt);
431     }
433     // Finally create or update the pref.
434     if (group) {
435       stmt = this._stmt(`
436         INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
437         VALUES(
438           (SELECT prefs.id
439            FROM prefs
440            JOIN groups ON groups.id = prefs.groupID
441            JOIN settings ON settings.id = prefs.settingID
442            WHERE groups.name = :group AND settings.name = :name),
443           (SELECT id FROM groups WHERE name = :group),
444           (SELECT id FROM settings WHERE name = :name),
445           :value,
446           :now
447         )
448       `);
449       stmt.params.group = group;
450     } else {
451       stmt = this._stmt(`
452         INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
453         VALUES(
454           (SELECT prefs.id
455            FROM prefs
456            JOIN settings ON settings.id = prefs.settingID
457            WHERE prefs.groupID IS NULL AND settings.name = :name),
458           NULL,
459           (SELECT id FROM settings WHERE name = :name),
460           :value,
461           :now
462         )
463       `);
464     }
465     stmt.params.name = name;
466     stmt.params.value = value;
467     stmt.params.now = Date.now() / 1000;
468     stmts.push(stmt);
470     this._execStmts(stmts, {
471       onDone: (reason, ok) => {
472         if (ok) {
473           this._cache.setWithCast(group, name, value);
474         }
475         cbHandleCompletion(callback, reason);
476         if (ok) {
477           this._notifyPrefSet(
478             group,
479             name,
480             value,
481             context && context.usePrivateBrowsing
482           );
483         }
484       },
485       onError: nsresult => {
486         cbHandleError(callback, nsresult);
487       },
488     });
489   },
491   removeByDomainAndName: function CPS2_removeByDomainAndName(
492     group,
493     name,
494     context,
495     callback
496   ) {
497     checkGroupArg(group);
498     this._remove(group, name, false, context, callback);
499   },
501   removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(
502     group,
503     name,
504     context,
505     callback
506   ) {
507     checkGroupArg(group);
508     this._remove(group, name, true, context, callback);
509   },
511   removeGlobal: function CPS2_removeGlobal(name, context, callback) {
512     this._remove(null, name, false, context, callback);
513   },
515   _remove: function CPS2__remove(
516     group,
517     name,
518     includeSubdomains,
519     context,
520     callback
521   ) {
522     group = this._parseGroup(group);
523     checkNameArg(name);
524     checkCallbackArg(callback, false);
526     // Invalidate the cached values so consumers accessing the cache between now
527     // and when the operation finishes don't get old data.
528     for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
529       this._cache.remove(sgroup, name);
530     }
532     let stmts = [];
534     // First get the matching prefs.
535     stmts.push(this._commonGetStmt(group, name, includeSubdomains));
537     // Delete the matching prefs.
538     let stmt = this._stmtWithGroupClause(
539       group,
540       includeSubdomains,
541       `
542       DELETE FROM prefs
543       WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND
544             CASE typeof(:group)
545             WHEN 'null' THEN prefs.groupID IS NULL
546             ELSE prefs.groupID IN (${GROUP_CLAUSE})
547             END
548     `
549     );
550     stmt.params.name = name;
551     stmts.push(stmt);
553     stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
555     let prefs = new ContentPrefStore();
557     let isPrivate = context && context.usePrivateBrowsing;
558     this._execStmts(stmts, {
559       onRow: row => {
560         let grp = row.getResultByName("grp");
561         prefs.set(grp, name, undefined);
562         this._cache.set(grp, name, undefined);
563       },
564       onDone: (reason, ok) => {
565         if (ok) {
566           this._cache.set(group, name, undefined);
567           if (isPrivate) {
568             for (let [sgroup] of this._pbStore.match(
569               group,
570               name,
571               includeSubdomains
572             )) {
573               prefs.set(sgroup, name, undefined);
574               this._pbStore.remove(sgroup, name);
575             }
576           }
577         }
578         cbHandleCompletion(callback, reason);
579         if (ok) {
580           for (let [sgroup, ,] of prefs) {
581             this._notifyPrefRemoved(sgroup, name, isPrivate);
582           }
583         }
584       },
585       onError: nsresult => {
586         cbHandleError(callback, nsresult);
587       },
588     });
589   },
591   // Deletes settings and groups that are no longer used.
592   _settingsAndGroupsCleanupStmts() {
593     // The NOTNULL term in the subquery of the second statment is needed because of
594     // SQLite's weird IN behavior vis-a-vis NULLs.  See http://sqlite.org/lang_expr.html.
595     return [
596       this._stmt(`
597         DELETE FROM settings
598         WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
599       `),
600       this._stmt(`
601         DELETE FROM groups WHERE id NOT IN (
602           SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
603         )
604       `),
605     ];
606   },
608   removeByDomain: function CPS2_removeByDomain(group, context, callback) {
609     checkGroupArg(group);
610     this._removeByDomain(group, false, context, callback);
611   },
613   removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
614     checkGroupArg(group);
615     this._removeByDomain(group, true, context, callback);
616   },
618   removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
619     this._removeByDomain(null, false, context, callback);
620   },
622   _removeByDomain: function CPS2__removeByDomain(
623     group,
624     includeSubdomains,
625     context,
626     callback
627   ) {
628     group = this._parseGroup(group);
629     checkCallbackArg(callback, false);
631     // Invalidate the cached values so consumers accessing the cache between now
632     // and when the operation finishes don't get old data.
633     for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
634       this._cache.removeGroup(sgroup);
635     }
637     let stmts = [];
639     // First get the matching prefs, then delete groups and prefs that reference
640     // deleted groups.
641     if (group) {
642       stmts.push(
643         this._stmtWithGroupClause(
644           group,
645           includeSubdomains,
646           `
647         SELECT groups.name AS grp, settings.name AS name
648         FROM prefs
649         JOIN settings ON settings.id = prefs.settingID
650         JOIN groups ON groups.id = prefs.groupID
651         WHERE prefs.groupID IN (${GROUP_CLAUSE})
652       `
653         )
654       );
655       stmts.push(
656         this._stmtWithGroupClause(
657           group,
658           includeSubdomains,
659           `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})`
660         )
661       );
662       stmts.push(
663         this._stmt(`
664         DELETE FROM prefs
665         WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)
666       `)
667       );
668     } else {
669       stmts.push(
670         this._stmt(`
671         SELECT NULL AS grp, settings.name AS name
672         FROM prefs
673         JOIN settings ON settings.id = prefs.settingID
674         WHERE prefs.groupID IS NULL
675       `)
676       );
677       stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL"));
678     }
680     // Finally delete settings that are no longer referenced.
681     stmts.push(
682       this._stmt(`
683       DELETE FROM settings
684       WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
685     `)
686     );
688     let prefs = new ContentPrefStore();
690     let isPrivate = context && context.usePrivateBrowsing;
691     this._execStmts(stmts, {
692       onRow: row => {
693         let grp = row.getResultByName("grp");
694         let name = row.getResultByName("name");
695         prefs.set(grp, name, undefined);
696         this._cache.set(grp, name, undefined);
697       },
698       onDone: (reason, ok) => {
699         if (ok && isPrivate) {
700           for (let [sgroup, sname] of this._pbStore) {
701             if (
702               !group ||
703               (!includeSubdomains && group == sgroup) ||
704               (includeSubdomains &&
705                 sgroup &&
706                 this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))
707             ) {
708               prefs.set(sgroup, sname, undefined);
709               this._pbStore.remove(sgroup, sname);
710             }
711           }
712         }
713         cbHandleCompletion(callback, reason);
714         if (ok) {
715           for (let [sgroup, sname] of prefs) {
716             this._notifyPrefRemoved(sgroup, sname, isPrivate);
717           }
718         }
719       },
720       onError: nsresult => {
721         cbHandleError(callback, nsresult);
722       },
723     });
724   },
726   _removeAllDomainsSince: function CPS2__removeAllDomainsSince(
727     since,
728     context,
729     callback
730   ) {
731     checkCallbackArg(callback, false);
733     since /= 1000;
735     // Invalidate the cached values so consumers accessing the cache between now
736     // and when the operation finishes don't get old data.
737     // Invalidate all the group cache because we don't know which groups will be removed.
738     this._cache.removeAllGroups();
740     let stmts = [];
742     // Get prefs that are about to be removed to notify about their removal.
743     let stmt = this._stmt(`
744       SELECT groups.name AS grp, settings.name AS name
745       FROM prefs
746       JOIN settings ON settings.id = prefs.settingID
747       JOIN groups ON groups.id = prefs.groupID
748       WHERE timestamp >= :since
749     `);
750     stmt.params.since = since;
751     stmts.push(stmt);
753     // Do the actual remove.
754     stmt = this._stmt(`
755       DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since
756     `);
757     stmt.params.since = since;
758     stmts.push(stmt);
760     // Cleanup no longer used values.
761     stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
763     let prefs = new ContentPrefStore();
764     let isPrivate = context && context.usePrivateBrowsing;
765     this._execStmts(stmts, {
766       onRow: row => {
767         let grp = row.getResultByName("grp");
768         let name = row.getResultByName("name");
769         prefs.set(grp, name, undefined);
770         this._cache.set(grp, name, undefined);
771       },
772       onDone: (reason, ok) => {
773         // This nukes all the groups in _pbStore since we don't have their timestamp
774         // information.
775         if (ok && isPrivate) {
776           for (let [sgroup, sname] of this._pbStore) {
777             if (sgroup) {
778               prefs.set(sgroup, sname, undefined);
779             }
780           }
781           this._pbStore.removeAllGroups();
782         }
783         cbHandleCompletion(callback, reason);
784         if (ok) {
785           for (let [sgroup, sname] of prefs) {
786             this._notifyPrefRemoved(sgroup, sname, isPrivate);
787           }
788         }
789       },
790       onError: nsresult => {
791         cbHandleError(callback, nsresult);
792       },
793     });
794   },
796   removeAllDomainsSince: function CPS2_removeAllDomainsSince(
797     since,
798     context,
799     callback
800   ) {
801     this._removeAllDomainsSince(since, context, callback);
802   },
804   removeAllDomains: function CPS2_removeAllDomains(context, callback) {
805     this._removeAllDomainsSince(0, context, callback);
806   },
808   removeByName: function CPS2_removeByName(name, context, callback) {
809     checkNameArg(name);
810     checkCallbackArg(callback, false);
812     // Invalidate the cached values so consumers accessing the cache between now
813     // and when the operation finishes don't get old data.
814     for (let [group, sname] of this._cache) {
815       if (sname == name) {
816         this._cache.remove(group, name);
817       }
818     }
820     let stmts = [];
822     // First get the matching prefs.  Include null if any of those prefs are
823     // global.
824     let stmt = this._stmt(`
825       SELECT groups.name AS grp
826       FROM prefs
827       JOIN settings ON settings.id = prefs.settingID
828       JOIN groups ON groups.id = prefs.groupID
829       WHERE settings.name = :name
830       UNION
831       SELECT NULL AS grp
832       WHERE EXISTS (
833         SELECT prefs.id
834         FROM prefs
835         JOIN settings ON settings.id = prefs.settingID
836         WHERE settings.name = :name AND prefs.groupID IS NULL
837       )
838     `);
839     stmt.params.name = name;
840     stmts.push(stmt);
842     // Delete the target settings.
843     stmt = this._stmt("DELETE FROM settings WHERE name = :name");
844     stmt.params.name = name;
845     stmts.push(stmt);
847     // Delete prefs and groups that are no longer used.
848     stmts.push(
849       this._stmt(
850         "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)"
851       )
852     );
853     stmts.push(
854       this._stmt(`
855       DELETE FROM groups WHERE id NOT IN (
856         SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
857       )
858     `)
859     );
861     let prefs = new ContentPrefStore();
862     let isPrivate = context && context.usePrivateBrowsing;
864     this._execStmts(stmts, {
865       onRow: row => {
866         let grp = row.getResultByName("grp");
867         prefs.set(grp, name, undefined);
868         this._cache.set(grp, name, undefined);
869       },
870       onDone: (reason, ok) => {
871         if (ok && isPrivate) {
872           for (let [sgroup, sname] of this._pbStore) {
873             if (sname === name) {
874               prefs.set(sgroup, name, undefined);
875               this._pbStore.remove(sgroup, name);
876             }
877           }
878         }
879         cbHandleCompletion(callback, reason);
880         if (ok) {
881           for (let [sgroup, ,] of prefs) {
882             this._notifyPrefRemoved(sgroup, name, isPrivate);
883           }
884         }
885       },
886       onError: nsresult => {
887         cbHandleError(callback, nsresult);
888       },
889     });
890   },
892   /**
893    * Returns the cached mozIStorageAsyncStatement for the given SQL.  If no such
894    * statement is cached, one is created and cached.
895    *
896    * @param sql  The SQL query string.
897    * @return     The cached, possibly new, statement.
898    */
899   _stmt: function CPS2__stmt(sql, cachable = true) {
900     return {
901       sql,
902       cachable,
903       params: {},
904     };
905   },
907   /**
908    * Executes some async statements.
909    *
910    * @param stmts      An array of mozIStorageAsyncStatements.
911    * @param callbacks  An object with the following methods:
912    *                   onRow(row) (optional)
913    *                     Called once for each result row.
914    *                     row: A mozIStorageRow.
915    *                   onDone(reason, reasonOK, didGetRow) (required)
916    *                     Called when done.
917    *                     reason: A nsIContentPrefService2.COMPLETE_* value.
918    *                     reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
919    *                     didGetRow: True if onRow was ever called.
920    *                   onError(nsresult) (optional)
921    *                     Called on error.
922    *                     nsresult: The error code.
923    */
924   _execStmts: async function CPS2__execStmts(stmts, callbacks) {
925     let conn = await this.conn;
926     let rows;
927     let ok = true;
928     try {
929       rows = await executeStatementsInTransaction(conn, stmts);
930     } catch (e) {
931       ok = false;
932       if (callbacks.onError) {
933         try {
934           callbacks.onError(e);
935         } catch (e) {
936           console.error(e);
937         }
938       } else {
939         console.error(e);
940       }
941     }
943     if (rows && callbacks.onRow) {
944       for (let row of rows) {
945         try {
946           callbacks.onRow(row);
947         } catch (e) {
948           console.error(e);
949         }
950       }
951     }
953     try {
954       callbacks.onDone(
955         ok
956           ? Ci.nsIContentPrefCallback2.COMPLETE_OK
957           : Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
958         ok,
959         rows && !!rows.length
960       );
961     } catch (e) {
962       console.error(e);
963     }
964   },
966   /**
967    * Parses the domain (the "group", to use the database's term) from the given
968    * string.
969    *
970    * @param groupStr  Assumed to be either a string or falsey.
971    * @return          If groupStr is a valid URL string, returns the domain of
972    *                  that URL.  If groupStr is some other nonempty string,
973    *                  returns groupStr itself.  Otherwise returns null.
974    *                  The return value is truncated at GROUP_NAME_MAX_LENGTH.
975    */
976   _parseGroup: function CPS2__parseGroup(groupStr) {
977     if (!groupStr) {
978       return null;
979     }
980     try {
981       var groupURI = Services.io.newURI(groupStr);
982       groupStr = HostnameGrouper_group(groupURI);
983     } catch (err) {}
984     return groupStr.substring(
985       0,
986       Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH - 1
987     );
988   },
990   _schedule: function CPS2__schedule(fn) {
991     Services.tm.dispatchToMainThread(fn.bind(this));
992   },
994   // A hash of arrays of observers, indexed by setting name.
995   _observers: new Map(),
997   // An array of generic observers, which observe all settings.
998   _genericObservers: new Set(),
1000   addObserverForName(aName, aObserver) {
1001     let observers;
1002     if (aName) {
1003       observers = this._observers.get(aName);
1004       if (!observers) {
1005         observers = new Set();
1006         this._observers.set(aName, observers);
1007       }
1008     } else {
1009       observers = this._genericObservers;
1010     }
1012     observers.add(aObserver);
1013   },
1015   removeObserverForName(aName, aObserver) {
1016     let observers;
1017     if (aName) {
1018       observers = this._observers.get(aName);
1019       if (!observers) {
1020         return;
1021       }
1022     } else {
1023       observers = this._genericObservers;
1024     }
1026     observers.delete(aObserver);
1027   },
1029   /**
1030    * Construct a list of observers to notify about a change to some setting,
1031    * putting setting-specific observers before before generic ones, so observers
1032    * that initialize individual settings (like the page style controller)
1033    * execute before observers that display multiple settings and depend on them
1034    * being initialized first (like the content prefs sidebar).
1035    */
1036   _getObservers(aName) {
1037     let genericObserverList = Array.from(this._genericObservers);
1038     if (aName) {
1039       let observersForName = this._observers.get(aName);
1040       if (observersForName) {
1041         return Array.from(observersForName).concat(genericObserverList);
1042       }
1043     }
1044     return genericObserverList;
1045   },
1047   /**
1048    * Notify all observers about the removal of a preference.
1049    */
1050   _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(
1051     aGroup,
1052     aName,
1053     aIsPrivate
1054   ) {
1055     for (var observer of this._getObservers(aName)) {
1056       try {
1057         observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
1058       } catch (ex) {
1059         console.error(ex);
1060       }
1061     }
1062   },
1064   /**
1065    * Notify all observers about a preference change.
1066    */
1067   _notifyPrefSet: function ContentPrefService__notifyPrefSet(
1068     aGroup,
1069     aName,
1070     aValue,
1071     aIsPrivate
1072   ) {
1073     for (var observer of this._getObservers(aName)) {
1074       try {
1075         observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
1076       } catch (ex) {
1077         console.error(ex);
1078       }
1079     }
1080   },
1082   extractDomain: function CPS2_extractDomain(str) {
1083     return this._parseGroup(str);
1084   },
1086   /**
1087    * Tests use this as a backchannel by calling it directly.
1088    *
1089    * @param subj   This value depends on topic.
1090    * @param topic  The backchannel "method" name.
1091    * @param data   This value depends on topic.
1092    */
1093   observe: function CPS2_observe(subj, topic, data) {
1094     switch (topic) {
1095       case "profile-before-change":
1096         this._destroy();
1097         break;
1098       case "last-pb-context-exited":
1099         this._pbStore.removeAll();
1100         break;
1101       case "test:reset":
1102         let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1103         this._reset(fn);
1104         break;
1105       case "test:db":
1106         let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1107         obj.value = this.conn;
1108         break;
1109     }
1110   },
1112   /**
1113    * Removes all state from the service.  Used by tests.
1114    *
1115    * @param callback  A function that will be called when done.
1116    */
1117   async _reset(callback) {
1118     this._pbStore.removeAll();
1119     this._cache.removeAll();
1121     this._observers = new Map();
1122     this._genericObservers = new Set();
1124     let tables = ["prefs", "groups", "settings"];
1125     let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
1126     this._execStmts(stmts, {
1127       onDone: () => {
1128         callback();
1129       },
1130     });
1131   },
1133   QueryInterface: ChromeUtils.generateQI([
1134     "nsIContentPrefService2",
1135     "nsIObserver",
1136   ]),
1138   // Database Creation & Access
1140   _dbVersion: 6,
1142   _dbSchema: {
1143     tables: {
1144       groups:
1145         "id           INTEGER PRIMARY KEY, \
1146                    name         TEXT NOT NULL",
1148       settings:
1149         "id           INTEGER PRIMARY KEY, \
1150                    name         TEXT NOT NULL",
1152       prefs:
1153         "id           INTEGER PRIMARY KEY, \
1154                    groupID      INTEGER REFERENCES groups(id), \
1155                    settingID    INTEGER NOT NULL REFERENCES settings(id), \
1156                    value        BLOB, \
1157                    timestamp    INTEGER NOT NULL DEFAULT 0", // Storage in seconds, API in ms. 0 for migrated values.
1158     },
1159     indices: {
1160       groups_idx: {
1161         table: "groups",
1162         columns: ["name"],
1163       },
1164       settings_idx: {
1165         table: "settings",
1166         columns: ["name"],
1167       },
1168       prefs_idx: {
1169         table: "prefs",
1170         columns: ["timestamp", "groupID", "settingID"],
1171       },
1172     },
1173   },
1175   _debugLog: false,
1177   log: function CPS2_log(aMessage) {
1178     if (this._debugLog) {
1179       Services.console.logStringMessage("ContentPrefService2: " + aMessage);
1180     }
1181   },
1183   async _getConnection(aAttemptNum = 0) {
1184     if (
1185       Services.startup.isInOrBeyondShutdownPhase(
1186         Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWN
1187       )
1188     ) {
1189       throw new Error("Can't open content prefs, we're in shutdown.");
1190     }
1191     let path = PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite");
1192     let conn;
1193     let resetAndRetry = async e => {
1194       if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
1195         throw e;
1196       }
1198       if (aAttemptNum >= this.MAX_ATTEMPTS) {
1199         if (conn) {
1200           await conn.close();
1201         }
1202         this.log("Establishing connection failed too many times. Giving up.");
1203         throw e;
1204       }
1206       try {
1207         await this._failover(conn, path);
1208       } catch (e) {
1209         console.error(e);
1210         throw e;
1211       }
1212       return this._getConnection(++aAttemptNum);
1213     };
1214     try {
1215       conn = await lazy.Sqlite.openConnection({
1216         path,
1217         incrementalVacuum: true,
1218         vacuumOnIdle: true,
1219       });
1220       try {
1221         lazy.Sqlite.shutdown.addBlocker(
1222           "Closing ContentPrefService2 connection.",
1223           () => conn.close()
1224         );
1225       } catch (ex) {
1226         // Uh oh, we failed to add a shutdown blocker. Close the connection
1227         // anyway, but make sure that doesn't throw.
1228         try {
1229           await conn?.close();
1230         } catch (ex) {
1231           console.error(ex);
1232         }
1233         return null;
1234       }
1235     } catch (e) {
1236       console.error(e);
1237       return resetAndRetry(e);
1238     }
1240     try {
1241       await this._dbMaybeInit(conn);
1242     } catch (e) {
1243       console.error(e);
1244       return resetAndRetry(e);
1245     }
1247     // Turn off disk synchronization checking to reduce disk churn and speed up
1248     // operations when prefs are changed rapidly (such as when a user repeatedly
1249     // changes the value of the browser zoom setting for a site).
1250     //
1251     // Note: this could cause database corruption if the OS crashes or machine
1252     // loses power before the data gets written to disk, but this is considered
1253     // a reasonable risk for the not-so-critical data stored in this database.
1254     await conn.execute("PRAGMA synchronous = OFF");
1256     return conn;
1257   },
1259   async _failover(aConn, aPath) {
1260     this.log("Cleaning up DB file - close & remove & backup.");
1261     if (aConn) {
1262       await aConn.close();
1263     }
1264     let uniquePath = await IOUtils.createUniqueFile(
1265       PathUtils.parent(aPath),
1266       PathUtils.filename(aPath) + ".corrupt",
1267       0o600
1268     );
1269     await IOUtils.copy(aPath, uniquePath);
1270     await IOUtils.remove(aPath);
1271     this.log("Completed DB cleanup.");
1272   },
1274   _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
1275     let version = parseInt(await aConn.getSchemaVersion(), 10);
1276     this.log("Schema version: " + version);
1278     if (version == 0) {
1279       await this._dbCreateSchema(aConn);
1280     } else if (version != this._dbVersion) {
1281       await this._dbMigrate(aConn, version, this._dbVersion);
1282     }
1283   },
1285   _createTable: async function CPS2__createTable(aConn, aName) {
1286     let tSQL = this._dbSchema.tables[aName];
1287     this.log("Creating table " + aName + " with " + tSQL);
1288     await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`);
1289   },
1291   _createIndex: async function CPS2__createTable(aConn, aName) {
1292     let index = this._dbSchema.indices[aName];
1293     let statement =
1294       "CREATE INDEX IF NOT EXISTS " +
1295       aName +
1296       " ON " +
1297       index.table +
1298       "(" +
1299       index.columns.join(", ") +
1300       ")";
1301     await aConn.execute(statement);
1302   },
1304   _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) {
1305     await aConn.executeTransaction(async () => {
1306       this.log("Creating DB -- tables");
1307       for (let name in this._dbSchema.tables) {
1308         await this._createTable(aConn, name);
1309       }
1311       this.log("Creating DB -- indices");
1312       for (let name in this._dbSchema.indices) {
1313         await this._createIndex(aConn, name);
1314       }
1316       await aConn.setSchemaVersion(this._dbVersion);
1317     });
1318   },
1320   _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
1321     /**
1322      * Migrations should follow the template rules in bug 1074817 comment 3 which are:
1323      * 1. Migration should be incremental and non-breaking.
1324      * 2. It should be idempotent because one can downgrade an upgrade again.
1325      * On downgrade:
1326      * 1. Decrement schema version so that upgrade runs the migrations again.
1327      */
1328     await aConn.executeTransaction(async () => {
1329       for (let i = aOldVersion; i < aNewVersion; i++) {
1330         let migrationName = "_dbMigrate" + i + "To" + (i + 1);
1331         if (typeof this[migrationName] != "function") {
1332           throw new Error(
1333             "no migrator function from version " +
1334               aOldVersion +
1335               " to version " +
1336               aNewVersion
1337           );
1338         }
1339         await this[migrationName](aConn);
1340       }
1341       await aConn.setSchemaVersion(aNewVersion);
1342     });
1343   },
1345   _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) {
1346     await aConn.execute("ALTER TABLE groups RENAME TO groupsOld");
1347     await this._createTable(aConn, "groups");
1348     await aConn.execute(`
1349       INSERT INTO groups (id, name)
1350       SELECT id, name FROM groupsOld
1351     `);
1353     await aConn.execute("DROP TABLE groupers");
1354     await aConn.execute("DROP TABLE groupsOld");
1355   },
1357   _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
1358     for (let name in this._dbSchema.indices) {
1359       await this._createIndex(aConn, name);
1360     }
1361   },
1363   _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
1364     // Add timestamp column if it does not exist yet. This operation is idempotent.
1365     try {
1366       await aConn.execute("SELECT timestamp FROM prefs");
1367     } catch (e) {
1368       await aConn.execute(
1369         "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"
1370       );
1371     }
1373     // To modify prefs_idx drop it and create again.
1374     await aConn.execute("DROP INDEX IF EXISTS prefs_idx");
1375     for (let name in this._dbSchema.indices) {
1376       await this._createIndex(aConn, name);
1377     }
1378   },
1380   async _dbMigrate4To5(conn) {
1381     // This is a data migration for browser.download.lastDir. While it may not
1382     // affect all consumers, it's simpler and safer to do it here than elsewhere.
1383     await conn.execute(`
1384       DELETE FROM prefs
1385       WHERE id IN (
1386         SELECT p.id FROM prefs p
1387         JOIN groups g ON g.id = p.groupID
1388         JOIN settings s ON s.id = p.settingID
1389         WHERE s.name = 'browser.download.lastDir'
1390           AND (
1391           (g.name BETWEEN 'data:' AND 'data:' || X'FFFF') OR
1392           (g.name BETWEEN 'file:' AND 'file:' || X'FFFF')
1393         )
1394       )
1395     `);
1396     await conn.execute(`
1397       DELETE FROM groups WHERE NOT EXISTS (
1398         SELECT 1 FROM prefs WHERE groupId = groups.id
1399       )
1400     `);
1401     // Trim group names longer than MAX_GROUP_LENGTH.
1402     await conn.execute(
1403       `
1404       UPDATE groups
1405       SET name = substr(name, 0, :maxlen)
1406       WHERE LENGTH(name) > :maxlen
1407       `,
1408       {
1409         maxlen: Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH,
1410       }
1411     );
1412   },
1414   async _dbMigrate5To6(conn) {
1415     // This is a data migration for blob: URIs, as we started storing their
1416     // origin when possible.
1417     // Rather than trying to migrate blob URIs to their origins, that would
1418     // require multiple steps for a tiny benefit (they never worked anyway),
1419     // just remove them, as they are one-time generated URLs unlikely to be
1420     // useful in the future. New inserted blobs will do the right thing.
1421     await conn.execute(`
1422       DELETE FROM prefs
1423       WHERE id IN (
1424         SELECT p.id FROM prefs p
1425         JOIN groups g ON g.id = p.groupID
1426         AND g.name BETWEEN 'blob:' AND 'blob:' || X'FFFF'
1427       )
1428     `);
1429     await conn.execute(`
1430       DELETE FROM groups WHERE NOT EXISTS (
1431         SELECT 1 FROM prefs WHERE groupId = groups.id
1432       )
1433     `);
1434   },
1437 function checkGroupArg(group) {
1438   if (!group || typeof group != "string") {
1439     throw invalidArg("domain must be nonempty string.");
1440   }
1443 function checkNameArg(name) {
1444   if (!name || typeof name != "string") {
1445     throw invalidArg("name must be nonempty string.");
1446   }
1449 function checkValueArg(value) {
1450   if (value === undefined) {
1451     throw invalidArg("value must not be undefined.");
1452   }
1455 function checkCallbackArg(callback, required) {
1456   if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) {
1457     throw invalidArg("callback must be an nsIContentPrefCallback2.");
1458   }
1459   if (!callback && required) {
1460     throw invalidArg("callback must be given.");
1461   }
1464 function invalidArg(msg) {
1465   return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);
1468 // XPCOM Plumbing