dom/settings/SettingsManager.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     5 "use strict";
     7 const DEBUG = false;
     8 function debug(s) {
     9   if (DEBUG) dump("-*- SettingsManager: " + s + "\n");
    10 }
    12 const Cc = Components.classes;
    13 const Ci = Components.interfaces;
    14 const Cu = Components.utils;
    16 Cu.import("resource://gre/modules/SettingsQueue.jsm");
    17 Cu.import("resource://gre/modules/SettingsDB.jsm");
    18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://gre/modules/ObjectWrapper.jsm");
    22 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
    23                                    "@mozilla.org/childprocessmessagemanager;1",
    24                                    "nsIMessageSender");
    25 XPCOMUtils.defineLazyServiceGetter(this, "mrm",
    26                                    "@mozilla.org/memory-reporter-manager;1",
    27                                    "nsIMemoryReporterManager");
    29 function SettingsLock(aSettingsManager) {
    30   this._open = true;
    31   this._isBusy = false;
    32   this._requests = new Queue();
    33   this._settingsManager = aSettingsManager;
    34   this._transaction = null;
    35 }
    37 SettingsLock.prototype = {
    38   get closed() {
    39     return !this._open;
    40   },
    42   _wrap: function _wrap(obj) {
    43     return Cu.cloneInto(obj, this._settingsManager._window);
    44   },
    46   process: function process() {
    47     let lock = this;
    48     let store = lock._transaction.objectStore(SETTINGSSTORE_NAME);
    50     while (!lock._requests.isEmpty()) {
    51       let info = lock._requests.dequeue();
    52       if (DEBUG) debug("info: " + info.intent);
    53       let request = info.request;
    54       switch (info.intent) {
    55         case "clear":
    56           let clearReq = store.clear();
    57           clearReq.onsuccess = function() {
    58             this._open = true;
    59             Services.DOMRequest.fireSuccess(request, 0);
    60             this._open = false;
    61           }.bind(lock);
    62           clearReq.onerror = function() {
    63             Services.DOMRequest.fireError(request, 0)
    64           };
    65           break;
    66         case "set":
    67           let keys = Object.getOwnPropertyNames(info.settings);
    68           for (let i = 0; i < keys.length; i++) {
    69             let key = keys[i];
    70             let last = i === keys.length - 1;
    71             if (DEBUG) debug("key: " + key + ", val: " + JSON.stringify(info.settings[key]) + ", type: " + typeof(info.settings[key]));
    72             lock._isBusy = true;
    73             let checkKeyRequest = store.get(key);
    75             checkKeyRequest.onsuccess = function (event) {
    76               let defaultValue;
    77               let userValue = info.settings[key];
    78               if (event.target.result) {
    79                 defaultValue = event.target.result.defaultValue;
    80               } else {
    81                 defaultValue = null;
    82                 if (DEBUG) debug("MOZSETTINGS-SET-WARNING: " + key + " is not in the database.\n");
    83               }
    85               let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue};
    86               if (DEBUG) debug("store1: " + JSON.stringify(obj));
    87               let setReq = store.put(obj);
    89               setReq.onsuccess = function() {
    90                 lock._isBusy = false;
    91                 cpmm.sendAsyncMessage("Settings:Changed", { key: key, value: userValue });
    92                 if (last && !request.error) {
    93                   lock._open = true;
    94                   Services.DOMRequest.fireSuccess(request, 0);
    95                   lock._open = false;
    96                   if (!lock._requests.isEmpty()) {
    97                     lock.process();
    98                   }
    99                 }
   100               };
   102               setReq.onerror = function() {
   103                 if (!request.error) {
   104                   Services.DOMRequest.fireError(request, setReq.error.name)
   105                 }
   106               };
   107             };
   108             checkKeyRequest.onerror = function(event) {
   109               if (!request.error) {
   110                 Services.DOMRequest.fireError(request, checkKeyRequest.error.name)
   111               }
   112             };
   113           }
   114           break;
   115         case "get":
   116           let getReq = (info.name === "*") ? store.mozGetAll()
   117                                            : store.mozGetAll(info.name);
   119           getReq.onsuccess = function(event) {
   120             if (DEBUG) debug("Request for '" + info.name + "' successful. " +
   121                              "Record count: " + event.target.result.length);
   123             if (event.target.result.length == 0) {
   124               if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + info.name + " is not in the database.\n");
   125             }
   127             let results = {};
   129             for (var i in event.target.result) {
   130               let result = event.target.result[i];
   131               var name = result.settingName;
   132               if (DEBUG) debug("VAL: " + result.userValue +", " + result.defaultValue + "\n");
   133               var value = result.userValue !== undefined ? result.userValue : result.defaultValue;
   134               results[name] = this._wrap(value);
   135             }
   137             this._open = true;
   138             Services.DOMRequest.fireSuccess(request, this._wrap(results));
   139             this._open = false;
   140           }.bind(lock);
   142           getReq.onerror = function() {
   143             Services.DOMRequest.fireError(request, 0)
   144           };
   145           break;
   146       }
   147     }
   148   },
   150   createTransactionAndProcess: function() {
   151     if (this._settingsManager._settingsDB._db) {
   152       var lock;
   153       while (lock = this._settingsManager._locks.dequeue()) {
   154         if (!lock._transaction) {
   155           let transactionType = this._settingsManager.hasWritePrivileges ? "readwrite" : "readonly";
   156           lock._transaction = lock._settingsManager._settingsDB._db.transaction(SETTINGSSTORE_NAME, transactionType);
   157         }
   158         if (!lock._isBusy) {
   159           lock.process();
   160         } else {
   161           this._settingsManager._locks.enqueue(lock);
   162         }
   163       }
   164       if (!this._requests.isEmpty() && !this._isBusy) {
   165         this.process();
   166       }
   167     }
   168   },
   170   get: function get(aName) {
   171     if (!this._open) {
   172       dump("Settings lock not open!\n");
   173       throw Components.results.NS_ERROR_ABORT;
   174     }
   176     if (this._settingsManager.hasReadPrivileges) {
   177       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
   178       this._requests.enqueue({ request: req, intent:"get", name: aName });
   179       this.createTransactionAndProcess();
   180       return req;
   181     } else {
   182       if (DEBUG) debug("get not allowed");
   183       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   184     }
   185   },
   187   _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
   188     // We need to serialize settings objects, otherwise they can change between
   189     // the set() call and the enqueued request being processed. We can't simply
   190     // parse(stringify(obj)) because that breaks things like Blobs, Files and
   191     // Dates, so we use stringify's replacer and parse's reviver parameters to
   192     // preserve binaries.
   193     let manager = this._settingsManager;
   194     let binaries = Object.create(null);
   195     let stringified = JSON.stringify(aObject, function(key, value) {
   196       value = manager._settingsDB.prepareValue(value);
   197       let kind = ObjectWrapper.getObjectKind(value);
   198       if (kind == "file" || kind == "blob" || kind == "date") {
   199         let uuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)
   200                                                       .generateUUID().toString();
   201         binaries[uuid] = value;
   202         return uuid;
   203       }
   204       return value;
   205     });
   206     return JSON.parse(stringified, function(key, value) {
   207       if (value in binaries) {
   208         return binaries[value];
   209       }
   210       return value;
   211     });
   212   },
   214   set: function set(aSettings) {
   215     if (!this._open) {
   216       throw "Settings lock not open";
   217     }
   219     if (this._settingsManager.hasWritePrivileges) {
   220       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
   221       if (DEBUG) debug("send: " + JSON.stringify(aSettings));
   222       let settings = this._serializePreservingBinaries(aSettings);
   223       this._requests.enqueue({request: req, intent: "set", settings: settings});
   224       this.createTransactionAndProcess();
   225       return req;
   226     } else {
   227       if (DEBUG) debug("set not allowed");
   228       throw "No permission to call set";
   229     }
   230   },
   232   clear: function clear() {
   233     if (!this._open) {
   234       throw "Settings lock not open";
   235     }
   237     if (this._settingsManager.hasWritePrivileges) {
   238       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
   239       this._requests.enqueue({ request: req, intent: "clear"});
   240       this.createTransactionAndProcess();
   241       return req;
   242     } else {
   243       if (DEBUG) debug("clear not allowed");
   244       throw "No permission to call clear";
   245     }
   246   },
   248   classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"),
   249   contractID: "@mozilla.org/settingsLock;1",
   250   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
   251 };
   253 function SettingsManager() {
   254   this._locks = new Queue();
   255   this._settingsDB = new SettingsDB();
   256   this._settingsDB.init();
   257 }
   259 SettingsManager.prototype = {
   260   _callbacks: null,
   262   _wrap: function _wrap(obj) {
   263     return Cu.cloneInto(obj, this._window);
   264   },
   266   nextTick: function nextTick(aCallback, thisObj) {
   267     if (thisObj)
   268       aCallback = aCallback.bind(thisObj);
   270     Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
   271   },
   273   set onsettingchange(aHandler) {
   274     this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler);
   275   },
   277   get onsettingchange() {
   278     return this.__DOM_IMPL__.getEventHandler("onsettingchange");
   279   },
   281   createLock: function() {
   282     if (DEBUG) debug("get lock!");
   283     var lock = new SettingsLock(this);
   284     this._locks.enqueue(lock);
   285     this._settingsDB.ensureDB(
   286       function() { lock.createTransactionAndProcess(); },
   287       function() { dump("Cannot open Settings DB. Trying to open an old version?\n"); }
   288     );
   289     this.nextTick(function() { this._open = false; }, lock);
   290     return lock;
   291   },
   293   receiveMessage: function(aMessage) {
   294     if (DEBUG) debug("Settings::receiveMessage: " + aMessage.name);
   295     let msg = aMessage.json;
   297     switch (aMessage.name) {
   298       case "Settings:Change:Return:OK":
   299         if (DEBUG) debug('data:' + msg.key + ':' + msg.value + '\n');
   301         let event = new this._window.MozSettingsEvent("settingchange", this._wrap({
   302           settingName: msg.key,
   303           settingValue: msg.value
   304         }));
   305         this.__DOM_IMPL__.dispatchEvent(event);
   307         if (this._callbacks && this._callbacks[msg.key]) {
   308           if (DEBUG) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length);
   309           this._callbacks[msg.key].forEach(function(cb) {
   310             cb(this._wrap({settingName: msg.key, settingValue: msg.value}));
   311           }.bind(this));
   312         } else {
   313           if (DEBUG) debug("no observers stored!");
   314         }
   315         break;
   316       default:
   317         if (DEBUG) debug("Wrong message: " + aMessage.name);
   318     }
   319   },
   321   addObserver: function addObserver(aName, aCallback) {
   322     if (DEBUG) debug("addObserver " + aName);
   323     if (!this._callbacks) {
   324       cpmm.sendAsyncMessage("Settings:RegisterForMessages");
   325       this._callbacks = {};
   326     }
   327     if (!this._callbacks[aName]) {
   328       this._callbacks[aName] = [aCallback];
   329     } else {
   330       this._callbacks[aName].push(aCallback);
   331     }
   332   },
   334   removeObserver: function removeObserver(aName, aCallback) {
   335     if (DEBUG) debug("deleteObserver " + aName);
   336     if (this._callbacks && this._callbacks[aName]) {
   337       let index = this._callbacks[aName].indexOf(aCallback)
   338       if (index != -1) {
   339         this._callbacks[aName].splice(index, 1)
   340       } else {
   341         if (DEBUG) debug("Callback not found for: " + aName);
   342       }
   343     } else {
   344       if (DEBUG) debug("No observers stored for " + aName);
   345     }
   346   },
   348   init: function(aWindow) {
   349     mrm.registerStrongReporter(this);
   350     cpmm.addMessageListener("Settings:Change:Return:OK", this);
   351     this._window = aWindow;
   352     Services.obs.addObserver(this, "inner-window-destroyed", false);
   353     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
   354     this.innerWindowID = util.currentInnerWindowID;
   356     let readPerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-read");
   357     let writePerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-write");
   358     this.hasReadPrivileges = readPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
   359     this.hasWritePrivileges = writePerm == Ci.nsIPermissionManager.ALLOW_ACTION;
   361     if (this.hasReadPrivileges) {
   362       cpmm.sendAsyncMessage("Settings:RegisterForMessages");
   363     }
   365     if (!this.hasReadPrivileges && !this.hasWritePrivileges) {
   366       dump("No settings permission for: " + aWindow.document.nodePrincipal.origin + "\n");
   367       Cu.reportError("No settings permission for: " + aWindow.document.nodePrincipal.origin);
   368     }
   369   },
   371   observe: function(aSubject, aTopic, aData) {
   372     if (DEBUG) debug("Topic: " + aTopic);
   373     if (aTopic == "inner-window-destroyed") {
   374       let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
   375       if (wId == this.innerWindowID) {
   376         this.cleanup();
   377       }
   378     }
   379   },
   381   collectReports: function(aCallback, aData) {
   382     for (var topic in this._callbacks) {
   383       let length = this._callbacks[topic].length;
   384       if (length == 0) {
   385         continue;
   386       }
   388       let path;
   389       if (length < 20) {
   390         path = "settings-observers";
   391       } else {
   392         path = "settings-observers-suspect/referent(topic=" + topic + ")";
   393       }
   395       aCallback.callback("", path,
   396                          Ci.nsIMemoryReporter.KIND_OTHER,
   397                          Ci.nsIMemoryReporter.UNITS_COUNT,
   398                          this._callbacks[topic].length,
   399                          "The number of settings observers for this topic.",
   400                          aData);
   401     }
   402   },
   404   cleanup: function() {
   405     Services.obs.removeObserver(this, "inner-window-destroyed");
   406     cpmm.removeMessageListener("Settings:Change:Return:OK", this);
   407     mrm.unregisterStrongReporter(this);
   408     this._requests = null;
   409     this._window = null;
   410     this._innerWindowID = null;
   411     this._settingsDB.close();
   412   },
   414   classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"),
   415   contractID: "@mozilla.org/settingsManager;1",
   416   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
   417                                          Ci.nsIDOMGlobalPropertyInitializer,
   418                                          Ci.nsIObserver,
   419                                          Ci.nsIMemoryReporter]),
   420 };
   422 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock])

mercurial