Bumping gaia.json for 1 gaia revision(s) a=gaia-bump
[gecko.git] / dom / settings / SettingsRequestManager.jsm
blob525434c172a17669b86cca800dc86fc1d1274180
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 Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
11 this.EXPORTED_SYMBOLS = ["SettingsRequestManager"];
13 Cu.import("resource://gre/modules/SettingsDB.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
15 Cu.import("resource://gre/modules/Services.jsm");
16 Cu.import("resource://gre/modules/PermissionsTable.jsm");
18 let DEBUG = false;
19 let VERBOSE = false;
21 try {
22   DEBUG   =
23     Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.debug.enabled");
24   VERBOSE =
25     Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.verbose.enabled");
26 } catch (ex) { }
28 function debug(s) {
29   dump("-*- SettingsRequestManager: " + s + "\n");
32 const kXpcomShutdownObserverTopic      = "xpcom-shutdown";
33 const kInnerWindowDestroyed            = "inner-window-destroyed";
34 const kMozSettingsChangedObserverTopic = "mozsettings-changed";
35 const kSettingsReadSuffix              = "-read";
36 const kSettingsWriteSuffix             = "-write";
37 const kSettingsClearPermission         = "settings-clear";
38 const kAllSettingsReadPermission       = "settings" + kSettingsReadSuffix;
39 const kAllSettingsWritePermission      = "settings" + kSettingsWriteSuffix;
40 // Any application with settings permissions, be it for all settings
41 // or a single one, will need to be able to access the settings API.
42 // The settings-api permission allows an app to see the mozSettings
43 // API in order to create locks and queue tasks. Whether these tasks
44 // will be allowed depends on the exact permissions the app has.
45 const kSomeSettingsReadPermission      = "settings-api" + kSettingsReadSuffix;
46 const kSomeSettingsWritePermission     = "settings-api" + kSettingsWriteSuffix;
48 XPCOMUtils.defineLazyServiceGetter(this, "mrm",
49                                    "@mozilla.org/memory-reporter-manager;1",
50                                    "nsIMemoryReporterManager");
51 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
52                                    "@mozilla.org/parentprocessmessagemanager;1",
53                                    "nsIMessageBroadcaster");
54 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
55                                    "@mozilla.org/uuid-generator;1",
56                                    "nsIUUIDGenerator");
58 let SettingsPermissions = {
59   checkPermission: function(aPrincipal, aPerm) {
60     if (!aPrincipal) {
61       Cu.reportError("SettingsPermissions.checkPermission was passed a null principal. Denying all permissions.");
62       return false;
63     }
64     if (aPrincipal.origin == "[System Principal]" ||
65         Services.perms.testExactPermissionFromPrincipal(aPrincipal, aPerm) == Ci.nsIPermissionManager.ALLOW_ACTION) {
66       return true;
67     }
68     return false;
69   },
70   hasAllReadPermission: function(aPrincipal) {
71     return this.checkPermission(aPrincipal, kAllSettingsReadPermission);
72   },
73   hasAllWritePermission: function(aPrincipal) {
74     return this.checkPermission(aPrincipal, kAllSettingsWritePermission);
75   },
76   hasSomeReadPermission: function(aPrincipal) {
77     return this.checkPermission(aPrincipal, kSomeSettingsReadPermission);
78   },
79   hasSomeWritePermission: function(aPrincipal) {
80     return this.checkPermission(aPrincipal, kSomeSettingsWritePermission);
81   },
82   hasClearPermission: function(aPrincipal) {
83     return this.checkPermission(aPrincipal, kSettingsClearPermission);
84   },
85   hasReadPermission: function(aPrincipal, aSettingsName) {
86     return this.hasAllReadPermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsReadSuffix);
87   },
88   hasWritePermission: function(aPrincipal, aSettingsName) {
89     return this.hasAllWritePermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsWriteSuffix);
90   }
94 function SettingsLockInfo(aDB, aMsgMgr, aPrincipal, aLockID, aIsServiceLock, aWindowID) {
95   return {
96     // ID Shared with the object on the child side
97     lockID: aLockID,
98     // Is this a content lock or a settings service lock?
99     isServiceLock: aIsServiceLock,
100     // Which inner window ID
101     windowID: aWindowID,
102     // Tasks to be run once the lock is at the head of the queue
103     tasks: [],
104     // This is set to true once a transaction is ready to run, but is not at the
105     // head of the lock queue.
106     consumable: false,
107     // Holds values that are requested to be set until the lock lifetime ends,
108     // then commits them to the DB.
109     queuedSets: {},
110     // Internal transaction object
111     _transaction: undefined,
112     // Message manager that controls the lock
113     _mm: aMsgMgr,
114     // If true, it means a permissions check failed, so just fail everything now
115     _failed: false,
116     // If we're slated to run finalize, set this to make sure we don't
117     // somehow run other events afterward.
118     finalizing: false,
119     // Lets us know if we can use this lock for a clear command
120     canClear: true,
121     // Lets us know if this lock has been used to clear at any point.
122     hasCleared: false,
123     // Principal the lock was created under. We assume that the lock
124     // will continue to exist under this principal for the duration of
125     // its lifetime.
126     principal: aPrincipal,
127     getObjectStore: function() {
128       if (VERBOSE) debug("Getting transaction for " + this.lockID);
129       let store;
130       // Test for transaction validity via trying to get the
131       // datastore. If it doesn't work, assume the transaction is
132       // closed, create a new transaction and try again.
133       if (this._transaction) {
134         try {
135           store = this._transaction.objectStore(SETTINGSSTORE_NAME);
136         } catch (e) {
137           if (e.name == "InvalidStateError") {
138             if (VERBOSE) debug("Current transaction for " + this.lockID + " closed, trying to create new one.");
139           } else {
140             if (DEBUG) debug("Unexpected exception, throwing: " + e);
141             throw e;
142           }
143         }
144       }
145       // Create one transaction with a global permission. This may be
146       // slightly slower on apps with full settings permissions, but
147       // it means we don't have to do our own transaction order
148       // bookkeeping.
149       if (!SettingsPermissions.hasSomeWritePermission(this.principal)) {
150         if (VERBOSE) debug("Making READONLY transaction for " + this.lockID);
151         this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly");
152       } else {
153         if (VERBOSE) debug("Making READWRITE transaction for " + this.lockID);
154         this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite");
155       }
156       this._transaction.oncomplete = function() {
157         if (VERBOSE) debug("Transaction for lock " + this.lockID + " closed");
158       }.bind(this);
159       this._transaction.onabort = function () {
160         if (DEBUG) debug("Transaction for lock " + this.lockID + " aborted");
161         this._failed = true;
162       }.bind(this);
163       try {
164         store = this._transaction.objectStore(SETTINGSSTORE_NAME);
165       } catch (e) {
166           if (e.name == "InvalidStateError") {
167             if (DEBUG) debug("Cannot create objectstore on transaction for " + this.lockID);
168             return null;
169           } else {
170             if (DEBUG) debug("Unexpected exception, throwing: " + e);
171             throw e;
172           }
173       }
174       return store;
175     }
176   };
179 let SettingsRequestManager = {
180   // Access to the settings DB
181   settingsDB: new SettingsDB(),
182   // Remote messages to listen for from child
183   messages: ["child-process-shutdown", "Settings:Get", "Settings:Set",
184              "Settings:Clear", "Settings:Run", "Settings:Finalize",
185              "Settings:CreateLock", "Settings:RegisterForMessages"],
186   // Map of LockID to SettingsLockInfo objects
187   lockInfo: {},
188   // Queue of LockIDs. The LockID on the front of the queue is the only lock
189   // that will have requests processed, all other locks will queue requests
190   // until they hit the front of the queue.
191   settingsLockQueue: [],
192   children: [],
193   // Since we need to call observers at times when we may not have
194   // just received a message from a child process, we cache principals
195   // for message managers and check permissions on them before we send
196   // settings notifications to child processes.
197   observerPrincipalCache: new Map(),
198   tasksConsumed: 0,
200   init: function() {
201     if (VERBOSE) debug("init");
202     this.settingsDB.init();
203     this.messages.forEach((function(msgName) {
204       ppmm.addMessageListener(msgName, this);
205     }).bind(this));
206     Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
207     Services.obs.addObserver(this, kInnerWindowDestroyed, false);
208     mrm.registerStrongReporter(this);
209   },
211   _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
212     function needsUUID(aValue) {
213       if (!aValue || !aValue.constructor) {
214         return false;
215       }
216       return (aValue.constructor.name == "Date") || (aValue instanceof Ci.nsIDOMFile) ||
217              (aValue instanceof Ci.nsIDOMBlob);
218     }
219     // We need to serialize settings objects, otherwise they can change between
220     // the set() call and the enqueued request being processed. We can't simply
221     // parse(stringify(obj)) because that breaks things like Blobs, Files and
222     // Dates, so we use stringify's replacer and parse's reviver parameters to
223     // preserve binaries.
224     let binaries = Object.create(null);
225     let stringified = JSON.stringify(aObject, function(key, value) {
226       value = this.settingsDB.prepareValue(value);
227       if (needsUUID(value)) {
228         let uuid = uuidgen.generateUUID().toString();
229         binaries[uuid] = value;
230         return uuid;
231       }
232       return value;
233     }.bind(this));
234     return JSON.parse(stringified, function(key, value) {
235       if (value in binaries) {
236         return binaries[value];
237       }
238       return value;
239     });
240   },
242   queueTask: function(aOperation, aData) {
243     if (VERBOSE) debug("Queueing task: " + aOperation);
245     let defer = {};
247     let lock = this.lockInfo[aData.lockID];
249     if (!lock) {
250       return Promise.reject({error: "Lock already dead, cannot queue task"});
251     }
253     if (aOperation == "set") {
254       aData.settings = this._serializePreservingBinaries(aData.settings);
255     }
257     this.lockInfo[aData.lockID].tasks.push({
258       operation: aOperation,
259       data: aData,
260       defer: defer
261     });
263     let promise = new Promise(function(resolve, reject) {
264       defer.resolve = resolve;
265       defer.reject = reject;
266     });
268     return promise;
269   },
271   // Due to the fact that we're skipping the database in some places
272   // by keeping a local "set" value cache, resolving some calls
273   // without a call to the database would mean we could potentially
274   // receive promise responses out of expected order if a get is
275   // called before a set. Therefore, we wrap our resolve in a null
276   // get, which means it will resolves afer the rest of the calls
277   // queued to the DB.
278   queueTaskReturn: function(aTask, aReturnValue) {
279     if (VERBOSE) debug("Making task queuing transaction request.");
280     let data = aTask.data;
281     let lock = this.lockInfo[data.lockID];
282     let store = lock.getObjectStore(lock.principal);
283     if (!store) {
284       if (DEBUG) debug("Rejecting task queue on lock " + aTask.data.lockID);
285       return Promise.reject({task: aTask, error: "Cannot get object store"});
286     }
287     // Due to the fact that we're skipping the database, resolving
288     // this without a call to the database would mean we could
289     // potentially receive promise responses out of expected order if
290     // a get is called before a set. Therefore, we wrap our resolve in
291     // a null get, which means it will resolves afer the rest of the
292     // calls queued to the DB.
293     let getReq = store.get(0);
295     let defer = {};
296     let promiseWrapper = new Promise(function(resolve, reject) {
297       defer.resolve = resolve;
298       defer.reject = reject;
299     });
301     getReq.onsuccess = function(event) {
302       return defer.resolve(aReturnValue);
303     };
304     getReq.onerror = function() {
305       return defer.reject({task: aTask, error: getReq.error.name});
306     };
307     return promiseWrapper;
308   },
309   
310   taskGet: function(aTask) {
311     if (VERBOSE) debug("Running Get task on lock " + aTask.data.lockID);
313     // Check that we have permissions for getting the value
314     let data = aTask.data;
315     let lock = this.lockInfo[data.lockID];
317     if (!lock) {
318       return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
319     }
320     if (lock._failed) {
321       if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
322       return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
323     }
325     if (lock.hasCleared) {
326       if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
327       return Promise.reject({task: aTask, error: "Lock was used for a clear command. All subsequent requests will fail."});
328     }
330     lock.canClear = false;
331     
332     if (!SettingsPermissions.hasReadPermission(lock.principal, data.name)) {
333       if (DEBUG) debug("get not allowed for " + data.name);
334       lock._failed = true;
335       return Promise.reject({task: aTask, error: "No permission to get " + data.name});
336     }
338     // If the value was set during this transaction, use the cached value
339     if (data.name in lock.queuedSets) {
340       if (VERBOSE) debug("Returning cached set value " + lock.queuedSets[data.name] + " for " + data.name);
341       let local_results = {};
342       local_results[data.name] = lock.queuedSets[data.name];
343       return this.queueTaskReturn(aTask, {task: aTask, results: local_results});
344     }
346     // Create/Get transaction and make request
347     if (VERBOSE) debug("Making get transaction request for " + data.name);
348     let store = lock.getObjectStore(lock.principal);
349     if (!store) {
350       if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID);
351       return Promise.reject({task: aTask, error: "Cannot get object store"});
352     }
354     if (VERBOSE) debug("Making get request for " + data.name);
355     let getReq = (data.name === "*") ? store.mozGetAll() : store.mozGetAll(data.name);
357     let defer = {};
358     let promiseWrapper = new Promise(function(resolve, reject) {
359       defer.resolve = resolve;
360       defer.reject = reject;
361     });
363     getReq.onsuccess = function(event) {
364       if (VERBOSE) debug("Request for '" + data.name + "' successful. " +
365             "Record count: " + event.target.result.length);
367       if (event.target.result.length == 0) {
368         if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + data.name + " is not in the database.\n");
369       }
371       let results = {};
373       for (let i in event.target.result) {
374         let result = event.target.result[i];
375         let name = result.settingName;
376         if (VERBOSE) debug(name + ": " + result.userValue +", " + result.defaultValue);
377         let value = result.userValue !== undefined ? result.userValue : result.defaultValue;
378         results[name] = value;
379       }
380       return defer.resolve({task: aTask, results: results});
381     };
382     getReq.onerror = function() {
383       return defer.reject({task: aTask, error: getReq.error.name});
384     };
385     return promiseWrapper;
386   },
388   taskSet: function(aTask) {
389     let data = aTask.data;
390     let lock = this.lockInfo[data.lockID];
391     let keys = Object.getOwnPropertyNames(data.settings);
393     if (!lock) {
394       return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
395     }
396     if (lock._failed) {
397       if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
398       return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
399     }
401     if (lock.hasCleared) {
402       if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
403       return Promise.reject({task: aTask, error: "Lock was used for a clear command. All other requests will fail."});
404     }
406     lock.canClear = false;
408     // If we have no keys, resolve
409     if (keys.length === 0) {
410       if (DEBUG) debug("No keys to change entered!");
411       return Promise.resolve({task: aTask});
412     }
414     for (let i = 0; i < keys.length; i++) {
415       if (!SettingsPermissions.hasWritePermission(lock.principal, keys[i])) {
416         if (DEBUG) debug("set not allowed on " + keys[i]);
417         lock._failed = true;
418         return Promise.reject({task: aTask, error: "No permission to set " + keys[i]});
419       }
420     }
422     for (let i = 0; i < keys.length; i++) {
423       let key = keys[i];
424       if (VERBOSE) debug("key: " + key + ", val: " + JSON.stringify(data.settings[key]) + ", type: " + typeof(data.settings[key]));
425       lock.queuedSets[key] = data.settings[key];
426     }
428     return this.queueTaskReturn(aTask, {task: aTask});
429   },
431   startRunning: function(aLockID) {
432     let lock = this.lockInfo[aLockID];
434     if (!lock) {
435       if (DEBUG) debug("Lock no longer alive, cannot start running");
436       return;
437     }
439     lock.consumable = true;
440     if (aLockID == this.settingsLockQueue[0] || this.settingsLockQueue.length == 0) {
441       // If a lock is currently at the head of the queue, run all tasks for
442       // it.
443       if (VERBOSE) debug("Start running tasks for " + aLockID);
444       this.queueConsume();
445     } else {
446       // If a lock isn't at the head of the queue, but requests to be run,
447       // simply mark it as consumable, which means it will automatically run
448       // once it comes to the head of the queue.
449       if (VERBOSE) debug("Queuing tasks for " + aLockID + " while waiting for " + this.settingsLockQueue[0]);
450     }
451   },
453   queueConsume: function() {
454     if (this.settingsLockQueue.length > 0 && this.lockInfo[this.settingsLockQueue[0]].consumable) {
455       Services.tm.currentThread.dispatch(SettingsRequestManager.consumeTasks.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
456     }
457   },
459   finalizeSets: function(aTask) {
460     let data = aTask.data;
461     if (VERBOSE) debug("Finalizing tasks for lock " + data.lockID);
462     let lock = this.lockInfo[data.lockID];
464     if (!lock) {
465       return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
466     }
467     lock.finalizing = true;
468     if (lock._failed) {
469       this.removeLock(data.lockID);
470       return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
471     }
472     // If we have cleared, there is no reason to continue finalizing
473     // this lock. Just resolve promise with task and move on.
474     if (lock.hasCleared) {
475       if (VERBOSE) debug("Clear was called on lock, skipping finalize");
476       this.removeLock(data.lockID);
477       return Promise.resolve({task: aTask});
478     }
479     let keys = Object.getOwnPropertyNames(lock.queuedSets);
480     if (keys.length === 0) {
481       if (VERBOSE) debug("Nothing to finalize. Exiting.");
482       this.removeLock(data.lockID);
483       return Promise.resolve({task: aTask});
484     }
486     let store = lock.getObjectStore(lock.principal);
487     if (!store) {
488       if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID);
489       this.removeLock(data.lockID);
490       return Promise.reject({task: aTask, error: "Cannot get object store"});
491     }
493     // Due to the fact there may have multiple set operations to clear, and
494     // they're all async, callbacks are gathered into promises, and the promises
495     // are processed with Promises.all().
496     let checkPromises = [];
497     let finalValues = {};
498     for (let i = 0; i < keys.length; i++) {
499       let key = keys[i];
500       if (VERBOSE) debug("key: " + key + ", val: " + lock.queuedSets[key] + ", type: " + typeof(lock.queuedSets[key]));
501       let checkDefer = {};
502       let checkPromise = new Promise(function(resolve, reject) {
503         checkDefer.resolve = resolve;
504         checkDefer.reject = reject;
505       });
507       // Get operation is used to fill in the default value, assuming there is
508       // one. For the moment, if a value doesn't exist in the settings DB, we
509       // allow the user to add it, and just pass back a null default value.
510       let checkKeyRequest = store.get(key);
511       checkKeyRequest.onsuccess = function (event) {
512         let userValue = lock.queuedSets[key];
513         let defaultValue;
514         if (!event.target.result) {
515           defaultValue = null;
516           if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + key + " is not in the database.\n");
517         } else {
518           defaultValue = event.target.result.defaultValue;
519         }
520         let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue};
521         finalValues[key] = {defaultValue: defaultValue, userValue: userValue};
522         let setReq = store.put(obj);
523         setReq.onsuccess = function() {
524           if (VERBOSE) debug("Set successful!");
525           if (VERBOSE) debug("key: " + key + ", val: " + finalValues[key] + ", type: " + typeof(finalValues[key]));
526           return checkDefer.resolve({task: aTask});
527         };
528         setReq.onerror = function() {
529           return checkDefer.reject({task: aTask, error: setReq.error.name});
530         };
531       }.bind(this);
532       checkKeyRequest.onerror = function(event) {
533         return checkDefer.reject({task: aTask, error: checkKeyRequest.error.name});
534       };
535       checkPromises.push(checkPromise);
536     }
538     let defer = {};
539     let promiseWrapper = new Promise(function(resolve, reject) {
540       defer.resolve = resolve;
541       defer.reject = reject;
542     });
544     // Once all transactions are done, or any have failed, remove the lock and
545     // start processing the tasks from the next lock in the queue.
546     Promise.all(checkPromises).then(function() {
547       // If all commits were successful, notify observers
548       for (let i = 0; i < keys.length; i++) {
549         this.sendSettingsChange(keys[i], finalValues[keys[i]].userValue, lock.isServiceLock);
550       }
551       this.removeLock(data.lockID);
552       defer.resolve({task: aTask});
553     }.bind(this), function(ret) {
554       this.removeLock(data.lockID);
555       defer.reject({task: aTask, error: "Set transaction failure"});
556     }.bind(this));
557     return promiseWrapper;
558   },
560   // Clear is only expected to be called via tests, and if a lock
561   // calls clear, it should be the only thing the lock does. This
562   // allows us to not have to deal with the possibility of query
563   // integrity checking. Clear should never be called in the wild,
564   // even by certified apps, which is why it has its own permission
565   // (settings-clear).
566   taskClear: function(aTask) {
567     if (VERBOSE) debug("Clearing");
568     let data = aTask.data;
569     let lock = this.lockInfo[data.lockID];
571     if (lock._failed) {
572       if (DEBUG) debug("Lock failed, all requests now failing.");
573       return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
574     }
576     if (!lock.canClear) {
577       if (DEBUG) debug("Lock tried to clear after queuing other tasks. Failing.");
578       lock._failed = true;
579       return Promise.reject({task: aTask, error: "Cannot call clear after queuing other tasks, all requests now failing."});
580     }
582     if (!SettingsPermissions.hasClearPermission(lock.principal)) {
583       if (DEBUG) debug("clear not allowed");
584       lock._failed = true;
585       return Promise.reject({task: aTask, error: "No permission to clear DB"});
586     }
588     lock.hasCleared = true;
590     let store = lock.getObjectStore(lock.principal);
591     if (!store) {
592       if (DEBUG) debug("Rejecting Clear task on lock " + aTask.data.lockID);
593       return Promise.reject({task: aTask, error: "Cannot get object store"});
594     }
595     let defer = {};
596     let promiseWrapper = new Promise(function(resolve, reject) {
597       defer.resolve = resolve;
598       defer.reject = reject;
599     });
601     let clearReq = store.clear();
602     clearReq.onsuccess = function() {
603       return defer.resolve({task: aTask});
604     };
605     clearReq.onerror = function() {
606       return defer.reject({task: aTask});
607     };
608     return promiseWrapper;
609   },
611   ensureConnection : function() {
612     if (VERBOSE) debug("Ensuring Connection");
613     let defer = {};
614     let promiseWrapper = new Promise(function(resolve, reject) {
615       defer.resolve = resolve;
616       defer.reject = reject;
617     });
618     this.settingsDB.ensureDB(
619       function() { defer.resolve(); },
620       function(error) {
621         if (DEBUG) debug("Cannot open Settings DB. Trying to open an old version?\n");
622         defer.reject(error);
623       }
624     );
625     return promiseWrapper;
626   },
628   runTasks: function(aLockID) {
629     if (VERBOSE) debug("Running tasks for " + aLockID);
630     let lock = this.lockInfo[aLockID];
631     if (!lock) {
632       if (DEBUG) debug("Lock no longer alive, cannot run tasks");
633       return;
634     }
635     let currentTask = lock.tasks.shift();
636     let promises = [];
637     while (currentTask) {
638       if (VERBOSE) debug("Running Operation " + currentTask.operation);
639       if (lock.finalizing) {
640         // We should really never get to this point, but if we do,
641         // fail every task that happens.
642         Cu.reportError("Settings lock " + aLockID + " trying to run task '" + currentTask.operation + "' after finalizing. Ignoring tasks, but this is bad. Lock: " + aLockID);
643         currentTask.defer.reject("Cannot call new task after finalizing");
644       } else {
645       let p;
646       this.tasksConsumed++;
647       switch (currentTask.operation) {
648         case "get":
649           p = this.taskGet(currentTask);
650           break;
651         case "set":
652           p = this.taskSet(currentTask);
653           break;
654         case "clear":
655           p = this.taskClear(currentTask);
656           break;
657         case "finalize":
658           p = this.finalizeSets(currentTask);
659           break;
660         default:
661           if (DEBUG) debug("Invalid operation: " + currentTask.operation);
662           p.reject("Invalid operation: " + currentTask.operation);
663       }
664       p.then(function(ret) {
665         ret.task.defer.resolve("results" in ret ? ret.results : null);
666       }.bind(currentTask), function(ret) {
667         ret.task.defer.reject(ret.error);
668       });
669       promises.push(p);
670       }
671       currentTask = lock.tasks.shift();
672     }
673   },
675   consumeTasks: function() {
676     if (this.settingsLockQueue.length == 0) {
677       if (VERBOSE) debug("Nothing to run!");
678       return;
679     }
681     let lockID = this.settingsLockQueue[0];
682     if (VERBOSE) debug("Consuming tasks for " + lockID);
683     let lock = this.lockInfo[lockID];
685     // If a process dies, we should clean up after it via the
686     // child-process-shutdown event. But just in case we don't, we want to make
687     // sure we never block on consuming.
688     if (!lock) {
689       if (DEBUG) debug("Lock no longer alive, cannot consume tasks");
690       this.queueConsume();
691       return;
692     }
694     if (!lock.consumable || lock.tasks.length === 0) {
695       if (VERBOSE) debug("No more tasks to run or not yet consuamble.");
696       return;
697     }
699     lock.consumable = false;
700     this.ensureConnection().then(
701       function(task) {
702         this.runTasks(lockID);
703       }.bind(this), function(ret) {
704         dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n");
705     });
706   },
708   observe: function(aSubject, aTopic, aData) {
709     if (VERBOSE) debug("observe: " + aTopic);
710     switch (aTopic) {
711       case kXpcomShutdownObserverTopic:
712         this.messages.forEach((function(msgName) {
713           ppmm.removeMessageListener(msgName, this);
714         }).bind(this));
715         Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
716         ppmm = null;
717         mrm.unregisterStrongReporter(this);
718         break;
720       case kInnerWindowDestroyed:
721         let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
722         this.forceFinalizeChildLocksNonOOP(wId);
723         break;
725       default:
726         if (DEBUG) debug("Wrong observer topic: " + aTopic);
727         break;
728     }
729   },
731   collectReports: function(aCallback, aData, aAnonymize) {
732     for (let lockId of Object.keys(this.lockInfo)) {
733       let lock = this.lockInfo[lockId];
734       let length = lock.tasks.length;
736       if (length === 0) {
737         continue;
738       }
740       let path = "settings-locks/tasks/queue-length(id=" + lockId + ")";
742       aCallback.callback("", path,
743                          Ci.nsIMemoryReporter.KIND_OTHER,
744                          Ci.nsIMemoryReporter.UNITS_COUNT,
745                          length,
746                          "Tasks queue length for this lock",
747                          aData);
748     }
750     aCallback.callback("",
751                        "settings-locks/tasks/processed",
752                        Ci.nsIMemoryReporter.KIND_OTHER,
753                        Ci.nsIMemoryReporter.UNITS_COUNT,
754                        this.tasksConsumed,
755                        "The number of tasks that were executed.",
756                        aData);
757   },
759   sendSettingsChange: function(aKey, aValue, aIsServiceLock) {
760     this.broadcastMessage("Settings:Change:Return:OK",
761       { key: aKey, value: aValue });
762     Services.obs.notifyObservers(this, kMozSettingsChangedObserverTopic,
763       JSON.stringify({
764         key: aKey,
765         value: aValue,
766         isInternalChange: aIsServiceLock
767       }));
768   },
770   broadcastMessage: function broadcastMessage(aMsgName, aContent) {
771     if (VERBOSE) debug("Broadcast");
772     this.children.forEach(function(msgMgr) {
773       let principal = this.observerPrincipalCache.get(msgMgr);
774       if (!principal) {
775         if (DEBUG) debug("Cannot find principal for message manager to check permissions");
776       }
777       else if (SettingsPermissions.hasReadPermission(principal, aContent.key)) {
778         try {
779           msgMgr.sendAsyncMessage(aMsgName, aContent);
780         } catch (e) {
781           if (DEBUG) debug("Failed sending message: " + aMsgName);
782         }
783       }
784     }.bind(this));
785     if (VERBOSE) debug("Finished Broadcasting");
786   },
788   addObserver: function(aMsgMgr, aPrincipal) {
789     if (VERBOSE) debug("Add observer for " + aPrincipal.origin);
790     if (this.children.indexOf(aMsgMgr) == -1) {
791       this.children.push(aMsgMgr);
792       this.observerPrincipalCache.set(aMsgMgr, aPrincipal);
793     }
794   },
796   removeObserver: function(aMsgMgr) {
797     if (VERBOSE) {
798       let principal = this.observerPrincipalCache.get(aMsgMgr);
799       if (principal) {
800         debug("Remove observer for " + principal.origin);
801       }
802     }
803     let index = this.children.indexOf(aMsgMgr);
804     if (index != -1) {
805       this.children.splice(index, 1);
806       this.observerPrincipalCache.delete(aMsgMgr);
807     }
808     if (VERBOSE) debug("Principal/MessageManager pairs left in observer cache: " + this.observerPrincipalCache.size);
809   },
811   removeLock: function(aLockID) {
812     if (VERBOSE) debug("Removing lock " + aLockID);
813     if (this.lockInfo[aLockID]) {
814     let transaction = this.lockInfo[aLockID]._transaction;
815     if (transaction) {
816       try {
817         transaction.abort();
818       } catch (e) {
819         if (e.name == "InvalidStateError") {
820           if (VERBOSE) debug("Transaction for " + aLockID + " closed already");
821         } else {
822           if (DEBUG) debug("Unexpected exception, throwing: " + e);
823           throw e;
824         }
825       }
826     }
827     delete this.lockInfo[aLockID];
828     }
829     let index = this.settingsLockQueue.indexOf(aLockID);
830     if (index > -1) {
831       this.settingsLockQueue.splice(index, 1);
832     }
833     // If index is 0, the lock we just removed was at the head of
834     // the queue, so possibly queue the next lock if it's
835     // consumable.
836     if (index == 0) {
837       this.queueConsume();
838     }
839   },
841   hasLockFinalizeTask: function(lock) {
842     // Go in reverse order because finalize should be the last one
843     for (let task_index = lock.tasks.length; task_index >= 0; task_index--) {
844       if (lock.tasks[task_index]
845           && lock.tasks[task_index].operation === "finalize") {
846         return true;
847       }
848     }
849     return false;
850   },
852   enqueueForceFinalize: function(lock) {
853     if (!this.hasLockFinalizeTask(lock)) {
854       if (VERBOSE) debug("Alive lock has pending tasks: " + lock.lockID);
855       this.queueTask("finalize", {lockID: lock.lockID}).then(
856         function() {
857           if (VERBOSE) debug("Alive lock " + lock.lockID + " succeeded to force-finalize");
858         },
859         function(error) {
860           if (DEBUG) debug("Alive lock " + lock.lockID + " failed to force-finalize due to error: " + error);
861         }
862       );
863       // Finalize is considered a task running situation, but it also needs to
864       // queue a task.
865       this.startRunning(lock.lockID);
866     }
867   },
869   forceFinalizeChildLocksNonOOP: function(windowId) {
870     if (VERBOSE) debug("Forcing finalize on child locks, non OOP");
872     for (let lockId of Object.keys(this.lockInfo)) {
873       let lock = this.lockInfo[lockId];
874       if (lock.windowID === windowId) {
875         this.enqueueForceFinalize(lock);
876       }
877     }
878   },
880   forceFinalizeChildLocksOOP: function(aMsgMgr) {
881     if (VERBOSE) debug("Forcing finalize on child locks, OOP");
883     for (let lockId of Object.keys(this.lockInfo)) {
884       let lock = this.lockInfo[lockId];
885       if (lock._mm === aMsgMgr) {
886         this.enqueueForceFinalize(lock);
887       }
888     }
889   },
891   receiveMessage: function(aMessage) {
892     if (VERBOSE) debug("receiveMessage " + aMessage.name + ": " + JSON.stringify(aMessage.data));
894     let msg = aMessage.data;
895     let mm = aMessage.target;
897     function returnMessage(name, data) {
898       try {
899         mm.sendAsyncMessage(name, data);
900       } catch (e) {
901         if (DEBUG) debug("Return message failed, " + name);
902       }
903     }
905     // For all message types that expect a lockID, we check to make
906     // sure that we're accessing a lock that's part of our process. If
907     // not, consider it a security violation and kill the app. Killing
908     // based on creating a colliding lock ID happens as part of
909     // CreateLock check below.
910     switch (aMessage.name) {
911       case "Settings:Get":
912       case "Settings:Set":
913       case "Settings:Clear":
914       case "Settings:Run":
915       case "Settings:Finalize":
916         let kill_process = false;
917         if (!msg.lockID) {
918           Cu.reportError("Process sending request for lock that does not exist. Killing.");
919           kill_process = true;
920         }
921         else if (!this.lockInfo[msg.lockID]) {
922           if (DEBUG) debug("Cannot find lock ID " + msg.lockID);
923           // This doesn't kill, because we can have things that file
924           // finalize, then die, and we may get the observer
925           // notification before we get the IPC messages.
926           return;
927         }
928         else if (mm != this.lockInfo[msg.lockID]._mm) {
929           Cu.reportError("Process trying to access settings lock from another process. Killing.");
930           kill_process = true;
931         }
932         if (kill_process) {
933           // Kill the app by checking for a non-existent permission
934           aMessage.target.assertPermission("message-manager-mismatch-kill");
935           return;
936         }
937       default:
938       break;
939     }
941     switch (aMessage.name) {
942       case "child-process-shutdown":
943         if (VERBOSE) debug("Child process shutdown received.");
944         this.forceFinalizeChildLocksOOP(mm);
945         this.removeObserver(mm);
946         break;
947       case "Settings:RegisterForMessages":
948         if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) {
949           Cu.reportError("Settings message " + aMessage.name +
950                          " from a content process with no 'settings-api-read' privileges.");
951           aMessage.target.assertPermission("message-manager-no-read-kill");
952           return;
953         }
954         this.addObserver(mm, aMessage.principal);
955         break;
956       case "Settings:UnregisterForMessages":
957         this.removeObserver(mm);
958         break;
959       case "Settings:CreateLock":
960         if (VERBOSE) debug("Received CreateLock for " + msg.lockID + " from " + aMessage.principal.origin + " window: " + msg.windowID);
961         // If we try to create a lock ID that collides with one
962         // already in the system, consider it a security violation and
963         // kill.
964         if (msg.lockID in this.settingsLockQueue) {
965           Cu.reportError("Trying to queue a lock with the same ID as an already queued lock. Killing app.");
966           aMessage.target.assertPermission("lock-id-duplicate-kill");
967           return;
968         }
969         this.settingsLockQueue.push(msg.lockID);
970         this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB,
971                                                      mm,
972                                                      aMessage.principal,
973                                                      msg.lockID,
974                                                      msg.isServiceLock,
975                                                      msg.windowID);
976         break;
977       case "Settings:Get":
978         if (VERBOSE) debug("Received getRequest from " + msg.lockID);
979         this.queueTask("get", msg).then(function(settings) {
980             returnMessage("Settings:Get:OK", {
981               lockID: msg.lockID,
982               requestID: msg.requestID,
983               settings: settings
984             });
985           }, function(error) {
986             if (DEBUG) debug("getRequest FAILED " + msg.name);
987             returnMessage("Settings:Get:KO", {
988               lockID: msg.lockID,
989               requestID: msg.requestID,
990               errorMsg: error
991             });
992         });
993         break;
994       case "Settings:Set":
995         if (VERBOSE) debug("Received Set Request from " + msg.lockID);
996         this.queueTask("set", msg).then(function(settings) {
997           returnMessage("Settings:Set:OK", {
998             lockID: msg.lockID,
999             requestID: msg.requestID
1000           });
1001         }, function(error) {
1002           returnMessage("Settings:Set:KO", {
1003             lockID: msg.lockID,
1004             requestID: msg.requestID,
1005             errorMsg: error
1006           });
1007         });
1008         break;
1009       case "Settings:Clear":
1010         if (VERBOSE) debug("Received Clear Request from " + msg.lockID);
1011         this.queueTask("clear", msg).then(function() {
1012           returnMessage("Settings:Clear:OK", {
1013             lockID: msg.lockID,
1014             requestID: msg.requestID
1015           });
1016         }, function(error) {
1017           returnMessage("Settings:Clear:KO", {
1018             lockID: msg.lockID,
1019             requestID: msg.requestID,
1020             errorMsg: error
1021           });
1022         });
1023         break;
1024       case "Settings:Finalize":
1025         if (VERBOSE) debug("Received Finalize");
1026         this.queueTask("finalize", msg).then(function() {
1027           returnMessage("Settings:Finalize:OK", {
1028             lockID: msg.lockID
1029           });
1030         }, function(error) {
1031           returnMessage("Settings:Finalize:KO", {
1032             lockID: msg.lockID,
1033             errorMsg: error
1034           });
1035         });
1036       // YES THIS IS SUPPOSED TO FALL THROUGH. Finalize is considered a task
1037       // running situation, but it also needs to queue a task.
1038       case "Settings:Run":
1039         if (VERBOSE) debug("Received Run");
1040         this.startRunning(msg.lockID);
1041         break;
1042       default:
1043         if (DEBUG) debug("Wrong message: " + aMessage.name);
1044     }
1045   }
1048 SettingsRequestManager.init();