diff options
Diffstat (limited to 'toolkit/components/contentprefs/nsContentPrefService.js')
-rw-r--r-- | toolkit/components/contentprefs/nsContentPrefService.js | 1332 |
1 files changed, 1332 insertions, 0 deletions
diff --git a/toolkit/components/contentprefs/nsContentPrefService.js b/toolkit/components/contentprefs/nsContentPrefService.js new file mode 100644 index 000000000..6360134a5 --- /dev/null +++ b/toolkit/components/contentprefs/nsContentPrefService.js @@ -0,0 +1,1332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +const CACHE_MAX_GROUP_ENTRIES = 100; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function ContentPrefService() { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm") + .ContentPrefServiceChild; + } + + // If this throws an exception, it causes the getService call to fail, + // but the next time a consumer tries to retrieve the service, we'll try + // to initialize the database again, which might work if the failure + // was due to a temporary condition (like being out of disk space). + this._dbInit(); + + this._observerSvc.addObserver(this, "last-pb-context-exited", false); + + // Observe shutdown so we can shut down the database connection. + this._observerSvc.addObserver(this, "xpcom-shutdown", false); +} + +Cu.import("resource://gre/modules/ContentPrefStore.jsm"); +const cache = new ContentPrefStore(); +cache.set = function CPS_cache_set(group, name, val) { + Object.getPrototypeOf(this).set.apply(this, arguments); + let groupCount = this._groups.size; + if (groupCount >= CACHE_MAX_GROUP_ENTRIES) { + // Clean half of the entries + for (let [group, name, ] of this) { + this.remove(group, name); + groupCount--; + if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) + break; + } + } +}; + +const privModeStorage = new ContentPrefStore(); + +ContentPrefService.prototype = { + // XPCOM Plumbing + + classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"), + + QueryInterface: function CPS_QueryInterface(iid) { + let supportedIIDs = [ + Ci.nsIContentPrefService, + Ci.nsISupports, + ]; + if (supportedIIDs.some(i => iid.equals(i))) + return this; + if (iid.equals(Ci.nsIContentPrefService2)) { + if (!this._contentPrefService2) { + let s = {}; + Cu.import("resource://gre/modules/ContentPrefService2.jsm", s); + this._contentPrefService2 = new s.ContentPrefService2(this); + } + return this._contentPrefService2; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // Convenience Getters + + // Observer Service + __observerSvc: null, + get _observerSvc() { + if (!this.__observerSvc) + this.__observerSvc = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerSvc; + }, + + // Console Service + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + // Preferences Service + __prefSvc: null, + get _prefSvc() { + if (!this.__prefSvc) + this.__prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + return this.__prefSvc; + }, + + + // Destruction + + _destroy: function ContentPrefService__destroy() { + this._observerSvc.removeObserver(this, "xpcom-shutdown"); + this._observerSvc.removeObserver(this, "last-pb-context-exited"); + + // Finalize statements which may have been used asynchronously. + // FIXME(696499): put them in an object cache like other components. + if (this.__stmtSelectPrefID) { + this.__stmtSelectPrefID.finalize(); + this.__stmtSelectPrefID = null; + } + if (this.__stmtSelectGlobalPrefID) { + this.__stmtSelectGlobalPrefID.finalize(); + this.__stmtSelectGlobalPrefID = null; + } + if (this.__stmtInsertPref) { + this.__stmtInsertPref.finalize(); + this.__stmtInsertPref = null; + } + if (this.__stmtInsertGroup) { + this.__stmtInsertGroup.finalize(); + this.__stmtInsertGroup = null; + } + if (this.__stmtInsertSetting) { + this.__stmtInsertSetting.finalize(); + this.__stmtInsertSetting = null; + } + if (this.__stmtSelectGroupID) { + this.__stmtSelectGroupID.finalize(); + this.__stmtSelectGroupID = null; + } + if (this.__stmtSelectSettingID) { + this.__stmtSelectSettingID.finalize(); + this.__stmtSelectSettingID = null; + } + if (this.__stmtSelectPref) { + this.__stmtSelectPref.finalize(); + this.__stmtSelectPref = null; + } + if (this.__stmtSelectGlobalPref) { + this.__stmtSelectGlobalPref.finalize(); + this.__stmtSelectGlobalPref = null; + } + if (this.__stmtSelectPrefsByName) { + this.__stmtSelectPrefsByName.finalize(); + this.__stmtSelectPrefsByName = null; + } + if (this.__stmtDeleteSettingIfUnused) { + this.__stmtDeleteSettingIfUnused.finalize(); + this.__stmtDeleteSettingIfUnused = null; + } + if (this.__stmtSelectPrefs) { + this.__stmtSelectPrefs.finalize(); + this.__stmtSelectPrefs = null; + } + if (this.__stmtDeleteGroupIfUnused) { + this.__stmtDeleteGroupIfUnused.finalize(); + this.__stmtDeleteGroupIfUnused = null; + } + if (this.__stmtDeletePref) { + this.__stmtDeletePref.finalize(); + this.__stmtDeletePref = null; + } + if (this.__stmtUpdatePref) { + this.__stmtUpdatePref.finalize(); + this.__stmtUpdatePref = null; + } + + if (this._contentPrefService2) + this._contentPrefService2.destroy(); + + this._dbConnection.asyncClose(); + + // Delete references to XPCOM components to make sure we don't leak them + // (although we haven't observed leakage in tests). Also delete references + // in _observers and _genericObservers to avoid cycles with those that + // refer to us and don't remove themselves from those observer pools. + delete this._observers; + delete this._genericObservers; + delete this.__consoleSvc; + delete this.__grouper; + delete this.__observerSvc; + delete this.__prefSvc; + }, + + + // nsIObserver + + observe: function ContentPrefService_observe(subject, topic, data) { + switch (topic) { + case "xpcom-shutdown": + this._destroy(); + break; + case "last-pb-context-exited": + this._privModeStorage.removeAll(); + break; + } + }, + + + // in-memory cache and private-browsing stores + + _cache: cache, + _privModeStorage: privModeStorage, + + // nsIContentPrefService + + getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + if (this._privModeStorage.has(group, aName)) { + let value = this._privModeStorage.get(group, aName); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + // if we don't have a pref specific to this private mode browsing + // session, to try to get one from normal mode + } + + if (group == null) + return this._selectGlobalPref(aName, aCallback); + return this._selectPref(group, aName, aCallback); + }, + + setPref: function ContentPrefService_setPref(aGroup, aName, aValue, aContext) { + warnDeprecated(); + + // If the pref is already set to the value, there's nothing more to do. + var currentValue = this.getPref(aGroup, aName, aContext); + if (typeof currentValue != "undefined") { + if (currentValue == aValue) + return; + } + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + this._privModeStorage.setWithCast(group, aName, aValue); + this._notifyPrefSet(group, aName, aValue, aContext.usePrivateBrowsing); + return; + } + + var settingID = this._selectSettingID(aName) || this._insertSetting(aName); + var groupID, prefID; + if (group == null) { + groupID = null; + prefID = this._selectGlobalPrefID(settingID); + } + else { + groupID = this._selectGroupID(group) || this._insertGroup(group); + prefID = this._selectPrefID(groupID, settingID); + } + + // Update the existing record, if any, or create a new one. + if (prefID) + this._updatePref(prefID, aValue); + else + this._insertPref(groupID, settingID, aValue); + + this._cache.setWithCast(group, aName, aValue); + + this._notifyPrefSet(group, aName, aValue, + aContext ? aContext.usePrivateBrowsing : false); + }, + + hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) { + warnDeprecated(); + + // XXX If consumers end up calling this method regularly, then we should + // optimize this to query the database directly. + return (typeof this.getPref(aGroup, aName, aContext) != "undefined"); + }, + + hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + let group = this._parseGroupParam(aGroup); + let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage: this._cache; + return storage.has(group, aName); + }, + + removePref: function ContentPrefService_removePref(aGroup, aName, aContext) { + warnDeprecated(); + + // If there's no old value, then there's nothing to remove. + if (!this.hasPref(aGroup, aName, aContext)) + return; + + var group = this._parseGroupParam(aGroup); + + if (aContext && aContext.usePrivateBrowsing) { + this._privModeStorage.remove(group, aName); + this._notifyPrefRemoved(group, aName, true); + return; + } + + var settingID = this._selectSettingID(aName); + var groupID, prefID; + if (group == null) { + groupID = null; + prefID = this._selectGlobalPrefID(settingID); + } + else { + groupID = this._selectGroupID(group); + prefID = this._selectPrefID(groupID, settingID); + } + + this._deletePref(prefID); + + // Get rid of extraneous records that are no longer being used. + this._deleteSettingIfUnused(settingID); + if (groupID) + this._deleteGroupIfUnused(groupID); + + this._cache.remove(group, aName); + this._notifyPrefRemoved(group, aName, false); + }, + + removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) { + warnDeprecated(); + + // will not delete global preferences + if (aContext && aContext.usePrivateBrowsing) { + // keep only global prefs + this._privModeStorage.removeAllGroups(); + } + this._cache.removeAllGroups(); + this._dbConnection.beginTransaction(); + try { + this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL"); + this._dbConnection.executeSimpleSQL("DELETE FROM groups"); + this._dbConnection.executeSimpleSQL(` + DELETE FROM settings + WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) + `); + this._dbConnection.commitTransaction(); + } + catch (ex) { + this._dbConnection.rollbackTransaction(); + throw ex; + } + }, + + removePrefsByName: function ContentPrefService_removePrefsByName(aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + if (aContext && aContext.usePrivateBrowsing) { + for (let [group, name, ] of this._privModeStorage) { + if (name === aName) { + this._privModeStorage.remove(group, aName); + this._notifyPrefRemoved(group, aName, true); + } + } + } + + var settingID = this._selectSettingID(aName); + if (!settingID) + return; + + var selectGroupsStmt = this._dbCreateStatement(` + SELECT groups.id AS groupID, groups.name AS groupName + FROM prefs + JOIN groups ON prefs.groupID = groups.id + WHERE prefs.settingID = :setting + `); + + var groupNames = []; + var groupIDs = []; + try { + selectGroupsStmt.params.setting = settingID; + + while (selectGroupsStmt.executeStep()) { + groupIDs.push(selectGroupsStmt.row["groupID"]); + groupNames.push(selectGroupsStmt.row["groupName"]); + } + } + finally { + selectGroupsStmt.reset(); + } + + if (this.hasPref(null, aName)) { + groupNames.push(null); + } + + this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID); + this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID); + + for (var i = 0; i < groupNames.length; i++) { + this._cache.remove(groupNames[i], aName); + if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length) + this._deleteGroupIfUnused(groupIDs[i]); + if (!aContext || !aContext.usePrivateBrowsing) { + this._notifyPrefRemoved(groupNames[i], aName, false); + } + } + }, + + getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) { + warnDeprecated(); + + var group = this._parseGroupParam(aGroup); + if (aContext && aContext.usePrivateBrowsing) { + let prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let [sgroup, sname, sval] of this._privModeStorage) { + if (sgroup === group) + prefs.setProperty(sname, sval); + } + return prefs; + } + + if (group == null) + return this._selectGlobalPrefs(); + return this._selectPrefs(group); + }, + + getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) { + warnDeprecated(); + + if (!aName) + throw Components.Exception("aName cannot be null or an empty string", + Cr.NS_ERROR_ILLEGAL_VALUE); + + if (aContext && aContext.usePrivateBrowsing) { + let prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let [sgroup, sname, sval] of this._privModeStorage) { + if (sname === aName) + prefs.setProperty(sgroup, sval); + } + return prefs; + } + + return this._selectPrefsByName(aName); + }, + + // A hash of arrays of observers, indexed by setting name. + _observers: {}, + + // An array of generic observers, which observe all settings. + _genericObservers: [], + + addObserver: function ContentPrefService_addObserver(aName, aObserver) { + warnDeprecated(); + this._addObserver.apply(this, arguments); + }, + + _addObserver: function ContentPrefService__addObserver(aName, aObserver) { + var observers; + if (aName) { + if (!this._observers[aName]) + this._observers[aName] = []; + observers = this._observers[aName]; + } + else + observers = this._genericObservers; + + if (observers.indexOf(aObserver) == -1) + observers.push(aObserver); + }, + + removeObserver: function ContentPrefService_removeObserver(aName, aObserver) { + warnDeprecated(); + this._removeObserver.apply(this, arguments); + }, + + _removeObserver: function ContentPrefService__removeObserver(aName, aObserver) { + var observers; + if (aName) { + if (!this._observers[aName]) + return; + observers = this._observers[aName]; + } + else + observers = this._genericObservers; + + if (observers.indexOf(aObserver) != -1) + observers.splice(observers.indexOf(aObserver), 1); + }, + + /** + * Construct a list of observers to notify about a change to some setting, + * putting setting-specific observers before before generic ones, so observers + * that initialize individual settings (like the page style controller) + * execute before observers that display multiple settings and depend on them + * being initialized first (like the content prefs sidebar). + */ + _getObservers: function ContentPrefService__getObservers(aName) { + var observers = []; + + if (aName && this._observers[aName]) + observers = observers.concat(this._observers[aName]); + observers = observers.concat(this._genericObservers); + + return observers; + }, + + /** + * Notify all observers about the removal of a preference. + */ + _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName, aIsPrivate) { + for (var observer of this._getObservers(aName)) { + try { + observer.onContentPrefRemoved(aGroup, aName, aIsPrivate); + } + catch (ex) { + Cu.reportError(ex); + } + } + }, + + /** + * Notify all observers about a preference change. + */ + _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue, aIsPrivate) { + for (var observer of this._getObservers(aName)) { + try { + observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate); + } + catch (ex) { + Cu.reportError(ex); + } + } + }, + + get grouper() { + warnDeprecated(); + return this._grouper; + }, + __grouper: null, + get _grouper() { + if (!this.__grouper) + this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"]. + getService(Ci.nsIContentURIGrouper); + return this.__grouper; + }, + + get DBConnection() { + warnDeprecated(); + return this._dbConnection; + }, + + + // Data Retrieval & Modification + + __stmtSelectPref: null, + get _stmtSelectPref() { + if (!this.__stmtSelectPref) + this.__stmtSelectPref = this._dbCreateStatement(` + SELECT prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE groups.name = :group + AND settings.name = :setting + `); + + return this.__stmtSelectPref; + }, + + _scheduleCallback: function(func) { + let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL); + }, + + _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) { + let value = undefined; + if (this._cache.has(aGroup, aSetting)) { + value = this._cache.get(aGroup, aSetting); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + + try { + this._stmtSelectPref.params.group = aGroup; + this._stmtSelectPref.params.setting = aSetting; + + if (aCallback) { + let cache = this._cache; + new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) { + cache.set(aGroup, aSetting, aResult); + aCallback.onResult(aResult); + }}); + } + else { + if (this._stmtSelectPref.executeStep()) { + value = this._stmtSelectPref.row["value"]; + } + this._cache.set(aGroup, aSetting, value); + } + } + finally { + this._stmtSelectPref.reset(); + } + + return value; + }, + + __stmtSelectGlobalPref: null, + get _stmtSelectGlobalPref() { + if (!this.__stmtSelectGlobalPref) + this.__stmtSelectGlobalPref = this._dbCreateStatement(` + SELECT prefs.value AS value + FROM prefs + JOIN settings ON prefs.settingID = settings.id + WHERE prefs.groupID IS NULL + AND settings.name = :name + `); + + return this.__stmtSelectGlobalPref; + }, + + _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) { + let value = undefined; + if (this._cache.has(null, aName)) { + value = this._cache.get(null, aName); + if (aCallback) { + this._scheduleCallback(function() { aCallback.onResult(value); }); + return undefined; + } + return value; + } + + try { + this._stmtSelectGlobalPref.params.name = aName; + + if (aCallback) { + let cache = this._cache; + new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) { + cache.set(null, aName, aResult); + aCallback.onResult(aResult); + }}); + } + else { + if (this._stmtSelectGlobalPref.executeStep()) { + value = this._stmtSelectGlobalPref.row["value"]; + } + this._cache.set(null, aName, value); + } + } + finally { + this._stmtSelectGlobalPref.reset(); + } + + return value; + }, + + __stmtSelectGroupID: null, + get _stmtSelectGroupID() { + if (!this.__stmtSelectGroupID) + this.__stmtSelectGroupID = this._dbCreateStatement(` + SELECT groups.id AS id + FROM groups + WHERE groups.name = :name + `); + + return this.__stmtSelectGroupID; + }, + + _selectGroupID: function ContentPrefService__selectGroupID(aName) { + var id; + + try { + this._stmtSelectGroupID.params.name = aName; + + if (this._stmtSelectGroupID.executeStep()) + id = this._stmtSelectGroupID.row["id"]; + } + finally { + this._stmtSelectGroupID.reset(); + } + + return id; + }, + + __stmtInsertGroup: null, + get _stmtInsertGroup() { + if (!this.__stmtInsertGroup) + this.__stmtInsertGroup = this._dbCreateStatement( + "INSERT INTO groups (name) VALUES (:name)" + ); + + return this.__stmtInsertGroup; + }, + + _insertGroup: function ContentPrefService__insertGroup(aName) { + this._stmtInsertGroup.params.name = aName; + this._stmtInsertGroup.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtSelectSettingID: null, + get _stmtSelectSettingID() { + if (!this.__stmtSelectSettingID) + this.__stmtSelectSettingID = this._dbCreateStatement( + "SELECT id FROM settings WHERE name = :name" + ); + + return this.__stmtSelectSettingID; + }, + + _selectSettingID: function ContentPrefService__selectSettingID(aName) { + var id; + + try { + this._stmtSelectSettingID.params.name = aName; + + if (this._stmtSelectSettingID.executeStep()) + id = this._stmtSelectSettingID.row["id"]; + } + finally { + this._stmtSelectSettingID.reset(); + } + + return id; + }, + + __stmtInsertSetting: null, + get _stmtInsertSetting() { + if (!this.__stmtInsertSetting) + this.__stmtInsertSetting = this._dbCreateStatement( + "INSERT INTO settings (name) VALUES (:name)" + ); + + return this.__stmtInsertSetting; + }, + + _insertSetting: function ContentPrefService__insertSetting(aName) { + this._stmtInsertSetting.params.name = aName; + this._stmtInsertSetting.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtSelectPrefID: null, + get _stmtSelectPrefID() { + if (!this.__stmtSelectPrefID) + this.__stmtSelectPrefID = this._dbCreateStatement( + "SELECT id FROM prefs WHERE groupID = :groupID AND settingID = :settingID" + ); + + return this.__stmtSelectPrefID; + }, + + _selectPrefID: function ContentPrefService__selectPrefID(aGroupID, aSettingID) { + var id; + + try { + this._stmtSelectPrefID.params.groupID = aGroupID; + this._stmtSelectPrefID.params.settingID = aSettingID; + + if (this._stmtSelectPrefID.executeStep()) + id = this._stmtSelectPrefID.row["id"]; + } + finally { + this._stmtSelectPrefID.reset(); + } + + return id; + }, + + __stmtSelectGlobalPrefID: null, + get _stmtSelectGlobalPrefID() { + if (!this.__stmtSelectGlobalPrefID) + this.__stmtSelectGlobalPrefID = this._dbCreateStatement( + "SELECT id FROM prefs WHERE groupID IS NULL AND settingID = :settingID" + ); + + return this.__stmtSelectGlobalPrefID; + }, + + _selectGlobalPrefID: function ContentPrefService__selectGlobalPrefID(aSettingID) { + var id; + + try { + this._stmtSelectGlobalPrefID.params.settingID = aSettingID; + + if (this._stmtSelectGlobalPrefID.executeStep()) + id = this._stmtSelectGlobalPrefID.row["id"]; + } + finally { + this._stmtSelectGlobalPrefID.reset(); + } + + return id; + }, + + __stmtInsertPref: null, + get _stmtInsertPref() { + if (!this.__stmtInsertPref) + this.__stmtInsertPref = this._dbCreateStatement(` + INSERT INTO prefs (groupID, settingID, value) + VALUES (:groupID, :settingID, :value) + `); + + return this.__stmtInsertPref; + }, + + _insertPref: function ContentPrefService__insertPref(aGroupID, aSettingID, aValue) { + this._stmtInsertPref.params.groupID = aGroupID; + this._stmtInsertPref.params.settingID = aSettingID; + this._stmtInsertPref.params.value = aValue; + this._stmtInsertPref.execute(); + return this._dbConnection.lastInsertRowID; + }, + + __stmtUpdatePref: null, + get _stmtUpdatePref() { + if (!this.__stmtUpdatePref) + this.__stmtUpdatePref = this._dbCreateStatement( + "UPDATE prefs SET value = :value WHERE id = :id" + ); + + return this.__stmtUpdatePref; + }, + + _updatePref: function ContentPrefService__updatePref(aPrefID, aValue) { + this._stmtUpdatePref.params.id = aPrefID; + this._stmtUpdatePref.params.value = aValue; + this._stmtUpdatePref.execute(); + }, + + __stmtDeletePref: null, + get _stmtDeletePref() { + if (!this.__stmtDeletePref) + this.__stmtDeletePref = this._dbCreateStatement( + "DELETE FROM prefs WHERE id = :id" + ); + + return this.__stmtDeletePref; + }, + + _deletePref: function ContentPrefService__deletePref(aPrefID) { + this._stmtDeletePref.params.id = aPrefID; + this._stmtDeletePref.execute(); + }, + + __stmtDeleteSettingIfUnused: null, + get _stmtDeleteSettingIfUnused() { + if (!this.__stmtDeleteSettingIfUnused) + this.__stmtDeleteSettingIfUnused = this._dbCreateStatement(` + DELETE FROM settings WHERE id = :id + AND id NOT IN (SELECT DISTINCT settingID FROM prefs) + `); + + return this.__stmtDeleteSettingIfUnused; + }, + + _deleteSettingIfUnused: function ContentPrefService__deleteSettingIfUnused(aSettingID) { + this._stmtDeleteSettingIfUnused.params.id = aSettingID; + this._stmtDeleteSettingIfUnused.execute(); + }, + + __stmtDeleteGroupIfUnused: null, + get _stmtDeleteGroupIfUnused() { + if (!this.__stmtDeleteGroupIfUnused) + this.__stmtDeleteGroupIfUnused = this._dbCreateStatement(` + DELETE FROM groups WHERE id = :id + AND id NOT IN (SELECT DISTINCT groupID FROM prefs) + `); + + return this.__stmtDeleteGroupIfUnused; + }, + + _deleteGroupIfUnused: function ContentPrefService__deleteGroupIfUnused(aGroupID) { + this._stmtDeleteGroupIfUnused.params.id = aGroupID; + this._stmtDeleteGroupIfUnused.execute(); + }, + + __stmtSelectPrefs: null, + get _stmtSelectPrefs() { + if (!this.__stmtSelectPrefs) + this.__stmtSelectPrefs = this._dbCreateStatement(` + SELECT settings.name AS name, prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE groups.name = :group + `); + + return this.__stmtSelectPrefs; + }, + + _selectPrefs: function ContentPrefService__selectPrefs(aGroup) { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + this._stmtSelectPrefs.params.group = aGroup; + + while (this._stmtSelectPrefs.executeStep()) + prefs.setProperty(this._stmtSelectPrefs.row["name"], + this._stmtSelectPrefs.row["value"]); + } + finally { + this._stmtSelectPrefs.reset(); + } + + return prefs; + }, + + __stmtSelectGlobalPrefs: null, + get _stmtSelectGlobalPrefs() { + if (!this.__stmtSelectGlobalPrefs) + this.__stmtSelectGlobalPrefs = this._dbCreateStatement(` + SELECT settings.name AS name, prefs.value AS value + FROM prefs + JOIN settings ON prefs.settingID = settings.id + WHERE prefs.groupID IS NULL + `); + + return this.__stmtSelectGlobalPrefs; + }, + + _selectGlobalPrefs: function ContentPrefService__selectGlobalPrefs() { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + while (this._stmtSelectGlobalPrefs.executeStep()) + prefs.setProperty(this._stmtSelectGlobalPrefs.row["name"], + this._stmtSelectGlobalPrefs.row["value"]); + } + finally { + this._stmtSelectGlobalPrefs.reset(); + } + + return prefs; + }, + + __stmtSelectPrefsByName: null, + get _stmtSelectPrefsByName() { + if (!this.__stmtSelectPrefsByName) + this.__stmtSelectPrefsByName = this._dbCreateStatement(` + SELECT groups.name AS groupName, prefs.value AS value + FROM prefs + JOIN groups ON prefs.groupID = groups.id + JOIN settings ON prefs.settingID = settings.id + WHERE settings.name = :setting + `); + + return this.__stmtSelectPrefsByName; + }, + + _selectPrefsByName: function ContentPrefService__selectPrefsByName(aName) { + var prefs = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + + try { + this._stmtSelectPrefsByName.params.setting = aName; + + while (this._stmtSelectPrefsByName.executeStep()) + prefs.setProperty(this._stmtSelectPrefsByName.row["groupName"], + this._stmtSelectPrefsByName.row["value"]); + } + finally { + this._stmtSelectPrefsByName.reset(); + } + + var global = this._selectGlobalPref(aName); + if (typeof global != "undefined") { + prefs.setProperty(null, global); + } + + return prefs; + }, + + + // Database Creation & Access + + _dbVersion: 4, + + _dbSchema: { + tables: { + groups: "id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL", + + settings: "id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL", + + prefs: "id INTEGER PRIMARY KEY, \ + groupID INTEGER REFERENCES groups(id), \ + settingID INTEGER NOT NULL REFERENCES settings(id), \ + value BLOB, \ + timestamp INTEGER NOT NULL DEFAULT 0" // Storage in seconds, API in ms. 0 for migrated values. + }, + indices: { + groups_idx: { + table: "groups", + columns: ["name"] + }, + settings_idx: { + table: "settings", + columns: ["name"] + }, + prefs_idx: { + table: "prefs", + columns: ["timestamp", "groupID", "settingID"] + } + } + }, + + _dbConnection: null, + + _dbCreateStatement: function ContentPrefService__dbCreateStatement(aSQLString) { + try { + var statement = this._dbConnection.createStatement(aSQLString); + } + catch (ex) { + Cu.reportError("error creating statement " + aSQLString + ": " + + this._dbConnection.lastError + " - " + + this._dbConnection.lastErrorString); + throw ex; + } + + return statement; + }, + + // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version- + // specific migration methods) must be careful not to call any method + // of the service that assumes the database connection has already been + // initialized, since it won't be initialized until at the end of _dbInit. + + _dbInit: function ContentPrefService__dbInit() { + var dirService = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + var dbFile = dirService.get("ProfD", Ci.nsIFile); + dbFile.append("content-prefs.sqlite"); + + var dbService = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + + var dbConnection; + + if (!dbFile.exists()) + dbConnection = this._dbCreate(dbService, dbFile); + else { + try { + dbConnection = dbService.openDatabase(dbFile); + } + // If the connection isn't ready after we open the database, that means + // the database has been corrupted, so we back it up and then recreate it. + catch (e) { + if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) + throw e; + dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, + dbConnection); + } + + // Get the version of the schema in the file. + var version = dbConnection.schemaVersion; + + // Try to migrate the schema in the database to the current schema used by + // the service. If migration fails, back up the database and recreate it. + if (version != this._dbVersion) { + try { + this._dbMigrate(dbConnection, version, this._dbVersion); + } + catch (ex) { + Cu.reportError("error migrating DB: " + ex + "; backing up and recreating"); + dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, dbConnection); + } + } + } + + // Turn off disk synchronization checking to reduce disk churn and speed up + // operations when prefs are changed rapidly (such as when a user repeatedly + // changes the value of the browser zoom setting for a site). + // + // Note: this could cause database corruption if the OS crashes or machine + // loses power before the data gets written to disk, but this is considered + // a reasonable risk for the not-so-critical data stored in this database. + // + // If you really don't want to take this risk, however, just set the + // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2 + // (FULL synchronization), in which case mozStorageConnection::Initialize + // will use that value, and we won't override it here. + if (!this._prefSvc.prefHasUserValue("toolkit.storage.synchronous")) + dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF"); + + this._dbConnection = dbConnection; + }, + + _dbCreate: function ContentPrefService__dbCreate(aDBService, aDBFile) { + var dbConnection = aDBService.openDatabase(aDBFile); + + try { + this._dbCreateSchema(dbConnection); + dbConnection.schemaVersion = this._dbVersion; + } + catch (ex) { + // If we failed to create the database (perhaps because the disk ran out + // of space), then remove the database file so we don't leave it in some + // half-created state from which we won't know how to recover. + dbConnection.close(); + aDBFile.remove(false); + throw ex; + } + + return dbConnection; + }, + + _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) { + this._dbCreateTables(aDBConnection); + this._dbCreateIndices(aDBConnection); + }, + + _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) { + for (let name in this._dbSchema.tables) + aDBConnection.createTable(name, this._dbSchema.tables[name]); + }, + + _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) { + for (let name in this._dbSchema.indices) { + let index = this._dbSchema.indices[name]; + let statement = ` + CREATE INDEX IF NOT EXISTS ${name} ON ${index.table} + (${index.columns.join(", ")}) + `; + aDBConnection.executeSimpleSQL(statement); + } + }, + + _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBService, + aDBFile, + aDBConnection) { + aDBService.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt"); + + // Close the database, ignoring the "already closed" exception, if any. + // It'll be open if we're here because of a migration failure but closed + // if we're here because of database corruption. + try { aDBConnection.close() } catch (ex) {} + + aDBFile.remove(false); + + let dbConnection = this._dbCreate(aDBService, aDBFile); + + return dbConnection; + }, + + _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) { + /** + * Migrations should follow the template rules in bug 1074817 comment 3 which are: + * 1. Migration should be incremental and non-breaking. + * 2. It should be idempotent because one can downgrade an upgrade again. + * On downgrade: + * 1. Decrement schema version so that upgrade runs the migrations again. + */ + aDBConnection.beginTransaction(); + + try { + /** + * If the schema version is 0, that means it was never set, which means + * the database was somehow created without the schema being applied, perhaps + * because the system ran out of disk space (although we check for this + * in _createDB) or because some other code created the database file without + * applying the schema. In any case, recover by simply reapplying the schema. + */ + if (aOldVersion == 0) { + this._dbCreateSchema(aDBConnection); + } else { + for (let i = aOldVersion; i < aNewVersion; i++) { + let migrationName = "_dbMigrate" + i + "To" + (i + 1); + if (typeof this[migrationName] != 'function') { + throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion); + } + this[migrationName](aDBConnection); + } + } + aDBConnection.schemaVersion = aNewVersion; + aDBConnection.commitTransaction(); + } catch (ex) { + aDBConnection.rollbackTransaction(); + throw ex; + } + }, + + _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) { + aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld"); + aDBConnection.createTable("groups", this._dbSchema.tables.groups); + aDBConnection.executeSimpleSQL(` + INSERT INTO groups (id, name) + SELECT id, name FROM groupsOld + `); + + aDBConnection.executeSimpleSQL("DROP TABLE groupers"); + aDBConnection.executeSimpleSQL("DROP TABLE groupsOld"); + }, + + _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) { + this._dbCreateIndices(aDBConnection); + }, + + _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) { + // Add timestamp column if it does not exist yet. This operation is idempotent. + try { + let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs"); + stmt.finalize(); + } catch (e) { + aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"); + } + + // To modify prefs_idx drop it and create again. + aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx"); + this._dbCreateIndices(aDBConnection); + }, + + _parseGroupParam: function ContentPrefService__parseGroupParam(aGroup) { + if (aGroup == null) + return null; + if (aGroup.constructor.name == "String") + return aGroup.toString(); + if (aGroup instanceof Ci.nsIURI) + return this.grouper.group(aGroup); + + throw Components.Exception("aGroup is not a string, nsIURI or null", + Cr.NS_ERROR_ILLEGAL_VALUE); + }, +}; + +function warnDeprecated() { + let Deprecated = Cu.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated; + Deprecated.warning("nsIContentPrefService is deprecated. Please use nsIContentPrefService2 instead.", + "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIContentPrefService2", + Components.stack.caller); +} + + +function HostnameGrouper() {} + +HostnameGrouper.prototype = { + // XPCOM Plumbing + + classID: Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentURIGrouper]), + + // nsIContentURIGrouper + + group: function HostnameGrouper_group(aURI) { + var group; + + try { + // Accessing the host property of the URI will throw an exception + // if the URI is of a type that doesn't have a host property. + // Otherwise, we manually throw an exception if the host is empty, + // since the effect is the same (we can't derive a group from it). + + group = aURI.host; + if (!group) + throw ("can't derive group from host; no host in URI"); + } + catch (ex) { + // If we don't have a host, then use the entire URI (minus the query, + // reference, and hash, if possible) as the group. This means that URIs + // like about:mozilla and about:blank will be considered separate groups, + // but at least they'll be grouped somehow. + + // This also means that each individual file: URL will be considered + // its own group. This seems suboptimal, but so does treating the entire + // file: URL space as a single group (especially if folks start setting + // group-specific capabilities prefs). + + // XXX Is there something better we can do here? + + try { + var url = aURI.QueryInterface(Ci.nsIURL); + group = aURI.prePath + url.filePath; + } + catch (ex) { + group = aURI.spec; + } + } + + return group; + } +}; + +function AsyncStatement(aStatement) { + this.stmt = aStatement; +} + +AsyncStatement.prototype = { + execute: function AsyncStmt_execute(aCallback) { + let stmt = this.stmt; + stmt.executeAsync({ + _callback: aCallback, + _hadResult: false, + handleResult: function(aResult) { + this._hadResult = true; + if (this._callback) { + let row = aResult.getNextRow(); + this._callback.onResult(row.getResultByName("value")); + } + }, + handleCompletion: function(aReason) { + if (!this._hadResult && this._callback && + aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) + this._callback.onResult(undefined); + }, + handleError: function(aError) {} + }); + } +}; + +// XPCOM Plumbing + +var components = [ContentPrefService, HostnameGrouper]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); |