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/. */
10 } from "resource://gre/modules/ContentPrefUtils.sys.mjs";
12 import { ContentPrefStore } from "resource://gre/modules/ContentPrefStore.sys.mjs";
15 ChromeUtils.defineESModuleGetters(lazy, {
16 Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
19 const CACHE_MAX_GROUP_ENTRIES = 100;
21 const GROUP_CLAUSE = `
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;
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);
50 if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) {
57 const privModeStorage = new ContentPrefStore();
59 function executeStatementsInTransaction(conn, stmts) {
60 return conn.executeTransaction(async () => {
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);
71 function HostnameGrouper_group(aURI) {
75 // Accessing the host property of the URI will throw an exception
76 // if the URI is of a type that doesn't have a host property.
77 // Otherwise, we manually throw an exception if the host is empty,
78 // since the effect is the same (we can't derive a group from it).
82 throw new Error("can't derive group from host; no host in URI");
85 // If we don't have a host, then use the entire URI (minus the query,
86 // reference, and hash, if possible) as the group. This means that URIs
87 // like about:mozilla and about:blank will be considered separate groups,
88 // but at least they'll be grouped somehow.
90 // This also means that each individual file: URL will be considered
91 // its own group. This seems suboptimal, but so does treating the entire
92 // file: URL space as a single group (especially if folks start setting
93 // group-specific capabilities prefs).
95 // XXX Is there something better we can do here?
98 var url = aURI.QueryInterface(Ci.nsIURL);
99 group = aURI.prePath + url.filePath;
108 ContentPrefService2.prototype = {
111 classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
116 Services.obs.removeObserver(this, "profile-before-change");
117 Services.obs.removeObserver(this, "last-pb-context-exited");
119 // Delete references to XPCOM components to make sure we don't leak them
120 // (although we haven't observed leakage in tests). Also delete references
121 // in _observers and _genericObservers to avoid cycles with those that
122 // refer to us and don't remove themselves from those observer pools.
123 delete this._observers;
124 delete this._genericObservers;
127 // in-memory cache and private-browsing stores
130 _pbStore: privModeStorage,
135 if (this._connPromise) {
136 return this._connPromise;
139 return (this._connPromise = (async () => {
142 conn = await this._getConnection();
144 this.log("Failed to establish database connection: " + e);
151 // nsIContentPrefService
153 getByName: function CPS2_getByName(name, context, callback) {
155 checkCallbackArg(callback, true);
157 // Some prefs may be in both the database and the private browsing store.
158 // Notify the caller of such prefs only once, using the values from private
160 let pbPrefs = new ContentPrefStore();
161 if (context && context.usePrivateBrowsing) {
162 for (let [sgroup, sname, val] of this._pbStore) {
164 pbPrefs.set(sgroup, sname, val);
169 let stmt1 = this._stmt(`
170 SELECT groups.name AS grp, prefs.value AS value
172 JOIN settings ON settings.id = prefs.settingID
173 JOIN groups ON groups.id = prefs.groupID
174 WHERE settings.name = :name
176 stmt1.params.name = name;
178 let stmt2 = this._stmt(`
179 SELECT NULL AS grp, prefs.value AS value
181 JOIN settings ON settings.id = prefs.settingID
182 WHERE settings.name = :name AND prefs.groupID ISNULL
184 stmt2.params.name = name;
186 this._execStmts([stmt1, stmt2], {
188 let grp = row.getResultByName("grp");
189 let val = row.getResultByName("value");
190 this._cache.set(grp, name, val);
191 if (!pbPrefs.has(grp, name)) {
192 cbHandleResult(callback, new ContentPref(grp, name, val));
195 onDone: (reason, ok, gotRow) => {
197 for (let [pbGroup, pbName, pbVal] of pbPrefs) {
198 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
201 cbHandleCompletion(callback, reason);
203 onError: nsresult => {
204 cbHandleError(callback, nsresult);
209 getByDomainAndName: function CPS2_getByDomainAndName(
215 checkGroupArg(group);
216 this._get(group, name, false, context, callback);
219 getBySubdomainAndName: function CPS2_getBySubdomainAndName(
225 checkGroupArg(group);
226 this._get(group, name, true, context, callback);
229 getGlobal: function CPS2_getGlobal(name, context, callback) {
230 this._get(null, name, false, context, callback);
233 _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
234 group = this._parseGroup(group);
236 checkCallbackArg(callback, true);
238 // Some prefs may be in both the database and the private browsing store.
239 // Notify the caller of such prefs only once, using the values from private
241 let pbPrefs = new ContentPrefStore();
242 if (context && context.usePrivateBrowsing) {
243 for (let [sgroup, val] of this._pbStore.match(
248 pbPrefs.set(sgroup, name, val);
252 this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
254 let grp = row.getResultByName("grp");
255 let val = row.getResultByName("value");
256 this._cache.set(grp, name, val);
257 if (!pbPrefs.has(group, name)) {
258 cbHandleResult(callback, new ContentPref(grp, name, val));
261 onDone: (reason, ok, gotRow) => {
264 this._cache.set(group, name, undefined);
266 for (let [pbGroup, pbName, pbVal] of pbPrefs) {
267 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
270 cbHandleCompletion(callback, reason);
272 onError: nsresult => {
273 cbHandleError(callback, nsresult);
278 _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
280 ? this._stmtWithGroupClause(
284 SELECT groups.name AS grp, prefs.value AS value
286 JOIN settings ON settings.id = prefs.settingID
287 JOIN groups ON groups.id = prefs.groupID
288 WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
292 SELECT NULL AS grp, prefs.value AS value
294 JOIN settings ON settings.id = prefs.settingID
295 WHERE settings.name = :name AND prefs.groupID ISNULL
297 stmt.params.name = name;
301 _stmtWithGroupClause: function CPS2__stmtWithGroupClause(
306 let stmt = this._stmt(sql, false);
307 stmt.params.group = group;
308 stmt.params.includeSubdomains = includeSubdomains || false;
309 stmt.params.pattern =
310 "%." + (group == null ? null : group.replace(/\/|%|_/g, "/$&"));
314 getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(
319 checkGroupArg(group);
320 let prefs = this._getCached(group, name, false, context);
321 return prefs[0] || null;
324 getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(
329 checkGroupArg(group);
330 return this._getCached(group, name, true, context);
333 getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
334 let prefs = this._getCached(null, name, false, context);
335 return prefs[0] || null;
338 _getCached: function CPS2__getCached(
344 group = this._parseGroup(group);
347 let storesToCheck = [this._cache];
348 if (context && context.usePrivateBrowsing) {
349 storesToCheck.push(this._pbStore);
352 let outStore = new ContentPrefStore();
353 storesToCheck.forEach(function (store) {
354 for (let [sgroup, val] of store.match(group, name, includeSubdomains)) {
355 outStore.set(sgroup, name, val);
360 for (let [sgroup, sname, val] of outStore) {
361 prefs.push(new ContentPref(sgroup, sname, val));
366 set: function CPS2_set(group, name, value, context, callback) {
367 checkGroupArg(group);
368 this._set(group, name, value, context, callback);
371 setGlobal: function CPS2_setGlobal(name, value, context, callback) {
372 this._set(null, name, value, context, callback);
375 _set: function CPS2__set(group, name, value, context, callback) {
376 group = this._parseGroup(group);
378 checkValueArg(value);
379 checkCallbackArg(callback, false);
381 if (context && context.usePrivateBrowsing) {
382 this._pbStore.set(group, name, value);
383 this._schedule(function () {
384 cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
385 this._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
390 // Invalidate the cached value so consumers accessing the cache between now
391 // and when the operation finishes don't get old data.
392 this._cache.remove(group, name);
396 // Create the setting if it doesn't exist.
397 let stmt = this._stmt(`
398 INSERT OR IGNORE INTO settings (id, name)
399 VALUES((SELECT id FROM settings WHERE name = :name), :name)
401 stmt.params.name = name;
404 // Create the group if it doesn't exist.
407 INSERT OR IGNORE INTO groups (id, name)
408 VALUES((SELECT id FROM groups WHERE name = :group), :group)
410 stmt.params.group = group;
414 // Finally create or update the pref.
417 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
421 JOIN groups ON groups.id = prefs.groupID
422 JOIN settings ON settings.id = prefs.settingID
423 WHERE groups.name = :group AND settings.name = :name),
424 (SELECT id FROM groups WHERE name = :group),
425 (SELECT id FROM settings WHERE name = :name),
430 stmt.params.group = group;
433 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
437 JOIN settings ON settings.id = prefs.settingID
438 WHERE prefs.groupID IS NULL AND settings.name = :name),
440 (SELECT id FROM settings WHERE name = :name),
446 stmt.params.name = name;
447 stmt.params.value = value;
448 stmt.params.now = Date.now() / 1000;
451 this._execStmts(stmts, {
452 onDone: (reason, ok) => {
454 this._cache.setWithCast(group, name, value);
456 cbHandleCompletion(callback, reason);
462 context && context.usePrivateBrowsing
466 onError: nsresult => {
467 cbHandleError(callback, nsresult);
472 removeByDomainAndName: function CPS2_removeByDomainAndName(
478 checkGroupArg(group);
479 this._remove(group, name, false, context, callback);
482 removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(
488 checkGroupArg(group);
489 this._remove(group, name, true, context, callback);
492 removeGlobal: function CPS2_removeGlobal(name, context, callback) {
493 this._remove(null, name, false, context, callback);
496 _remove: function CPS2__remove(
503 group = this._parseGroup(group);
505 checkCallbackArg(callback, false);
507 // Invalidate the cached values so consumers accessing the cache between now
508 // and when the operation finishes don't get old data.
509 for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
510 this._cache.remove(sgroup, name);
515 // First get the matching prefs.
516 stmts.push(this._commonGetStmt(group, name, includeSubdomains));
518 // Delete the matching prefs.
519 let stmt = this._stmtWithGroupClause(
524 WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND
526 WHEN 'null' THEN prefs.groupID IS NULL
527 ELSE prefs.groupID IN (${GROUP_CLAUSE})
531 stmt.params.name = name;
534 stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
536 let prefs = new ContentPrefStore();
538 let isPrivate = context && context.usePrivateBrowsing;
539 this._execStmts(stmts, {
541 let grp = row.getResultByName("grp");
542 prefs.set(grp, name, undefined);
543 this._cache.set(grp, name, undefined);
545 onDone: (reason, ok) => {
547 this._cache.set(group, name, undefined);
549 for (let [sgroup] of this._pbStore.match(
554 prefs.set(sgroup, name, undefined);
555 this._pbStore.remove(sgroup, name);
559 cbHandleCompletion(callback, reason);
561 for (let [sgroup, ,] of prefs) {
562 this._notifyPrefRemoved(sgroup, name, isPrivate);
566 onError: nsresult => {
567 cbHandleError(callback, nsresult);
572 // Deletes settings and groups that are no longer used.
573 _settingsAndGroupsCleanupStmts() {
574 // The NOTNULL term in the subquery of the second statment is needed because of
575 // SQLite's weird IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html.
579 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
582 DELETE FROM groups WHERE id NOT IN (
583 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
589 removeByDomain: function CPS2_removeByDomain(group, context, callback) {
590 checkGroupArg(group);
591 this._removeByDomain(group, false, context, callback);
594 removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
595 checkGroupArg(group);
596 this._removeByDomain(group, true, context, callback);
599 removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
600 this._removeByDomain(null, false, context, callback);
603 _removeByDomain: function CPS2__removeByDomain(
609 group = this._parseGroup(group);
610 checkCallbackArg(callback, false);
612 // Invalidate the cached values so consumers accessing the cache between now
613 // and when the operation finishes don't get old data.
614 for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
615 this._cache.removeGroup(sgroup);
620 // First get the matching prefs, then delete groups and prefs that reference
624 this._stmtWithGroupClause(
628 SELECT groups.name AS grp, settings.name AS name
630 JOIN settings ON settings.id = prefs.settingID
631 JOIN groups ON groups.id = prefs.groupID
632 WHERE prefs.groupID IN (${GROUP_CLAUSE})
637 this._stmtWithGroupClause(
640 `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})`
646 WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)
652 SELECT NULL AS grp, settings.name AS name
654 JOIN settings ON settings.id = prefs.settingID
655 WHERE prefs.groupID IS NULL
658 stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL"));
661 // Finally delete settings that are no longer referenced.
665 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
669 let prefs = new ContentPrefStore();
671 let isPrivate = context && context.usePrivateBrowsing;
672 this._execStmts(stmts, {
674 let grp = row.getResultByName("grp");
675 let name = row.getResultByName("name");
676 prefs.set(grp, name, undefined);
677 this._cache.set(grp, name, undefined);
679 onDone: (reason, ok) => {
680 if (ok && isPrivate) {
681 for (let [sgroup, sname] of this._pbStore) {
684 (!includeSubdomains && group == sgroup) ||
685 (includeSubdomains &&
687 this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))
689 prefs.set(sgroup, sname, undefined);
690 this._pbStore.remove(sgroup, sname);
694 cbHandleCompletion(callback, reason);
696 for (let [sgroup, sname] of prefs) {
697 this._notifyPrefRemoved(sgroup, sname, isPrivate);
701 onError: nsresult => {
702 cbHandleError(callback, nsresult);
707 _removeAllDomainsSince: function CPS2__removeAllDomainsSince(
712 checkCallbackArg(callback, false);
716 // Invalidate the cached values so consumers accessing the cache between now
717 // and when the operation finishes don't get old data.
718 // Invalidate all the group cache because we don't know which groups will be removed.
719 this._cache.removeAllGroups();
723 // Get prefs that are about to be removed to notify about their removal.
724 let stmt = this._stmt(`
725 SELECT groups.name AS grp, settings.name AS name
727 JOIN settings ON settings.id = prefs.settingID
728 JOIN groups ON groups.id = prefs.groupID
729 WHERE timestamp >= :since
731 stmt.params.since = since;
734 // Do the actual remove.
736 DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since
738 stmt.params.since = since;
741 // Cleanup no longer used values.
742 stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
744 let prefs = new ContentPrefStore();
745 let isPrivate = context && context.usePrivateBrowsing;
746 this._execStmts(stmts, {
748 let grp = row.getResultByName("grp");
749 let name = row.getResultByName("name");
750 prefs.set(grp, name, undefined);
751 this._cache.set(grp, name, undefined);
753 onDone: (reason, ok) => {
754 // This nukes all the groups in _pbStore since we don't have their timestamp
756 if (ok && isPrivate) {
757 for (let [sgroup, sname] of this._pbStore) {
759 prefs.set(sgroup, sname, undefined);
762 this._pbStore.removeAllGroups();
764 cbHandleCompletion(callback, reason);
766 for (let [sgroup, sname] of prefs) {
767 this._notifyPrefRemoved(sgroup, sname, isPrivate);
771 onError: nsresult => {
772 cbHandleError(callback, nsresult);
777 removeAllDomainsSince: function CPS2_removeAllDomainsSince(
782 this._removeAllDomainsSince(since, context, callback);
785 removeAllDomains: function CPS2_removeAllDomains(context, callback) {
786 this._removeAllDomainsSince(0, context, callback);
789 removeByName: function CPS2_removeByName(name, context, callback) {
791 checkCallbackArg(callback, false);
793 // Invalidate the cached values so consumers accessing the cache between now
794 // and when the operation finishes don't get old data.
795 for (let [group, sname] of this._cache) {
797 this._cache.remove(group, name);
803 // First get the matching prefs. Include null if any of those prefs are
805 let stmt = this._stmt(`
806 SELECT groups.name AS grp
808 JOIN settings ON settings.id = prefs.settingID
809 JOIN groups ON groups.id = prefs.groupID
810 WHERE settings.name = :name
816 JOIN settings ON settings.id = prefs.settingID
817 WHERE settings.name = :name AND prefs.groupID IS NULL
820 stmt.params.name = name;
823 // Delete the target settings.
824 stmt = this._stmt("DELETE FROM settings WHERE name = :name");
825 stmt.params.name = name;
828 // Delete prefs and groups that are no longer used.
831 "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)"
836 DELETE FROM groups WHERE id NOT IN (
837 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
842 let prefs = new ContentPrefStore();
843 let isPrivate = context && context.usePrivateBrowsing;
845 this._execStmts(stmts, {
847 let grp = row.getResultByName("grp");
848 prefs.set(grp, name, undefined);
849 this._cache.set(grp, name, undefined);
851 onDone: (reason, ok) => {
852 if (ok && isPrivate) {
853 for (let [sgroup, sname] of this._pbStore) {
854 if (sname === name) {
855 prefs.set(sgroup, name, undefined);
856 this._pbStore.remove(sgroup, name);
860 cbHandleCompletion(callback, reason);
862 for (let [sgroup, ,] of prefs) {
863 this._notifyPrefRemoved(sgroup, name, isPrivate);
867 onError: nsresult => {
868 cbHandleError(callback, nsresult);
874 * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such
875 * statement is cached, one is created and cached.
877 * @param sql The SQL query string.
878 * @return The cached, possibly new, statement.
880 _stmt: function CPS2__stmt(sql, cachable = true) {
889 * Executes some async statements.
891 * @param stmts An array of mozIStorageAsyncStatements.
892 * @param callbacks An object with the following methods:
893 * onRow(row) (optional)
894 * Called once for each result row.
895 * row: A mozIStorageRow.
896 * onDone(reason, reasonOK, didGetRow) (required)
898 * reason: A nsIContentPrefService2.COMPLETE_* value.
899 * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
900 * didGetRow: True if onRow was ever called.
901 * onError(nsresult) (optional)
903 * nsresult: The error code.
905 _execStmts: async function CPS2__execStmts(stmts, callbacks) {
906 let conn = await this.conn;
910 rows = await executeStatementsInTransaction(conn, stmts);
913 if (callbacks.onError) {
915 callbacks.onError(e);
924 if (rows && callbacks.onRow) {
925 for (let row of rows) {
927 callbacks.onRow(row);
937 ? Ci.nsIContentPrefCallback2.COMPLETE_OK
938 : Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
940 rows && !!rows.length
948 * Parses the domain (the "group", to use the database's term) from the given
951 * @param groupStr Assumed to be either a string or falsey.
952 * @return If groupStr is a valid URL string, returns the domain of
953 * that URL. If groupStr is some other nonempty string,
954 * returns groupStr itself. Otherwise returns null.
955 * The return value is truncated at GROUP_NAME_MAX_LENGTH.
957 _parseGroup: function CPS2__parseGroup(groupStr) {
962 var groupURI = Services.io.newURI(groupStr);
963 groupStr = HostnameGrouper_group(groupURI);
965 return groupStr.substring(
967 Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH - 1
971 _schedule: function CPS2__schedule(fn) {
972 Services.tm.dispatchToMainThread(fn.bind(this));
975 // A hash of arrays of observers, indexed by setting name.
976 _observers: new Map(),
978 // An array of generic observers, which observe all settings.
979 _genericObservers: new Set(),
981 addObserverForName(aName, aObserver) {
984 observers = this._observers.get(aName);
986 observers = new Set();
987 this._observers.set(aName, observers);
990 observers = this._genericObservers;
993 observers.add(aObserver);
996 removeObserverForName(aName, aObserver) {
999 observers = this._observers.get(aName);
1004 observers = this._genericObservers;
1007 observers.delete(aObserver);
1011 * Construct a list of observers to notify about a change to some setting,
1012 * putting setting-specific observers before before generic ones, so observers
1013 * that initialize individual settings (like the page style controller)
1014 * execute before observers that display multiple settings and depend on them
1015 * being initialized first (like the content prefs sidebar).
1017 _getObservers(aName) {
1018 let genericObserverList = Array.from(this._genericObservers);
1020 let observersForName = this._observers.get(aName);
1021 if (observersForName) {
1022 return Array.from(observersForName).concat(genericObserverList);
1025 return genericObserverList;
1029 * Notify all observers about the removal of a preference.
1031 _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(
1036 for (var observer of this._getObservers(aName)) {
1038 observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
1046 * Notify all observers about a preference change.
1048 _notifyPrefSet: function ContentPrefService__notifyPrefSet(
1054 for (var observer of this._getObservers(aName)) {
1056 observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
1063 extractDomain: function CPS2_extractDomain(str) {
1064 return this._parseGroup(str);
1068 * Tests use this as a backchannel by calling it directly.
1070 * @param subj This value depends on topic.
1071 * @param topic The backchannel "method" name.
1072 * @param data This value depends on topic.
1074 observe: function CPS2_observe(subj, topic, data) {
1076 case "profile-before-change":
1079 case "last-pb-context-exited":
1080 this._pbStore.removeAll();
1083 let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1087 let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1088 obj.value = this.conn;
1094 * Removes all state from the service. Used by tests.
1096 * @param callback A function that will be called when done.
1098 async _reset(callback) {
1099 this._pbStore.removeAll();
1100 this._cache.removeAll();
1102 this._observers = new Map();
1103 this._genericObservers = new Set();
1105 let tables = ["prefs", "groups", "settings"];
1106 let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
1107 this._execStmts(stmts, {
1114 QueryInterface: ChromeUtils.generateQI([
1115 "nsIContentPrefService2",
1119 // Database Creation & Access
1126 "id INTEGER PRIMARY KEY, \
1127 name TEXT NOT NULL",
1130 "id INTEGER PRIMARY KEY, \
1131 name TEXT NOT NULL",
1134 "id INTEGER PRIMARY KEY, \
1135 groupID INTEGER REFERENCES groups(id), \
1136 settingID INTEGER NOT NULL REFERENCES settings(id), \
1138 timestamp INTEGER NOT NULL DEFAULT 0", // Storage in seconds, API in ms. 0 for migrated values.
1151 columns: ["timestamp", "groupID", "settingID"],
1158 log: function CPS2_log(aMessage) {
1159 if (this._debugLog) {
1160 Services.console.logStringMessage("ContentPrefService2: " + aMessage);
1164 async _getConnection(aAttemptNum = 0) {
1166 Services.startup.isInOrBeyondShutdownPhase(
1167 Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWN
1170 throw new Error("Can't open content prefs, we're in shutdown.");
1172 let path = PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite");
1174 let resetAndRetry = async e => {
1175 if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
1179 if (aAttemptNum >= this.MAX_ATTEMPTS) {
1183 this.log("Establishing connection failed too many times. Giving up.");
1188 await this._failover(conn, path);
1193 return this._getConnection(++aAttemptNum);
1196 conn = await lazy.Sqlite.openConnection({
1198 incrementalVacuum: true,
1202 lazy.Sqlite.shutdown.addBlocker(
1203 "Closing ContentPrefService2 connection.",
1207 // Uh oh, we failed to add a shutdown blocker. Close the connection
1208 // anyway, but make sure that doesn't throw.
1210 await conn?.close();
1218 return resetAndRetry(e);
1222 await this._dbMaybeInit(conn);
1225 return resetAndRetry(e);
1228 // Turn off disk synchronization checking to reduce disk churn and speed up
1229 // operations when prefs are changed rapidly (such as when a user repeatedly
1230 // changes the value of the browser zoom setting for a site).
1232 // Note: this could cause database corruption if the OS crashes or machine
1233 // loses power before the data gets written to disk, but this is considered
1234 // a reasonable risk for the not-so-critical data stored in this database.
1235 await conn.execute("PRAGMA synchronous = OFF");
1240 async _failover(aConn, aPath) {
1241 this.log("Cleaning up DB file - close & remove & backup.");
1243 await aConn.close();
1245 let uniquePath = await IOUtils.createUniqueFile(
1246 PathUtils.parent(aPath),
1247 PathUtils.filename(aPath) + ".corrupt",
1250 await IOUtils.copy(aPath, uniquePath);
1251 await IOUtils.remove(aPath);
1252 this.log("Completed DB cleanup.");
1255 _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
1256 let version = parseInt(await aConn.getSchemaVersion(), 10);
1257 this.log("Schema version: " + version);
1260 await this._dbCreateSchema(aConn);
1261 } else if (version != this._dbVersion) {
1262 await this._dbMigrate(aConn, version, this._dbVersion);
1266 _createTable: async function CPS2__createTable(aConn, aName) {
1267 let tSQL = this._dbSchema.tables[aName];
1268 this.log("Creating table " + aName + " with " + tSQL);
1269 await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`);
1272 _createIndex: async function CPS2__createTable(aConn, aName) {
1273 let index = this._dbSchema.indices[aName];
1275 "CREATE INDEX IF NOT EXISTS " +
1280 index.columns.join(", ") +
1282 await aConn.execute(statement);
1285 _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) {
1286 await aConn.executeTransaction(async () => {
1287 this.log("Creating DB -- tables");
1288 for (let name in this._dbSchema.tables) {
1289 await this._createTable(aConn, name);
1292 this.log("Creating DB -- indices");
1293 for (let name in this._dbSchema.indices) {
1294 await this._createIndex(aConn, name);
1297 await aConn.setSchemaVersion(this._dbVersion);
1301 _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
1303 * Migrations should follow the template rules in bug 1074817 comment 3 which are:
1304 * 1. Migration should be incremental and non-breaking.
1305 * 2. It should be idempotent because one can downgrade an upgrade again.
1307 * 1. Decrement schema version so that upgrade runs the migrations again.
1309 await aConn.executeTransaction(async () => {
1310 for (let i = aOldVersion; i < aNewVersion; i++) {
1311 let migrationName = "_dbMigrate" + i + "To" + (i + 1);
1312 if (typeof this[migrationName] != "function") {
1314 "no migrator function from version " +
1320 await this[migrationName](aConn);
1322 await aConn.setSchemaVersion(aNewVersion);
1326 _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) {
1327 await aConn.execute("ALTER TABLE groups RENAME TO groupsOld");
1328 await this._createTable(aConn, "groups");
1329 await aConn.execute(`
1330 INSERT INTO groups (id, name)
1331 SELECT id, name FROM groupsOld
1334 await aConn.execute("DROP TABLE groupers");
1335 await aConn.execute("DROP TABLE groupsOld");
1338 _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
1339 for (let name in this._dbSchema.indices) {
1340 await this._createIndex(aConn, name);
1344 _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
1345 // Add timestamp column if it does not exist yet. This operation is idempotent.
1347 await aConn.execute("SELECT timestamp FROM prefs");
1349 await aConn.execute(
1350 "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"
1354 // To modify prefs_idx drop it and create again.
1355 await aConn.execute("DROP INDEX IF EXISTS prefs_idx");
1356 for (let name in this._dbSchema.indices) {
1357 await this._createIndex(aConn, name);
1361 async _dbMigrate4To5(conn) {
1362 // This is a data migration for browser.download.lastDir. While it may not
1363 // affect all consumers, it's simpler and safer to do it here than elsewhere.
1364 await conn.execute(`
1367 SELECT p.id FROM prefs p
1368 JOIN groups g ON g.id = p.groupID
1369 JOIN settings s ON s.id = p.settingID
1370 WHERE s.name = 'browser.download.lastDir'
1372 (g.name BETWEEN 'data:' AND 'data:' || X'FFFF') OR
1373 (g.name BETWEEN 'file:' AND 'file:' || X'FFFF')
1377 await conn.execute(`
1378 DELETE FROM groups WHERE NOT EXISTS (
1379 SELECT 1 FROM prefs WHERE groupId = groups.id
1382 // Trim group names longer than MAX_GROUP_LENGTH.
1386 SET name = substr(name, 0, :maxlen)
1387 WHERE LENGTH(name) > :maxlen
1390 maxlen: Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH,
1396 function checkGroupArg(group) {
1397 if (!group || typeof group != "string") {
1398 throw invalidArg("domain must be nonempty string.");
1402 function checkNameArg(name) {
1403 if (!name || typeof name != "string") {
1404 throw invalidArg("name must be nonempty string.");
1408 function checkValueArg(value) {
1409 if (value === undefined) {
1410 throw invalidArg("value must not be undefined.");
1414 function checkCallbackArg(callback, required) {
1415 if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) {
1416 throw invalidArg("callback must be an nsIContentPrefCallback2.");
1418 if (!callback && required) {
1419 throw invalidArg("callback must be given.");
1423 function invalidArg(msg) {
1424 return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);