Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
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])