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

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

mercurial