michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const DEBUG = false; michael@0: function debug(s) { michael@0: if (DEBUG) dump("-*- SettingsManager: " + s + "\n"); michael@0: } michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/SettingsQueue.jsm"); michael@0: Cu.import("resource://gre/modules/SettingsDB.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/ObjectWrapper.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "mrm", michael@0: "@mozilla.org/memory-reporter-manager;1", michael@0: "nsIMemoryReporterManager"); michael@0: michael@0: function SettingsLock(aSettingsManager) { michael@0: this._open = true; michael@0: this._isBusy = false; michael@0: this._requests = new Queue(); michael@0: this._settingsManager = aSettingsManager; michael@0: this._transaction = null; michael@0: } michael@0: michael@0: SettingsLock.prototype = { michael@0: get closed() { michael@0: return !this._open; michael@0: }, michael@0: michael@0: _wrap: function _wrap(obj) { michael@0: return Cu.cloneInto(obj, this._settingsManager._window); michael@0: }, michael@0: michael@0: process: function process() { michael@0: let lock = this; michael@0: let store = lock._transaction.objectStore(SETTINGSSTORE_NAME); michael@0: michael@0: while (!lock._requests.isEmpty()) { michael@0: let info = lock._requests.dequeue(); michael@0: if (DEBUG) debug("info: " + info.intent); michael@0: let request = info.request; michael@0: switch (info.intent) { michael@0: case "clear": michael@0: let clearReq = store.clear(); michael@0: clearReq.onsuccess = function() { michael@0: this._open = true; michael@0: Services.DOMRequest.fireSuccess(request, 0); michael@0: this._open = false; michael@0: }.bind(lock); michael@0: clearReq.onerror = function() { michael@0: Services.DOMRequest.fireError(request, 0) michael@0: }; michael@0: break; michael@0: case "set": michael@0: let keys = Object.getOwnPropertyNames(info.settings); michael@0: for (let i = 0; i < keys.length; i++) { michael@0: let key = keys[i]; michael@0: let last = i === keys.length - 1; michael@0: if (DEBUG) debug("key: " + key + ", val: " + JSON.stringify(info.settings[key]) + ", type: " + typeof(info.settings[key])); michael@0: lock._isBusy = true; michael@0: let checkKeyRequest = store.get(key); michael@0: michael@0: checkKeyRequest.onsuccess = function (event) { michael@0: let defaultValue; michael@0: let userValue = info.settings[key]; michael@0: if (event.target.result) { michael@0: defaultValue = event.target.result.defaultValue; michael@0: } else { michael@0: defaultValue = null; michael@0: if (DEBUG) debug("MOZSETTINGS-SET-WARNING: " + key + " is not in the database.\n"); michael@0: } michael@0: michael@0: let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue}; michael@0: if (DEBUG) debug("store1: " + JSON.stringify(obj)); michael@0: let setReq = store.put(obj); michael@0: michael@0: setReq.onsuccess = function() { michael@0: lock._isBusy = false; michael@0: cpmm.sendAsyncMessage("Settings:Changed", { key: key, value: userValue }); michael@0: if (last && !request.error) { michael@0: lock._open = true; michael@0: Services.DOMRequest.fireSuccess(request, 0); michael@0: lock._open = false; michael@0: if (!lock._requests.isEmpty()) { michael@0: lock.process(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: setReq.onerror = function() { michael@0: if (!request.error) { michael@0: Services.DOMRequest.fireError(request, setReq.error.name) michael@0: } michael@0: }; michael@0: }; michael@0: checkKeyRequest.onerror = function(event) { michael@0: if (!request.error) { michael@0: Services.DOMRequest.fireError(request, checkKeyRequest.error.name) michael@0: } michael@0: }; michael@0: } michael@0: break; michael@0: case "get": michael@0: let getReq = (info.name === "*") ? store.mozGetAll() michael@0: : store.mozGetAll(info.name); michael@0: michael@0: getReq.onsuccess = function(event) { michael@0: if (DEBUG) debug("Request for '" + info.name + "' successful. " + michael@0: "Record count: " + event.target.result.length); michael@0: michael@0: if (event.target.result.length == 0) { michael@0: if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + info.name + " is not in the database.\n"); michael@0: } michael@0: michael@0: let results = {}; michael@0: michael@0: for (var i in event.target.result) { michael@0: let result = event.target.result[i]; michael@0: var name = result.settingName; michael@0: if (DEBUG) debug("VAL: " + result.userValue +", " + result.defaultValue + "\n"); michael@0: var value = result.userValue !== undefined ? result.userValue : result.defaultValue; michael@0: results[name] = this._wrap(value); michael@0: } michael@0: michael@0: this._open = true; michael@0: Services.DOMRequest.fireSuccess(request, this._wrap(results)); michael@0: this._open = false; michael@0: }.bind(lock); michael@0: michael@0: getReq.onerror = function() { michael@0: Services.DOMRequest.fireError(request, 0) michael@0: }; michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: createTransactionAndProcess: function() { michael@0: if (this._settingsManager._settingsDB._db) { michael@0: var lock; michael@0: while (lock = this._settingsManager._locks.dequeue()) { michael@0: if (!lock._transaction) { michael@0: let transactionType = this._settingsManager.hasWritePrivileges ? "readwrite" : "readonly"; michael@0: lock._transaction = lock._settingsManager._settingsDB._db.transaction(SETTINGSSTORE_NAME, transactionType); michael@0: } michael@0: if (!lock._isBusy) { michael@0: lock.process(); michael@0: } else { michael@0: this._settingsManager._locks.enqueue(lock); michael@0: } michael@0: } michael@0: if (!this._requests.isEmpty() && !this._isBusy) { michael@0: this.process(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: get: function get(aName) { michael@0: if (!this._open) { michael@0: dump("Settings lock not open!\n"); michael@0: throw Components.results.NS_ERROR_ABORT; michael@0: } michael@0: michael@0: if (this._settingsManager.hasReadPrivileges) { michael@0: let req = Services.DOMRequest.createRequest(this._settingsManager._window); michael@0: this._requests.enqueue({ request: req, intent:"get", name: aName }); michael@0: this.createTransactionAndProcess(); michael@0: return req; michael@0: } else { michael@0: if (DEBUG) debug("get not allowed"); michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: }, michael@0: michael@0: _serializePreservingBinaries: function _serializePreservingBinaries(aObject) { michael@0: // We need to serialize settings objects, otherwise they can change between michael@0: // the set() call and the enqueued request being processed. We can't simply michael@0: // parse(stringify(obj)) because that breaks things like Blobs, Files and michael@0: // Dates, so we use stringify's replacer and parse's reviver parameters to michael@0: // preserve binaries. michael@0: let manager = this._settingsManager; michael@0: let binaries = Object.create(null); michael@0: let stringified = JSON.stringify(aObject, function(key, value) { michael@0: value = manager._settingsDB.prepareValue(value); michael@0: let kind = ObjectWrapper.getObjectKind(value); michael@0: if (kind == "file" || kind == "blob" || kind == "date") { michael@0: let uuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator) michael@0: .generateUUID().toString(); michael@0: binaries[uuid] = value; michael@0: return uuid; michael@0: } michael@0: return value; michael@0: }); michael@0: return JSON.parse(stringified, function(key, value) { michael@0: if (value in binaries) { michael@0: return binaries[value]; michael@0: } michael@0: return value; michael@0: }); michael@0: }, michael@0: michael@0: set: function set(aSettings) { michael@0: if (!this._open) { michael@0: throw "Settings lock not open"; michael@0: } michael@0: michael@0: if (this._settingsManager.hasWritePrivileges) { michael@0: let req = Services.DOMRequest.createRequest(this._settingsManager._window); michael@0: if (DEBUG) debug("send: " + JSON.stringify(aSettings)); michael@0: let settings = this._serializePreservingBinaries(aSettings); michael@0: this._requests.enqueue({request: req, intent: "set", settings: settings}); michael@0: this.createTransactionAndProcess(); michael@0: return req; michael@0: } else { michael@0: if (DEBUG) debug("set not allowed"); michael@0: throw "No permission to call set"; michael@0: } michael@0: }, michael@0: michael@0: clear: function clear() { michael@0: if (!this._open) { michael@0: throw "Settings lock not open"; michael@0: } michael@0: michael@0: if (this._settingsManager.hasWritePrivileges) { michael@0: let req = Services.DOMRequest.createRequest(this._settingsManager._window); michael@0: this._requests.enqueue({ request: req, intent: "clear"}); michael@0: this.createTransactionAndProcess(); michael@0: return req; michael@0: } else { michael@0: if (DEBUG) debug("clear not allowed"); michael@0: throw "No permission to call clear"; michael@0: } michael@0: }, michael@0: michael@0: classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"), michael@0: contractID: "@mozilla.org/settingsLock;1", michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), michael@0: }; michael@0: michael@0: function SettingsManager() { michael@0: this._locks = new Queue(); michael@0: this._settingsDB = new SettingsDB(); michael@0: this._settingsDB.init(); michael@0: } michael@0: michael@0: SettingsManager.prototype = { michael@0: _callbacks: null, michael@0: michael@0: _wrap: function _wrap(obj) { michael@0: return Cu.cloneInto(obj, this._window); michael@0: }, michael@0: michael@0: nextTick: function nextTick(aCallback, thisObj) { michael@0: if (thisObj) michael@0: aCallback = aCallback.bind(thisObj); michael@0: michael@0: Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: set onsettingchange(aHandler) { michael@0: this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler); michael@0: }, michael@0: michael@0: get onsettingchange() { michael@0: return this.__DOM_IMPL__.getEventHandler("onsettingchange"); michael@0: }, michael@0: michael@0: createLock: function() { michael@0: if (DEBUG) debug("get lock!"); michael@0: var lock = new SettingsLock(this); michael@0: this._locks.enqueue(lock); michael@0: this._settingsDB.ensureDB( michael@0: function() { lock.createTransactionAndProcess(); }, michael@0: function() { dump("Cannot open Settings DB. Trying to open an old version?\n"); } michael@0: ); michael@0: this.nextTick(function() { this._open = false; }, lock); michael@0: return lock; michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: if (DEBUG) debug("Settings::receiveMessage: " + aMessage.name); michael@0: let msg = aMessage.json; michael@0: michael@0: switch (aMessage.name) { michael@0: case "Settings:Change:Return:OK": michael@0: if (DEBUG) debug('data:' + msg.key + ':' + msg.value + '\n'); michael@0: michael@0: let event = new this._window.MozSettingsEvent("settingchange", this._wrap({ michael@0: settingName: msg.key, michael@0: settingValue: msg.value michael@0: })); michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: michael@0: if (this._callbacks && this._callbacks[msg.key]) { michael@0: if (DEBUG) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length); michael@0: this._callbacks[msg.key].forEach(function(cb) { michael@0: cb(this._wrap({settingName: msg.key, settingValue: msg.value})); michael@0: }.bind(this)); michael@0: } else { michael@0: if (DEBUG) debug("no observers stored!"); michael@0: } michael@0: break; michael@0: default: michael@0: if (DEBUG) debug("Wrong message: " + aMessage.name); michael@0: } michael@0: }, michael@0: michael@0: addObserver: function addObserver(aName, aCallback) { michael@0: if (DEBUG) debug("addObserver " + aName); michael@0: if (!this._callbacks) { michael@0: cpmm.sendAsyncMessage("Settings:RegisterForMessages"); michael@0: this._callbacks = {}; michael@0: } michael@0: if (!this._callbacks[aName]) { michael@0: this._callbacks[aName] = [aCallback]; michael@0: } else { michael@0: this._callbacks[aName].push(aCallback); michael@0: } michael@0: }, michael@0: michael@0: removeObserver: function removeObserver(aName, aCallback) { michael@0: if (DEBUG) debug("deleteObserver " + aName); michael@0: if (this._callbacks && this._callbacks[aName]) { michael@0: let index = this._callbacks[aName].indexOf(aCallback) michael@0: if (index != -1) { michael@0: this._callbacks[aName].splice(index, 1) michael@0: } else { michael@0: if (DEBUG) debug("Callback not found for: " + aName); michael@0: } michael@0: } else { michael@0: if (DEBUG) debug("No observers stored for " + aName); michael@0: } michael@0: }, michael@0: michael@0: init: function(aWindow) { michael@0: mrm.registerStrongReporter(this); michael@0: cpmm.addMessageListener("Settings:Change:Return:OK", this); michael@0: this._window = aWindow; michael@0: Services.obs.addObserver(this, "inner-window-destroyed", false); michael@0: let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: this.innerWindowID = util.currentInnerWindowID; michael@0: michael@0: let readPerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-read"); michael@0: let writePerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-write"); michael@0: this.hasReadPrivileges = readPerm == Ci.nsIPermissionManager.ALLOW_ACTION; michael@0: this.hasWritePrivileges = writePerm == Ci.nsIPermissionManager.ALLOW_ACTION; michael@0: michael@0: if (this.hasReadPrivileges) { michael@0: cpmm.sendAsyncMessage("Settings:RegisterForMessages"); michael@0: } michael@0: michael@0: if (!this.hasReadPrivileges && !this.hasWritePrivileges) { michael@0: dump("No settings permission for: " + aWindow.document.nodePrincipal.origin + "\n"); michael@0: Cu.reportError("No settings permission for: " + aWindow.document.nodePrincipal.origin); michael@0: } michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (DEBUG) debug("Topic: " + aTopic); michael@0: if (aTopic == "inner-window-destroyed") { michael@0: let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: if (wId == this.innerWindowID) { michael@0: this.cleanup(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: collectReports: function(aCallback, aData) { michael@0: for (var topic in this._callbacks) { michael@0: let length = this._callbacks[topic].length; michael@0: if (length == 0) { michael@0: continue; michael@0: } michael@0: michael@0: let path; michael@0: if (length < 20) { michael@0: path = "settings-observers"; michael@0: } else { michael@0: path = "settings-observers-suspect/referent(topic=" + topic + ")"; michael@0: } michael@0: michael@0: aCallback.callback("", path, michael@0: Ci.nsIMemoryReporter.KIND_OTHER, michael@0: Ci.nsIMemoryReporter.UNITS_COUNT, michael@0: this._callbacks[topic].length, michael@0: "The number of settings observers for this topic.", michael@0: aData); michael@0: } michael@0: }, michael@0: michael@0: cleanup: function() { michael@0: Services.obs.removeObserver(this, "inner-window-destroyed"); michael@0: cpmm.removeMessageListener("Settings:Change:Return:OK", this); michael@0: mrm.unregisterStrongReporter(this); michael@0: this._requests = null; michael@0: this._window = null; michael@0: this._innerWindowID = null; michael@0: this._settingsDB.close(); michael@0: }, michael@0: michael@0: classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"), michael@0: contractID: "@mozilla.org/settingsManager;1", michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, michael@0: Ci.nsIDOMGlobalPropertyInitializer, michael@0: Ci.nsIObserver, michael@0: Ci.nsIMemoryReporter]), michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock])