|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const DEBUG = false; |
|
8 function debug(s) { |
|
9 if (DEBUG) dump("-*- SettingsManager: " + s + "\n"); |
|
10 } |
|
11 |
|
12 const Cc = Components.classes; |
|
13 const Ci = Components.interfaces; |
|
14 const Cu = Components.utils; |
|
15 |
|
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"); |
|
21 |
|
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"); |
|
28 |
|
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 } |
|
36 |
|
37 SettingsLock.prototype = { |
|
38 get closed() { |
|
39 return !this._open; |
|
40 }, |
|
41 |
|
42 _wrap: function _wrap(obj) { |
|
43 return Cu.cloneInto(obj, this._settingsManager._window); |
|
44 }, |
|
45 |
|
46 process: function process() { |
|
47 let lock = this; |
|
48 let store = lock._transaction.objectStore(SETTINGSSTORE_NAME); |
|
49 |
|
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); |
|
74 |
|
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 } |
|
84 |
|
85 let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue}; |
|
86 if (DEBUG) debug("store1: " + JSON.stringify(obj)); |
|
87 let setReq = store.put(obj); |
|
88 |
|
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 }; |
|
101 |
|
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); |
|
118 |
|
119 getReq.onsuccess = function(event) { |
|
120 if (DEBUG) debug("Request for '" + info.name + "' successful. " + |
|
121 "Record count: " + event.target.result.length); |
|
122 |
|
123 if (event.target.result.length == 0) { |
|
124 if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + info.name + " is not in the database.\n"); |
|
125 } |
|
126 |
|
127 let results = {}; |
|
128 |
|
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 } |
|
136 |
|
137 this._open = true; |
|
138 Services.DOMRequest.fireSuccess(request, this._wrap(results)); |
|
139 this._open = false; |
|
140 }.bind(lock); |
|
141 |
|
142 getReq.onerror = function() { |
|
143 Services.DOMRequest.fireError(request, 0) |
|
144 }; |
|
145 break; |
|
146 } |
|
147 } |
|
148 }, |
|
149 |
|
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 }, |
|
169 |
|
170 get: function get(aName) { |
|
171 if (!this._open) { |
|
172 dump("Settings lock not open!\n"); |
|
173 throw Components.results.NS_ERROR_ABORT; |
|
174 } |
|
175 |
|
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 }, |
|
186 |
|
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 }, |
|
213 |
|
214 set: function set(aSettings) { |
|
215 if (!this._open) { |
|
216 throw "Settings lock not open"; |
|
217 } |
|
218 |
|
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 }, |
|
231 |
|
232 clear: function clear() { |
|
233 if (!this._open) { |
|
234 throw "Settings lock not open"; |
|
235 } |
|
236 |
|
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 }, |
|
247 |
|
248 classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"), |
|
249 contractID: "@mozilla.org/settingsLock;1", |
|
250 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), |
|
251 }; |
|
252 |
|
253 function SettingsManager() { |
|
254 this._locks = new Queue(); |
|
255 this._settingsDB = new SettingsDB(); |
|
256 this._settingsDB.init(); |
|
257 } |
|
258 |
|
259 SettingsManager.prototype = { |
|
260 _callbacks: null, |
|
261 |
|
262 _wrap: function _wrap(obj) { |
|
263 return Cu.cloneInto(obj, this._window); |
|
264 }, |
|
265 |
|
266 nextTick: function nextTick(aCallback, thisObj) { |
|
267 if (thisObj) |
|
268 aCallback = aCallback.bind(thisObj); |
|
269 |
|
270 Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); |
|
271 }, |
|
272 |
|
273 set onsettingchange(aHandler) { |
|
274 this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler); |
|
275 }, |
|
276 |
|
277 get onsettingchange() { |
|
278 return this.__DOM_IMPL__.getEventHandler("onsettingchange"); |
|
279 }, |
|
280 |
|
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 }, |
|
292 |
|
293 receiveMessage: function(aMessage) { |
|
294 if (DEBUG) debug("Settings::receiveMessage: " + aMessage.name); |
|
295 let msg = aMessage.json; |
|
296 |
|
297 switch (aMessage.name) { |
|
298 case "Settings:Change:Return:OK": |
|
299 if (DEBUG) debug('data:' + msg.key + ':' + msg.value + '\n'); |
|
300 |
|
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); |
|
306 |
|
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 }, |
|
320 |
|
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 }, |
|
333 |
|
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 }, |
|
347 |
|
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; |
|
355 |
|
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; |
|
360 |
|
361 if (this.hasReadPrivileges) { |
|
362 cpmm.sendAsyncMessage("Settings:RegisterForMessages"); |
|
363 } |
|
364 |
|
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 }, |
|
370 |
|
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 }, |
|
380 |
|
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 } |
|
387 |
|
388 let path; |
|
389 if (length < 20) { |
|
390 path = "settings-observers"; |
|
391 } else { |
|
392 path = "settings-observers-suspect/referent(topic=" + topic + ")"; |
|
393 } |
|
394 |
|
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 }, |
|
403 |
|
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 }, |
|
413 |
|
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 }; |
|
421 |
|
422 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]) |