/* 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 = Object.keys(this._groups).length; if (groupCount >= CACHE_MAX_GROUP_ENTRIES) { // Clean half of the entries for (let [group, name, ] in 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(function (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; } 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); 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); }, 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); 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); }, 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, ] in this._privModeStorage) { if (name === aName) { this._privModeStorage.remove(group, aName); this._notifyPrefRemoved(group, aName); } } } 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); } } }, 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] in 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] in 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) { for each (var observer in this._getObservers(aName)) { try { observer.onContentPrefRemoved(aGroup, aName); } catch(ex) { Cu.reportError(ex); } } }, /** * Notify all observers about a preference change. */ _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue) { for each (var observer in this._getObservers(aName)) { try { observer.onContentPrefSet(aGroup, aName, aValue); } 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; } 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; } 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) { 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);