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);
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.
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;
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;
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).
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) {
105 return nonEmptyGroupFromURI(aURI);
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?
120 return aURI.prePath + aURI.filePath;
127 ContentPrefService2.prototype = {
130 classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
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;
146 // in-memory cache and private-browsing stores
149 _pbStore: privModeStorage,
154 if (this._connPromise) {
155 return this._connPromise;
158 return (this._connPromise = (async () => {
161 conn = await this._getConnection();
163 this.log("Failed to establish database connection: " + e);
170 // nsIContentPrefService
172 getByName: function CPS2_getByName(name, context, callback) {
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
179 let pbPrefs = new ContentPrefStore();
180 if (context && context.usePrivateBrowsing) {
181 for (let [sgroup, sname, val] of this._pbStore) {
183 pbPrefs.set(sgroup, sname, val);
188 let stmt1 = this._stmt(`
189 SELECT groups.name AS grp, prefs.value AS value
191 JOIN settings ON settings.id = prefs.settingID
192 JOIN groups ON groups.id = prefs.groupID
193 WHERE settings.name = :name
195 stmt1.params.name = name;
197 let stmt2 = this._stmt(`
198 SELECT NULL AS grp, prefs.value AS value
200 JOIN settings ON settings.id = prefs.settingID
201 WHERE settings.name = :name AND prefs.groupID ISNULL
203 stmt2.params.name = name;
205 this._execStmts([stmt1, stmt2], {
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));
214 onDone: (reason, ok, gotRow) => {
216 for (let [pbGroup, pbName, pbVal] of pbPrefs) {
217 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
220 cbHandleCompletion(callback, reason);
222 onError: nsresult => {
223 cbHandleError(callback, nsresult);
228 getByDomainAndName: function CPS2_getByDomainAndName(
234 checkGroupArg(group);
235 this._get(group, name, false, context, callback);
238 getBySubdomainAndName: function CPS2_getBySubdomainAndName(
244 checkGroupArg(group);
245 this._get(group, name, true, context, callback);
248 getGlobal: function CPS2_getGlobal(name, context, callback) {
249 this._get(null, name, false, context, callback);
252 _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
253 group = this._parseGroup(group);
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
260 let pbPrefs = new ContentPrefStore();
261 if (context && context.usePrivateBrowsing) {
262 for (let [sgroup, val] of this._pbStore.match(
267 pbPrefs.set(sgroup, name, val);
271 this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
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));
280 onDone: (reason, ok, gotRow) => {
283 this._cache.set(group, name, undefined);
285 for (let [pbGroup, pbName, pbVal] of pbPrefs) {
286 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
289 cbHandleCompletion(callback, reason);
291 onError: nsresult => {
292 cbHandleError(callback, nsresult);
297 _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
299 ? this._stmtWithGroupClause(
303 SELECT groups.name AS grp, prefs.value AS value
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})
311 SELECT NULL AS grp, prefs.value AS value
313 JOIN settings ON settings.id = prefs.settingID
314 WHERE settings.name = :name AND prefs.groupID ISNULL
316 stmt.params.name = name;
320 _stmtWithGroupClause: function CPS2__stmtWithGroupClause(
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, "/$&"));
333 getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(
338 checkGroupArg(group);
339 let prefs = this._getCached(group, name, false, context);
340 return prefs[0] || null;
343 getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(
348 checkGroupArg(group);
349 return this._getCached(group, name, true, context);
352 getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
353 let prefs = this._getCached(null, name, false, context);
354 return prefs[0] || null;
357 _getCached: function CPS2__getCached(
363 group = this._parseGroup(group);
366 let storesToCheck = [this._cache];
367 if (context && context.usePrivateBrowsing) {
368 storesToCheck.push(this._pbStore);
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);
379 for (let [sgroup, sname, val] of outStore) {
380 prefs.push(new ContentPref(sgroup, sname, val));
385 set: function CPS2_set(group, name, value, context, callback) {
386 checkGroupArg(group);
387 this._set(group, name, value, context, callback);
390 setGlobal: function CPS2_setGlobal(name, value, context, callback) {
391 this._set(null, name, value, context, callback);
394 _set: function CPS2__set(group, name, value, context, callback) {
395 group = this._parseGroup(group);
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);
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);
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)
420 stmt.params.name = name;
423 // Create the group if it doesn't exist.
426 INSERT OR IGNORE INTO groups (id, name)
427 VALUES((SELECT id FROM groups WHERE name = :group), :group)
429 stmt.params.group = group;
433 // Finally create or update the pref.
436 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
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),
449 stmt.params.group = group;
452 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
456 JOIN settings ON settings.id = prefs.settingID
457 WHERE prefs.groupID IS NULL AND settings.name = :name),
459 (SELECT id FROM settings WHERE name = :name),
465 stmt.params.name = name;
466 stmt.params.value = value;
467 stmt.params.now = Date.now() / 1000;
470 this._execStmts(stmts, {
471 onDone: (reason, ok) => {
473 this._cache.setWithCast(group, name, value);
475 cbHandleCompletion(callback, reason);
481 context && context.usePrivateBrowsing
485 onError: nsresult => {
486 cbHandleError(callback, nsresult);
491 removeByDomainAndName: function CPS2_removeByDomainAndName(
497 checkGroupArg(group);
498 this._remove(group, name, false, context, callback);
501 removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(
507 checkGroupArg(group);
508 this._remove(group, name, true, context, callback);
511 removeGlobal: function CPS2_removeGlobal(name, context, callback) {
512 this._remove(null, name, false, context, callback);
515 _remove: function CPS2__remove(
522 group = this._parseGroup(group);
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);
534 // First get the matching prefs.
535 stmts.push(this._commonGetStmt(group, name, includeSubdomains));
537 // Delete the matching prefs.
538 let stmt = this._stmtWithGroupClause(
543 WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND
545 WHEN 'null' THEN prefs.groupID IS NULL
546 ELSE prefs.groupID IN (${GROUP_CLAUSE})
550 stmt.params.name = name;
553 stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
555 let prefs = new ContentPrefStore();
557 let isPrivate = context && context.usePrivateBrowsing;
558 this._execStmts(stmts, {
560 let grp = row.getResultByName("grp");
561 prefs.set(grp, name, undefined);
562 this._cache.set(grp, name, undefined);
564 onDone: (reason, ok) => {
566 this._cache.set(group, name, undefined);
568 for (let [sgroup] of this._pbStore.match(
573 prefs.set(sgroup, name, undefined);
574 this._pbStore.remove(sgroup, name);
578 cbHandleCompletion(callback, reason);
580 for (let [sgroup, ,] of prefs) {
581 this._notifyPrefRemoved(sgroup, name, isPrivate);
585 onError: nsresult => {
586 cbHandleError(callback, nsresult);
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.
598 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
601 DELETE FROM groups WHERE id NOT IN (
602 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
608 removeByDomain: function CPS2_removeByDomain(group, context, callback) {
609 checkGroupArg(group);
610 this._removeByDomain(group, false, context, callback);
613 removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
614 checkGroupArg(group);
615 this._removeByDomain(group, true, context, callback);
618 removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
619 this._removeByDomain(null, false, context, callback);
622 _removeByDomain: function CPS2__removeByDomain(
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);
639 // First get the matching prefs, then delete groups and prefs that reference
643 this._stmtWithGroupClause(
647 SELECT groups.name AS grp, settings.name AS name
649 JOIN settings ON settings.id = prefs.settingID
650 JOIN groups ON groups.id = prefs.groupID
651 WHERE prefs.groupID IN (${GROUP_CLAUSE})
656 this._stmtWithGroupClause(
659 `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})`
665 WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)
671 SELECT NULL AS grp, settings.name AS name
673 JOIN settings ON settings.id = prefs.settingID
674 WHERE prefs.groupID IS NULL
677 stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL"));
680 // Finally delete settings that are no longer referenced.
684 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
688 let prefs = new ContentPrefStore();
690 let isPrivate = context && context.usePrivateBrowsing;
691 this._execStmts(stmts, {
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);
698 onDone: (reason, ok) => {
699 if (ok && isPrivate) {
700 for (let [sgroup, sname] of this._pbStore) {
703 (!includeSubdomains && group == sgroup) ||
704 (includeSubdomains &&
706 this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))
708 prefs.set(sgroup, sname, undefined);
709 this._pbStore.remove(sgroup, sname);
713 cbHandleCompletion(callback, reason);
715 for (let [sgroup, sname] of prefs) {
716 this._notifyPrefRemoved(sgroup, sname, isPrivate);
720 onError: nsresult => {
721 cbHandleError(callback, nsresult);
726 _removeAllDomainsSince: function CPS2__removeAllDomainsSince(
731 checkCallbackArg(callback, false);
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();
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
746 JOIN settings ON settings.id = prefs.settingID
747 JOIN groups ON groups.id = prefs.groupID
748 WHERE timestamp >= :since
750 stmt.params.since = since;
753 // Do the actual remove.
755 DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since
757 stmt.params.since = since;
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, {
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);
772 onDone: (reason, ok) => {
773 // This nukes all the groups in _pbStore since we don't have their timestamp
775 if (ok && isPrivate) {
776 for (let [sgroup, sname] of this._pbStore) {
778 prefs.set(sgroup, sname, undefined);
781 this._pbStore.removeAllGroups();
783 cbHandleCompletion(callback, reason);
785 for (let [sgroup, sname] of prefs) {
786 this._notifyPrefRemoved(sgroup, sname, isPrivate);
790 onError: nsresult => {
791 cbHandleError(callback, nsresult);
796 removeAllDomainsSince: function CPS2_removeAllDomainsSince(
801 this._removeAllDomainsSince(since, context, callback);
804 removeAllDomains: function CPS2_removeAllDomains(context, callback) {
805 this._removeAllDomainsSince(0, context, callback);
808 removeByName: function CPS2_removeByName(name, context, callback) {
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) {
816 this._cache.remove(group, name);
822 // First get the matching prefs. Include null if any of those prefs are
824 let stmt = this._stmt(`
825 SELECT groups.name AS grp
827 JOIN settings ON settings.id = prefs.settingID
828 JOIN groups ON groups.id = prefs.groupID
829 WHERE settings.name = :name
835 JOIN settings ON settings.id = prefs.settingID
836 WHERE settings.name = :name AND prefs.groupID IS NULL
839 stmt.params.name = name;
842 // Delete the target settings.
843 stmt = this._stmt("DELETE FROM settings WHERE name = :name");
844 stmt.params.name = name;
847 // Delete prefs and groups that are no longer used.
850 "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)"
855 DELETE FROM groups WHERE id NOT IN (
856 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
861 let prefs = new ContentPrefStore();
862 let isPrivate = context && context.usePrivateBrowsing;
864 this._execStmts(stmts, {
866 let grp = row.getResultByName("grp");
867 prefs.set(grp, name, undefined);
868 this._cache.set(grp, name, undefined);
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);
879 cbHandleCompletion(callback, reason);
881 for (let [sgroup, ,] of prefs) {
882 this._notifyPrefRemoved(sgroup, name, isPrivate);
886 onError: nsresult => {
887 cbHandleError(callback, nsresult);
893 * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such
894 * statement is cached, one is created and cached.
896 * @param sql The SQL query string.
897 * @return The cached, possibly new, statement.
899 _stmt: function CPS2__stmt(sql, cachable = true) {
908 * Executes some async statements.
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)
917 * reason: A nsIContentPrefService2.COMPLETE_* value.
918 * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
919 * didGetRow: True if onRow was ever called.
920 * onError(nsresult) (optional)
922 * nsresult: The error code.
924 _execStmts: async function CPS2__execStmts(stmts, callbacks) {
925 let conn = await this.conn;
929 rows = await executeStatementsInTransaction(conn, stmts);
932 if (callbacks.onError) {
934 callbacks.onError(e);
943 if (rows && callbacks.onRow) {
944 for (let row of rows) {
946 callbacks.onRow(row);
956 ? Ci.nsIContentPrefCallback2.COMPLETE_OK
957 : Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
959 rows && !!rows.length
967 * Parses the domain (the "group", to use the database's term) from the given
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.
976 _parseGroup: function CPS2__parseGroup(groupStr) {
981 var groupURI = Services.io.newURI(groupStr);
982 groupStr = HostnameGrouper_group(groupURI);
984 return groupStr.substring(
986 Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH - 1
990 _schedule: function CPS2__schedule(fn) {
991 Services.tm.dispatchToMainThread(fn.bind(this));
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) {
1003 observers = this._observers.get(aName);
1005 observers = new Set();
1006 this._observers.set(aName, observers);
1009 observers = this._genericObservers;
1012 observers.add(aObserver);
1015 removeObserverForName(aName, aObserver) {
1018 observers = this._observers.get(aName);
1023 observers = this._genericObservers;
1026 observers.delete(aObserver);
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).
1036 _getObservers(aName) {
1037 let genericObserverList = Array.from(this._genericObservers);
1039 let observersForName = this._observers.get(aName);
1040 if (observersForName) {
1041 return Array.from(observersForName).concat(genericObserverList);
1044 return genericObserverList;
1048 * Notify all observers about the removal of a preference.
1050 _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(
1055 for (var observer of this._getObservers(aName)) {
1057 observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
1065 * Notify all observers about a preference change.
1067 _notifyPrefSet: function ContentPrefService__notifyPrefSet(
1073 for (var observer of this._getObservers(aName)) {
1075 observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
1082 extractDomain: function CPS2_extractDomain(str) {
1083 return this._parseGroup(str);
1087 * Tests use this as a backchannel by calling it directly.
1089 * @param subj This value depends on topic.
1090 * @param topic The backchannel "method" name.
1091 * @param data This value depends on topic.
1093 observe: function CPS2_observe(subj, topic, data) {
1095 case "profile-before-change":
1098 case "last-pb-context-exited":
1099 this._pbStore.removeAll();
1102 let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1106 let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
1107 obj.value = this.conn;
1113 * Removes all state from the service. Used by tests.
1115 * @param callback A function that will be called when done.
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, {
1133 QueryInterface: ChromeUtils.generateQI([
1134 "nsIContentPrefService2",
1138 // Database Creation & Access
1145 "id INTEGER PRIMARY KEY, \
1146 name TEXT NOT NULL",
1149 "id INTEGER PRIMARY KEY, \
1150 name TEXT NOT NULL",
1153 "id INTEGER PRIMARY KEY, \
1154 groupID INTEGER REFERENCES groups(id), \
1155 settingID INTEGER NOT NULL REFERENCES settings(id), \
1157 timestamp INTEGER NOT NULL DEFAULT 0", // Storage in seconds, API in ms. 0 for migrated values.
1170 columns: ["timestamp", "groupID", "settingID"],
1177 log: function CPS2_log(aMessage) {
1178 if (this._debugLog) {
1179 Services.console.logStringMessage("ContentPrefService2: " + aMessage);
1183 async _getConnection(aAttemptNum = 0) {
1185 Services.startup.isInOrBeyondShutdownPhase(
1186 Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWN
1189 throw new Error("Can't open content prefs, we're in shutdown.");
1191 let path = PathUtils.join(PathUtils.profileDir, "content-prefs.sqlite");
1193 let resetAndRetry = async e => {
1194 if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
1198 if (aAttemptNum >= this.MAX_ATTEMPTS) {
1202 this.log("Establishing connection failed too many times. Giving up.");
1207 await this._failover(conn, path);
1212 return this._getConnection(++aAttemptNum);
1215 conn = await lazy.Sqlite.openConnection({
1217 incrementalVacuum: true,
1221 lazy.Sqlite.shutdown.addBlocker(
1222 "Closing ContentPrefService2 connection.",
1226 // Uh oh, we failed to add a shutdown blocker. Close the connection
1227 // anyway, but make sure that doesn't throw.
1229 await conn?.close();
1237 return resetAndRetry(e);
1241 await this._dbMaybeInit(conn);
1244 return resetAndRetry(e);
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).
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");
1259 async _failover(aConn, aPath) {
1260 this.log("Cleaning up DB file - close & remove & backup.");
1262 await aConn.close();
1264 let uniquePath = await IOUtils.createUniqueFile(
1265 PathUtils.parent(aPath),
1266 PathUtils.filename(aPath) + ".corrupt",
1269 await IOUtils.copy(aPath, uniquePath);
1270 await IOUtils.remove(aPath);
1271 this.log("Completed DB cleanup.");
1274 _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
1275 let version = parseInt(await aConn.getSchemaVersion(), 10);
1276 this.log("Schema version: " + version);
1279 await this._dbCreateSchema(aConn);
1280 } else if (version != this._dbVersion) {
1281 await this._dbMigrate(aConn, version, this._dbVersion);
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})`);
1291 _createIndex: async function CPS2__createTable(aConn, aName) {
1292 let index = this._dbSchema.indices[aName];
1294 "CREATE INDEX IF NOT EXISTS " +
1299 index.columns.join(", ") +
1301 await aConn.execute(statement);
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);
1311 this.log("Creating DB -- indices");
1312 for (let name in this._dbSchema.indices) {
1313 await this._createIndex(aConn, name);
1316 await aConn.setSchemaVersion(this._dbVersion);
1320 _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
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.
1326 * 1. Decrement schema version so that upgrade runs the migrations again.
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") {
1333 "no migrator function from version " +
1339 await this[migrationName](aConn);
1341 await aConn.setSchemaVersion(aNewVersion);
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
1353 await aConn.execute("DROP TABLE groupers");
1354 await aConn.execute("DROP TABLE groupsOld");
1357 _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
1358 for (let name in this._dbSchema.indices) {
1359 await this._createIndex(aConn, name);
1363 _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
1364 // Add timestamp column if it does not exist yet. This operation is idempotent.
1366 await aConn.execute("SELECT timestamp FROM prefs");
1368 await aConn.execute(
1369 "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"
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);
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(`
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'
1391 (g.name BETWEEN 'data:' AND 'data:' || X'FFFF') OR
1392 (g.name BETWEEN 'file:' AND 'file:' || X'FFFF')
1396 await conn.execute(`
1397 DELETE FROM groups WHERE NOT EXISTS (
1398 SELECT 1 FROM prefs WHERE groupId = groups.id
1401 // Trim group names longer than MAX_GROUP_LENGTH.
1405 SET name = substr(name, 0, :maxlen)
1406 WHERE LENGTH(name) > :maxlen
1409 maxlen: Ci.nsIContentPrefService2.GROUP_NAME_MAX_LENGTH,
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(`
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'
1429 await conn.execute(`
1430 DELETE FROM groups WHERE NOT EXISTS (
1431 SELECT 1 FROM prefs WHERE groupId = groups.id
1437 function checkGroupArg(group) {
1438 if (!group || typeof group != "string") {
1439 throw invalidArg("domain must be nonempty string.");
1443 function checkNameArg(name) {
1444 if (!name || typeof name != "string") {
1445 throw invalidArg("name must be nonempty string.");
1449 function checkValueArg(value) {
1450 if (value === undefined) {
1451 throw invalidArg("value must not be undefined.");
1455 function checkCallbackArg(callback, required) {
1456 if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) {
1457 throw invalidArg("callback must be an nsIContentPrefCallback2.");
1459 if (!callback && required) {
1460 throw invalidArg("callback must be given.");
1464 function invalidArg(msg) {
1465 return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);