bug 700693 - OCSP stapling PSM changes r=bsmith
[gecko.git] / dom / settings / SettingsManager.js
bloba24580ffefc10fe9cb28f0226a8551bf4618eb60
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");
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");
26 const nsIClassInfo            = Ci.nsIClassInfo;
27 const SETTINGSLOCK_CONTRACTID = "@mozilla.org/settingsLock;1";
28 const SETTINGSLOCK_CID        = Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}");
29 const nsIDOMSettingsLock      = Ci.nsIDOMSettingsLock;
31 function SettingsLock(aSettingsManager)
33   this._open = true;
34   this._isBusy = false;
35   this._requests = new Queue();
36   this._settingsManager = aSettingsManager;
37   this._transaction = null;
40 SettingsLock.prototype = {
42   get closed() {
43     return !this._open;
44   },
46   _wrap: function _wrap(obj) {
47     return ObjectWrapper.wrap(obj, this._settingsManager._window);
48   },
50   process: function process() {
51     let lock = this;
52     lock._open = false;
53     let store = lock._transaction.objectStore(SETTINGSSTORE_NAME);
55     while (!lock._requests.isEmpty()) {
56       let info = lock._requests.dequeue();
57       if (DEBUG) debug("info: " + info.intent);
58       let request = info.request;
59       switch (info.intent) {
60         case "clear":
61           let clearReq = store.clear();
62           clearReq.onsuccess = function() {
63             this._open = true;
64             Services.DOMRequest.fireSuccess(request, 0);
65             this._open = false;
66           }.bind(lock);
67           clearReq.onerror = function() {
68             Services.DOMRequest.fireError(request, 0)
69           };
70           break;
71         case "set":
72           let keys = Object.getOwnPropertyNames(info.settings);
73           for (let i = 0; i < keys.length; i++) {
74             let key = keys[i];
75             let last = i === keys.length - 1;
76             if (DEBUG) debug("key: " + key + ", val: " + JSON.stringify(info.settings[key]) + ", type: " + typeof(info.settings[key]));
77             lock._isBusy = true;
78             let checkKeyRequest = store.get(key);
80             checkKeyRequest.onsuccess = function (event) {
81               let defaultValue;
82               let userValue = info.settings[key];
83               if (event.target.result) {
84                 defaultValue = event.target.result.defaultValue;
85               } else {
86                 defaultValue = null;
87                 if (DEBUG) debug("MOZSETTINGS-SET-WARNING: " + key + " is not in the database.\n");
88               }
90               let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue};
91               if (DEBUG) debug("store1: " + JSON.stringify(obj));
92               let setReq = store.put(obj);
94               setReq.onsuccess = function() {
95                 lock._isBusy = false;
96                 cpmm.sendAsyncMessage("Settings:Changed", { key: key, value: userValue });
97                 if (last && !request.error) {
98                   lock._open = true;
99                   Services.DOMRequest.fireSuccess(request, 0);
100                   lock._open = false;
101                   if (!lock._requests.isEmpty()) {
102                     lock.process();
103                   }
104                 }
105               };
107               setReq.onerror = function() {
108                 if (!request.error) {
109                   Services.DOMRequest.fireError(request, setReq.error.name)
110                 }
111               };
112             };
113             checkKeyRequest.onerror = function(event) {
114               if (!request.error) {
115                 Services.DOMRequest.fireError(request, checkKeyRequest.error.name)
116               }
117             };
118           }
119           break;
120         case "get":
121           let getReq = (info.name === "*") ? store.mozGetAll()
122                                            : store.mozGetAll(info.name);
124           getReq.onsuccess = function(event) {
125             if (DEBUG) debug("Request for '" + info.name + "' successful. " +
126                              "Record count: " + event.target.result.length);
128             if (event.target.result.length == 0) {
129               if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + info.name + " is not in the database.\n");
130             }
132             let results = {};
134             for (var i in event.target.result) {
135               let result = event.target.result[i];
136               var name = result.settingName;
137               if (DEBUG) debug("VAL: " + result.userValue +", " + result.defaultValue + "\n");
138               var value = result.userValue !== undefined ? result.userValue : result.defaultValue;
139               results[name] = this._wrap(value);
140             }
142             this._open = true;
143             Services.DOMRequest.fireSuccess(request, this._wrap(results));
144             this._open = false;
145           }.bind(lock);
147           getReq.onerror = function() {
148             Services.DOMRequest.fireError(request, 0)
149           };
150           break;
151       }
152     }
153     lock._open = true;
154   },
156   createTransactionAndProcess: function() {
157     if (this._settingsManager._settingsDB._db) {
158       var lock;
159       while (lock = this._settingsManager._locks.dequeue()) {
160         if (!lock._transaction) {
161           let transactionType = this._settingsManager.hasWritePrivileges ? "readwrite" : "readonly";
162           lock._transaction = lock._settingsManager._settingsDB._db.transaction(SETTINGSSTORE_NAME, transactionType);
163         }
164         if (!lock._isBusy) {
165           lock.process();
166         } else {
167           this._settingsManager._locks.enqueue(lock);
168         }
169       }
170       if (!this._requests.isEmpty() && !this._isBusy) {
171         this.process();
172       }
173     }
174   },
176   get: function get(aName) {
177     if (!this._open) {
178       dump("Settings lock not open!\n");
179       throw Components.results.NS_ERROR_ABORT;
180     }
182     if (this._settingsManager.hasReadPrivileges) {
183       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
184       this._requests.enqueue({ request: req, intent:"get", name: aName });
185       this.createTransactionAndProcess();
186       return req;
187     } else {
188       if (DEBUG) debug("get not allowed");
189       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
190     }
191   },
193   _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
194     // We need to serialize settings objects, otherwise they can change between
195     // the set() call and the enqueued request being processed. We can't simply
196     // parse(stringify(obj)) because that breaks things like Blobs, Files and
197     // Dates, so we use stringify's replacer and parse's reviver parameters to
198     // preserve binaries.
199     let binaries = Object.create(null);
200     let stringified = JSON.stringify(aObject, function(key, value) {
201       let kind = ObjectWrapper.getObjectKind(value);
202       if (kind == "file" || kind == "blob" || kind == "date") {
203         let uuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)
204                                                       .generateUUID().toString();
205         binaries[uuid] = value;
206         return uuid;
207       }
208       return value;
209     });
210     return JSON.parse(stringified, function(key, value) {
211       if (value in binaries) {
212         return binaries[value];
213       }
214       return value;
215     });
216   },
218   set: function set(aSettings) {
219     if (!this._open) {
220       dump("Settings lock not open!\n");
221       throw Components.results.NS_ERROR_ABORT;
222     }
224     if (this._settingsManager.hasWritePrivileges) {
225       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
226       if (DEBUG) debug("send: " + JSON.stringify(aSettings));
227       let settings = this._serializePreservingBinaries(aSettings);
228       this._requests.enqueue({request: req, intent: "set", settings: settings});
229       this.createTransactionAndProcess();
230       return req;
231     } else {
232       if (DEBUG) debug("set not allowed");
233       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
234     }
235   },
237   clear: function clear() {
238     if (!this._open) {
239       dump("Settings lock not open!\n");
240       throw Components.results.NS_ERROR_ABORT;
241     }
243     if (this._settingsManager.hasWritePrivileges) {
244       let req = Services.DOMRequest.createRequest(this._settingsManager._window);
245       this._requests.enqueue({ request: req, intent: "clear"});
246       this.createTransactionAndProcess();
247       return req;
248     } else {
249       if (DEBUG) debug("clear not allowed");
250       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
251     }
252   },
254   classID : SETTINGSLOCK_CID,
255   QueryInterface : XPCOMUtils.generateQI([nsIDOMSettingsLock]),
257   classInfo : XPCOMUtils.generateCI({classID: SETTINGSLOCK_CID,
258                                      contractID: SETTINGSLOCK_CONTRACTID,
259                                      classDescription: "SettingsLock",
260                                      interfaces: [nsIDOMSettingsLock],
261                                      flags: nsIClassInfo.DOM_OBJECT})
264 const SETTINGSMANAGER_CONTRACTID = "@mozilla.org/settingsManager;1";
265 const SETTINGSMANAGER_CID        = Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}");
266 const nsIDOMSettingsManager      = Ci.nsIDOMSettingsManager;
268 let myGlobal = this;
270 function SettingsManager()
272   this._locks = new Queue();
273   if (!("indexedDB" in myGlobal)) {
274     let idbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager);
275     idbManager.initWindowless(myGlobal);
276   }
277   this._settingsDB = new SettingsDB();
278   this._settingsDB.init(myGlobal);
281 SettingsManager.prototype = {
282   _onsettingchange: null,
283   _callbacks: null,
285   _wrap: function _wrap(obj) {
286     return ObjectWrapper.wrap(obj, this._window);
287   },
289   nextTick: function nextTick(aCallback, thisObj) {
290     if (thisObj)
291       aCallback = aCallback.bind(thisObj);
293     Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
294   },
296   set onsettingchange(aCallback) {
297     if (this.hasReadPrivileges) {
298       if (!this._onsettingchange) {
299         cpmm.sendAsyncMessage("Settings:RegisterForMessages");
300       }
301       this._onsettingchange = aCallback;
302     } else {
303       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
304     }
305   },
307   get onsettingchange() {
308     return this._onsettingchange;
309   },
311   createLock: function() {
312     if (DEBUG) debug("get lock!");
313     var lock = new SettingsLock(this);
314     this._locks.enqueue(lock);
315     this._settingsDB.ensureDB(
316       function() { lock.createTransactionAndProcess(); },
317       function() { dump("Cannot open Settings DB. Trying to open an old version?\n"); },
318       myGlobal );
319     this.nextTick(function() { this._open = false; }, lock);
320     return lock;
321   },
323   receiveMessage: function(aMessage) {
324     if (DEBUG) debug("Settings::receiveMessage: " + aMessage.name);
325     let msg = aMessage.json;
327     switch (aMessage.name) {
328       case "Settings:Change:Return:OK":
329         if (this._onsettingchange || this._callbacks) {
330           if (DEBUG) debug('data:' + msg.key + ':' + msg.value + '\n');
332           if (this._onsettingchange) {
333             let event = new this._window.MozSettingsEvent("settingchanged", this._wrap({
334               settingName: msg.key,
335               settingValue: msg.value
336             }));
337             this._onsettingchange.handleEvent(event);
338           }
339           if (this._callbacks && this._callbacks[msg.key]) {
340             if (DEBUG) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length);
341             this._callbacks[msg.key].forEach(function(cb) {
342               cb(this._wrap({settingName: msg.key, settingValue: msg.value}));
343             }.bind(this));
344           }
345         } else {
346           if (DEBUG) debug("no observers stored!");
347         }
348         break;
349       default:
350         if (DEBUG) debug("Wrong message: " + aMessage.name);
351     }
352   },
354   addObserver: function addObserver(aName, aCallback) {
355     if (DEBUG) debug("addObserver " + aName);
356     if (!this._callbacks) {
357       cpmm.sendAsyncMessage("Settings:RegisterForMessages");
358       this._callbacks = {};
359     }
360     if (!this._callbacks[aName]) {
361       this._callbacks[aName] = [aCallback];
362     } else {
363       this._callbacks[aName].push(aCallback);
364     }
365   },
367   removeObserver: function removeObserver(aName, aCallback) {
368     if (DEBUG) debug("deleteObserver " + aName);
369     if (this._callbacks && this._callbacks[aName]) {
370       let index = this._callbacks[aName].indexOf(aCallback)
371       if (index != -1) {
372         this._callbacks[aName].splice(index, 1)
373       } else {
374         if (DEBUG) debug("Callback not found for: " + aName);
375       }
376     } else {
377       if (DEBUG) debug("No observers stored for " + aName);
378     }
379   },
381   init: function(aWindow) {
382     cpmm.addMessageListener("Settings:Change:Return:OK", this);
383     this._window = aWindow;
384     Services.obs.addObserver(this, "inner-window-destroyed", false);
385     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
386     this.innerWindowID = util.currentInnerWindowID;
388     let readPerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-read");
389     let writePerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-write");
390     this.hasReadPrivileges = readPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
391     this.hasWritePrivileges = writePerm == Ci.nsIPermissionManager.ALLOW_ACTION;
393     if (!this.hasReadPrivileges && !this.hasWritePrivileges) {
394       Cu.reportError("NO SETTINGS PERMISSION FOR: " + aWindow.document.nodePrincipal.origin + "\n");
395       return null;
396     }
397   },
399   observe: function(aSubject, aTopic, aData) {
400     if (DEBUG) debug("Topic: " + aTopic);
401     if (aTopic == "inner-window-destroyed") {
402       let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
403       if (wId == this.innerWindowID) {
404         Services.obs.removeObserver(this, "inner-window-destroyed");
405         cpmm.removeMessageListener("Settings:Change:Return:OK", this);
406         this._requests = null;
407         this._window = null;
408         this._innerWindowID = null;
409         this._onsettingchange = null;
410         this._settingsDB.close();
411       }
412     }
413   },
415   classID : SETTINGSMANAGER_CID,
416   QueryInterface : XPCOMUtils.generateQI([nsIDOMSettingsManager, Ci.nsIDOMGlobalPropertyInitializer]),
418   classInfo : XPCOMUtils.generateCI({classID: SETTINGSMANAGER_CID,
419                                      contractID: SETTINGSMANAGER_CONTRACTID,
420                                      classDescription: "SettingsManager",
421                                      interfaces: [nsIDOMSettingsManager],
422                                      flags: nsIClassInfo.DOM_OBJECT})
425 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock])