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