From 837c4a980c59ddee2051dc6ca8a31b827101ea7f Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Tue, 19 Aug 2014 14:49:02 -0700 Subject: [PATCH] Backed out 8 changesets (bug 1015518, bug 900551, bug 1011518, bug 846200) for build bustage Backed out changeset 59eb7edaa6ed (bug 1015518) Backed out changeset 04a531d5647e (bug 1011518) Backed out changeset 9ac939654549 (bug 1011518) Backed out changeset 2f707489d892 (bug 900551) Backed out changeset f2a0a448e563 (bug 900551) Backed out changeset f130f54ea09a (bug 900551) Backed out changeset 9d38e8901847 (bug 846200) Backed out changeset 744da032a2de (bug 846200) --- b2g/chrome/content/shell.js | 2 +- dom/apps/src/PermissionsInstaller.jsm | 14 + dom/apps/src/PermissionsTable.jsm | 39 +- dom/base/Navigator.cpp | 4 +- .../test/test_all_synthetic_events.html | 4 - dom/permission/tests/unit/test_bug808734.js | 1 - dom/settings/SettingsChangeNotifier.jsm | 123 +++ dom/settings/SettingsManager.js | 399 ++++---- dom/settings/SettingsRequestManager.jsm | 912 ------------------ dom/settings/SettingsService.js | 326 ++++--- dom/settings/moz.build | 3 +- dom/settings/tests/mochitest.ini | 1 - dom/settings/tests/test_settings_basics.html | 62 +- dom/settings/tests/test_settings_blobs.html | 5 +- .../tests/test_settings_data_uris.html | 5 +- dom/settings/tests/test_settings_events.html | 10 +- .../tests/test_settings_navigator_object.html | 5 +- .../tests/test_settings_onsettingchange.html | 16 +- .../tests/test_settings_permissions.html | 203 ---- dom/settings/tests/test_settings_service.js | 19 +- dom/system/gonk/RadioInterfaceLayer.js | 14 +- .../mochitest/general/test_interfaces.html | 2 - .../geolocation/test_mozsettings.html | 8 +- .../geolocation/test_mozsettingsWatch.html | 8 +- dom/webidl/MozSettingsTransactionEvent.webidl | 17 - dom/webidl/SettingsManager.webidl | 4 +- dom/webidl/moz.build | 1 - dom/wifi/WifiWorker.js | 8 +- 28 files changed, 599 insertions(+), 1616 deletions(-) create mode 100644 dom/settings/SettingsChangeNotifier.jsm delete mode 100644 dom/settings/SettingsRequestManager.jsm delete mode 100644 dom/settings/tests/test_settings_permissions.html delete mode 100644 dom/webidl/MozSettingsTransactionEvent.webidl diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index cd5009c67833..9eb3444fddce 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -5,7 +5,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ Cu.import('resource://gre/modules/ContactService.jsm'); -Cu.import('resource://gre/modules/SettingsRequestManager.jsm'); +Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm'); Cu.import('resource://gre/modules/DataStoreChangeNotifier.jsm'); Cu.import('resource://gre/modules/AlarmService.jsm'); Cu.import('resource://gre/modules/ActivitiesService.jsm'); diff --git a/dom/apps/src/PermissionsInstaller.jsm b/dom/apps/src/PermissionsInstaller.jsm index 69ae5c1bd86a..015b34653bf4 100644 --- a/dom/apps/src/PermissionsInstaller.jsm +++ b/dom/apps/src/PermissionsInstaller.jsm @@ -30,6 +30,20 @@ function debug(aMsg) { //dump("-*-*- PermissionsInstaller.jsm : " + aMsg + "\n"); } +// An array carring all the possible (expanded) permission names. +let AllPossiblePermissions = []; +for (let permName in PermissionsTable) { + let expandedPermNames = []; + if (PermissionsTable[permName].access) { + expandedPermNames = expandPermissions(permName, READWRITE); + } else { + expandedPermNames = expandPermissions(permName); + } + AllPossiblePermissions = AllPossiblePermissions.concat(expandedPermNames); + AllPossiblePermissions = + AllPossiblePermissions.concat(["offline-app", "pin-app"]); +} + this.PermissionsInstaller = { /** * Install permissisions or remove deprecated permissions upon re-install. diff --git a/dom/apps/src/PermissionsTable.jsm b/dom/apps/src/PermissionsTable.jsm index 8dfe4eefe054..2b09c8277f63 100644 --- a/dom/apps/src/PermissionsTable.jsm +++ b/dom/apps/src/PermissionsTable.jsm @@ -12,8 +12,7 @@ this.EXPORTED_SYMBOLS = [ "PermissionsReverseTable", "expandPermissions", "appendAccessToPermName", - "isExplicitInPermissionsTable", - "AllPossiblePermissions" + "isExplicitInPermissionsTable" ]; // Permission access flags @@ -145,16 +144,7 @@ this.PermissionsTable = { geolocation: { privileged: DENY_ACTION, certified: ALLOW_ACTION, access: ["read", "write"], - additional: ["indexedDB-chrome-settings", "settings-api"] - }, - // This exists purely for tests, no app - // should ever use it. It can only be - // handed out by SpecialPowers. - "settings-clear": { - app: DENY_ACTION, - privileged: DENY_ACTION, - certified: DENY_ACTION, - additional: ["indexedDB-chrome-settings", "settings-api"] + additional: ["indexedDB-chrome-settings"] }, permissions: { app: DENY_ACTION, @@ -393,13 +383,6 @@ this.PermissionsTable = { geolocation: { privileged: PROMPT_ACTION, certified: ALLOW_ACTION, substitute: ["firefox-accounts"] - }, - "settings:wallpaper.image": { - app: DENY_ACTION, - privileged: PROMPT_ACTION, - certified: ALLOW_ACTION, - access: ["read", "write"], - additional: ["settings-api"] } }; @@ -508,10 +491,7 @@ this.expandPermissions = function expandPermissions(aPermName, aAccess) { return expandedPermNames; }; -this.PermissionsReverseTable = {}; -this.AllPossiblePermissions = []; - -(function () { +this.PermissionsReverseTable = (function () { // PermissionsTable as it is works well for direct searches, but not // so well for reverse ones (that is, if I get something like // device-storage:music-read or indexedDB-chrome-settings-read how @@ -519,9 +499,8 @@ this.AllPossiblePermissions = []; // born. The idea is that // reverseTable[device-storage:music-read] should return // device-storage:music - // - // We also need a list of all the possible permissions for things like the - // settingsmanager, so construct that while we're at it. + let reverseTable = {}; + for (let permName in PermissionsTable) { let permAliases; if (PermissionsTable[permName].access) { @@ -530,12 +509,12 @@ this.AllPossiblePermissions = []; permAliases = expandPermissions(permName); } for (let i = 0; i < permAliases.length; i++) { - PermissionsReverseTable[permAliases[i]] = permName; - AllPossiblePermissions.push(permAliases[i]); + reverseTable[permAliases[i]] = permName; } } - AllPossiblePermissions = - AllPossiblePermissions.concat(["offline-app", "pin-app"]); + + return reverseTable; + })(); this.isExplicitInPermissionsTable = function(aPermName, aIntStatus) { diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index dcbafddd6693..c39aafc1808d 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -2109,8 +2109,8 @@ Navigator::DoNewResolve(JSContext* aCx, JS::Handle aObject, } if (name.EqualsLiteral("mozSettings")) { - bool hasPermission = CheckPermission("settings-api-read") || - CheckPermission("settings-api-write"); + bool hasPermission = CheckPermission("settings-read") || + CheckPermission("settings-write"); if (!hasPermission) { FillPropertyDescriptor(aDesc, aObject, JS::NullValue(), false); return true; diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html index 92d88b40d2e1..92bbbda8ea6c 100644 --- a/dom/events/test/test_all_synthetic_events.html +++ b/dom/events/test/test_all_synthetic_events.html @@ -277,10 +277,6 @@ const kEventConstructors = { return new MozSettingsEvent(aName, aProps); }, }, - MozSettingsTransactionEvent: { create: function (aName, aProps) { - return new MozSettingsTransactionEvent(aName, aProps); - }, - }, MozSmsEvent: { create: function (aName, aProps) { return new MozSmsEvent(aName, aProps); }, diff --git a/dom/permission/tests/unit/test_bug808734.js b/dom/permission/tests/unit/test_bug808734.js index f422c3ca6420..4d8446ffc89c 100644 --- a/dom/permission/tests/unit/test_bug808734.js +++ b/dom/permission/tests/unit/test_bug808734.js @@ -15,7 +15,6 @@ var gData = [ permission: "settings", access: READWRITE, expected: ["settings-read", "settings-write", - "settings-api-read", "settings-api-write", "indexedDB-chrome-settings-read", "indexedDB-chrome-settings-write"] }, diff --git a/dom/settings/SettingsChangeNotifier.jsm b/dom/settings/SettingsChangeNotifier.jsm new file mode 100644 index 000000000000..c38e2225ddc8 --- /dev/null +++ b/dom/settings/SettingsChangeNotifier.jsm @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict" + +const DEBUG = false; +function debug(s) { + if (DEBUG) dump("-*- SettingsChangeNotifier: " + s + "\n"); +} + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +this.EXPORTED_SYMBOLS = ["SettingsChangeNotifier"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const kXpcomShutdownObserverTopic = "xpcom-shutdown"; +const kMozSettingsChangedObserverTopic = "mozsettings-changed"; +const kFromSettingsChangeNotifier = "fromSettingsChangeNotifier"; + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +this.SettingsChangeNotifier = { + init: function() { + if (DEBUG) debug("init"); + this.children = []; + this._messages = ["Settings:Changed", "Settings:RegisterForMessages", "child-process-shutdown"]; + this._messages.forEach((function(msgName) { + ppmm.addMessageListener(msgName, this); + }).bind(this)); + + Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); + Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); + }, + + observe: function(aSubject, aTopic, aData) { + if (DEBUG) debug("observe"); + switch (aTopic) { + case kXpcomShutdownObserverTopic: + this._messages.forEach((function(msgName) { + ppmm.removeMessageListener(msgName, this); + }).bind(this)); + Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); + Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); + ppmm = null; + break; + case kMozSettingsChangedObserverTopic: + { + let setting = JSON.parse(aData); + // To avoid redundantly broadcasting settings-changed events that are + // just requested from content processes themselves, skip the observer + // messages that are notified from the internal SettingsChangeNotifier. + if (setting.message && setting.message === kFromSettingsChangeNotifier) + return; + this.broadcastMessage("Settings:Change:Return:OK", + { key: setting.key, value: setting.value }); + break; + } + default: + if (DEBUG) debug("Wrong observer topic: " + aTopic); + break; + } + }, + + broadcastMessage: function broadcastMessage(aMsgName, aContent) { + if (DEBUG) debug("Broadast"); + this.children.forEach(function(msgMgr) { + msgMgr.sendAsyncMessage(aMsgName, aContent); + }); + }, + + receiveMessage: function(aMessage) { + if (DEBUG) debug("receiveMessage"); + let msg = aMessage.data; + let mm = aMessage.target; + switch (aMessage.name) { + case "Settings:Changed": + if (!aMessage.target.assertPermission("settings-write")) { + Cu.reportError("Settings message " + msg.name + + " from a content process with no 'settings-write' privileges."); + return null; + } + this.broadcastMessage("Settings:Change:Return:OK", + { key: msg.key, value: msg.value }); + Services.obs.notifyObservers(this, kMozSettingsChangedObserverTopic, + JSON.stringify({ + key: msg.key, + value: msg.value, + message: kFromSettingsChangeNotifier + })); + break; + case "Settings:RegisterForMessages": + if (!aMessage.target.assertPermission("settings-read")) { + Cu.reportError("Settings message " + msg.name + + " from a content process with no 'settings-read' privileges."); + return null; + } + if (DEBUG) debug("Register!"); + if (this.children.indexOf(mm) == -1) { + this.children.push(mm); + } + break; + case "child-process-shutdown": + if (DEBUG) debug("Unregister"); + let index; + if ((index = this.children.indexOf(mm)) != -1) { + if (DEBUG) debug("Unregister index: " + index); + this.children.splice(index, 1); + } + break; + default: + if (DEBUG) debug("Wrong message: " + aMessage.name); + } + } +} + +SettingsChangeNotifier.init(); diff --git a/dom/settings/SettingsManager.js b/dom/settings/SettingsManager.js index 5875345a61e5..d01f58bd2842 100644 --- a/dom/settings/SettingsManager.js +++ b/dom/settings/SettingsManager.js @@ -5,84 +5,42 @@ "use strict"; const DEBUG = false; -function debug(s) { dump("-*- SettingsManager: " + s + "\n"); } +function debug(s) { + if (DEBUG) dump("-*- SettingsManager: " + s + "\n"); +} const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://gre/modules/SettingsQueue.jsm"); +Cu.import("resource://gre/modules/SettingsDB.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); -XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest", - "@mozilla.org/dom/dom-request-service;1", - "nsIDOMRequestService"); XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); XPCOMUtils.defineLazyServiceGetter(this, "mrm", "@mozilla.org/memory-reporter-manager;1", "nsIMemoryReporterManager"); -XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", - "@mozilla.org/uuid-generator;1", - "nsIUUIDGenerator"); - -/** - * In order to make SettingsManager work with Privileged Apps, we need the lock - * to be OOP. However, the lock state needs to be managed on the child process, - * while the IDB functions now happen on the parent process so we don't have to - * expose IDB permissions at the child process level. We use the - * DOMRequestHelper mechanism to deal with DOMRequests/promises across the - * processes. - * - * However, due to the nature of the IDBTransaction lifetime, we need to relay - * to the parent when to finalize the transaction once the child is done with the - * lock. We keep a list of all open requests for a lock, and once the lock - * reaches the end of its receiveMessage function with no more queued requests, - * we consider it dead. At that point, we send a message to the parent to notify - * it to finalize the transaction. - */ function SettingsLock(aSettingsManager) { - if (DEBUG) debug("settings lock init"); this._open = true; + this._isBusy = false; + this._requests = new Queue(); this._settingsManager = aSettingsManager; - this._id = uuidgen.generateUUID().toString(); + this._transaction = null; let closeHelper = function() { - if (DEBUG) debug("closing lock " + this._id); + if (DEBUG) debug("closing lock"); this._open = false; - this.runOrFinalizeQueries(); }.bind(this); - // DOMRequestIpcHelper.initHelper sets this._window - this.initDOMRequestHelper(this._settingsManager._window, ["Settings:Get:OK", "Settings:Get:KO", - "Settings:Clear:OK", "Settings:Clear:KO", - "Settings:Set:OK", "Settings:Set:KO", - "Settings:Finalize:OK", "Settings:Finalize:KO"]); - this.sendMessage("Settings:CreateLock", {lockID: this._id, isInternalLock: false}); Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL); } SettingsLock.prototype = { - __proto__: DOMRequestIpcHelper.prototype, - set onsettingstransactionsuccess(aHandler) { - this.__DOM_IMPL__.setEventHandler("onsettingstransactionsuccess", aHandler); - }, - - get onsettingstransactionsuccess() { - return this.__DOM_IMPL__.getEventHandler("onsettingstransactionsuccess"); - }, - - set onsettingstransactionfailure(aHandler) { - this.__DOM_IMPL__.setEventHandler("onsettingstransactionfailure", aHandler); - }, - - get onsettingstransactionfailure() { - return this.__DOM_IMPL__.getEventHandler("onsettingstransactionfailure"); - }, - get closed() { return !this._open; }, @@ -91,140 +49,230 @@ SettingsLock.prototype = { return Cu.cloneInto(obj, this._settingsManager._window); }, - sendMessage: function(aMessageName, aData) { - cpmm.sendAsyncMessage(aMessageName, - aData, - undefined, - this._settingsManager._window.document.nodePrincipal); - }, + process: function process() { + let lock = this; + let store = lock._transaction.objectStore(SETTINGSSTORE_NAME); - runOrFinalizeQueries: function() { - if (!this._requests || Object.keys(this._requests).length == 0) { - this.sendMessage("Settings:Finalize", {lockID: this._id}); + while (!lock._requests.isEmpty()) { + let info = lock._requests.dequeue(); + if (DEBUG) debug("info: " + info.intent); + let request = info.request; + switch (info.intent) { + case "clear": + let clearReq = store.clear(); + clearReq.onsuccess = function() { + this._open = true; + Services.DOMRequest.fireSuccess(request, 0); + this._open = false; + }.bind(lock); + clearReq.onerror = function() { + Services.DOMRequest.fireError(request, 0) + }; + break; + case "set": + let keys = Object.getOwnPropertyNames(info.settings); + if (keys.length) { + lock._isBusy = true; + } + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let last = i === keys.length - 1; + if (DEBUG) debug("key: " + key + ", val: " + JSON.stringify(info.settings[key]) + ", type: " + typeof(info.settings[key])); + let checkKeyRequest = store.get(key); + + checkKeyRequest.onsuccess = function (event) { + let defaultValue; + let userValue = info.settings[key]; + if (event.target.result) { + defaultValue = event.target.result.defaultValue; } else { - this.sendMessage("Settings:Run", {lockID: this._id}); + defaultValue = null; + if (DEBUG) debug("MOZSETTINGS-SET-WARNING: " + key + " is not in the database.\n"); + } + + let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue}; + if (DEBUG) debug("store1: " + JSON.stringify(obj)); + let setReq = store.put(obj); + + setReq.onsuccess = function() { + cpmm.sendAsyncMessage("Settings:Changed", { key: key, value: userValue }); + if (last && !request.error) { + lock._open = true; + Services.DOMRequest.fireSuccess(request, 0); + lock._open = false; + } + }; + + setReq.onerror = function() { + if (!request.error) { + Services.DOMRequest.fireError(request, setReq.error.name) + } + }; + + if (last) { + lock._isBusy = false; + if (!lock._requests.isEmpty()) { + lock.process(); + } + } + }; + checkKeyRequest.onerror = function(event) { + if (!request.error) { + Services.DOMRequest.fireError(request, checkKeyRequest.error.name) + } + }; + } + // Don't break here, instead return. Once the previous requests have + // finished this loop will start again. + return; + case "get": + let getReq = (info.name === "*") ? store.mozGetAll() + : store.mozGetAll(info.name); + + getReq.onsuccess = function(event) { + if (DEBUG) debug("Request for '" + info.name + "' successful. " + + "Record count: " + event.target.result.length); + + if (event.target.result.length == 0) { + if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + info.name + " is not in the database.\n"); + } + + let results = {}; + + for (var i in event.target.result) { + let result = event.target.result[i]; + var name = result.settingName; + if (DEBUG) debug("VAL: " + result.userValue +", " + result.defaultValue + "\n"); + results[name] = result.userValue !== undefined ? result.userValue : result.defaultValue; + } + + this._open = true; + Services.DOMRequest.fireSuccess(request, this._wrap(results)); + this._open = false; + }.bind(lock); + + getReq.onerror = function() { + Services.DOMRequest.fireError(request, 0) + }; + break; + } } }, - receiveMessage: function(aMessage) { - let msg = aMessage.data; - // SettingsRequestManager broadcasts changes to all locks in the child. If - // our lock isn't being addressed, just return. - if (msg.lockID != this._id) { - return; - } + createTransactionAndProcess: function() { + if (DEBUG) debug("database opened, creating transaction"); - // Finalizing a transaction does not return a request ID since we are - // supposed to fire callbacks. - if (!msg.requestID) { - let event; - switch (aMessage.name) { - case "Settings:Finalize:OK": - if (DEBUG) debug("Lock finalize ok!"); - event = new this._window.MozSettingsTransactionEvent("settingstransactionsuccess", {}); - this.__DOM_IMPL__.dispatchEvent(event); - break; - case "Settings:Finalize:KO": - if (DEBUG) debug("Lock finalize failed!"); - event = new this._window.MozSettingsTransactionEvent("settingstransactionfailure", { - error: msg.errorMsg - }); - this.__DOM_IMPL__.dispatchEvent(event); - break; - default: - if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID"); - } - return; - } + let manager = this._settingsManager; + let transactionType = manager.hasWritePrivileges ? "readwrite" : "readonly"; + this._transaction = + manager._settingsDB._db.transaction(SETTINGSSTORE_NAME, transactionType); - let req = this.getRequest(msg.requestID); - if (!req) { - if (DEBUG) debug("Matching request not found."); - return; + this.process(); + }, + + maybeProcess: function() { + if (this._transaction && !this._isBusy) { + this.process(); } - this.removeRequest(msg.requestID); - if (DEBUG) debug("receiveMessage: " + aMessage.name); - switch (aMessage.name) { - case "Settings:Get:OK": - for (let i in msg.settings) { - msg.settings[i] = this._wrap(msg.settings[i]); - } - this._open = true; - Services.DOMRequest.fireSuccess(req.request, this._wrap(msg.settings)); - this._open = false; - break; - case "Settings:Set:OK": - case "Settings:Clear:OK": - this._open = true; - Services.DOMRequest.fireSuccess(req.request, 0); - this._open = false; - break; - case "Settings:Get:KO": - case "Settings:Set:KO": - case "Settings:Clear:KO": - if (DEBUG) debug("error:" + msg.errorMsg); - Services.DOMRequest.fireError(req.request, msg.errorMsg); - break; - default: - if (DEBUG) debug("Wrong message: " + aMessage.name); - } - this.runOrFinalizeQueries(); }, get: function get(aName) { - if (DEBUG) debug("get (" + this._id + "): " + aName); if (!this._open) { dump("Settings lock not open!\n"); throw Components.results.NS_ERROR_ABORT; } - let req = this.createRequest(); - let reqID = this.getRequestId({request: req}); - this.sendMessage("Settings:Get", {requestID: reqID, - lockID: this._id, - name: aName}); + + if (this._settingsManager.hasReadPrivileges) { + let req = Services.DOMRequest.createRequest(this._settingsManager._window); + this._requests.enqueue({ request: req, intent:"get", name: aName }); + this.maybeProcess(); return req; + } else { + if (DEBUG) debug("get not allowed"); + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + _serializePreservingBinaries: function _serializePreservingBinaries(aObject) { + function needsUUID(aValue) { + if (!aValue || !aValue.constructor) { + return false; + } + return (aValue.constructor.name == "Date") || (aValue instanceof Ci.nsIDOMFile) || + (aValue instanceof Ci.nsIDOMBlob); + } + // We need to serialize settings objects, otherwise they can change between + // the set() call and the enqueued request being processed. We can't simply + // parse(stringify(obj)) because that breaks things like Blobs, Files and + // Dates, so we use stringify's replacer and parse's reviver parameters to + // preserve binaries. + let manager = this._settingsManager; + let binaries = Object.create(null); + let stringified = JSON.stringify(aObject, function(key, value) { + value = manager._settingsDB.prepareValue(value); + if (needsUUID(value)) { + let uuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator) + .generateUUID().toString(); + binaries[uuid] = value; + return uuid; + } + return value; + }); + return JSON.parse(stringified, function(key, value) { + if (value in binaries) { + return binaries[value]; + } + return value; + }); }, set: function set(aSettings) { - if (DEBUG) debug("send: " + JSON.stringify(aSettings)); if (!this._open) { throw "Settings lock not open"; } - let req = this.createRequest(); - let reqID = this.getRequestId({request: req}); - this.sendMessage("Settings:Set", {requestID: reqID, - lockID: this._id, - settings: aSettings}); + + if (this._settingsManager.hasWritePrivileges) { + let req = Services.DOMRequest.createRequest(this._settingsManager._window); + if (DEBUG) debug("send: " + JSON.stringify(aSettings)); + let settings = this._serializePreservingBinaries(aSettings); + this._requests.enqueue({request: req, intent: "set", settings: settings}); + this.maybeProcess(); return req; + } else { + if (DEBUG) debug("set not allowed"); + throw "No permission to call set"; + } }, clear: function clear() { - if (DEBUG) if (DEBUG) debug("clear"); if (!this._open) { throw "Settings lock not open"; } - let req = this.createRequest(); - let reqID = this.getRequestId({request: req}); - this.sendMessage("Settings:Clear", {requestID: reqID, - lockID: this._id}); + + if (this._settingsManager.hasWritePrivileges) { + let req = Services.DOMRequest.createRequest(this._settingsManager._window); + this._requests.enqueue({ request: req, intent: "clear"}); + this.maybeProcess(); return req; + } else { + if (DEBUG) debug("clear not allowed"); + throw "No permission to call clear"; + } }, classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"), contractID: "@mozilla.org/settingsLock;1", - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, - Ci.nsIObserver, - Ci.nsISupportsWeakReference]) + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), }; function SettingsManager() { + this._settingsDB = new SettingsDB(); + this._settingsDB.init(); } SettingsManager.prototype = { _callbacks: null, - _isRegistered: false, - _perms: [], _wrap: function _wrap(obj) { return Cu.cloneInto(obj, this._window); @@ -232,7 +280,6 @@ SettingsManager.prototype = { set onsettingchange(aHandler) { this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler); - this.checkMessageRegistration(); }, get onsettingchange() { @@ -240,8 +287,12 @@ SettingsManager.prototype = { }, createLock: function() { - if (DEBUG) debug("creating lock"); - let lock = new SettingsLock(this); + if (DEBUG) debug("get lock!"); + var lock = new SettingsLock(this); + this._settingsDB.ensureDB( + function() { lock.createTransactionAndProcess(); }, + function() { dump("Cannot open Settings DB. Trying to open an old version?\n"); } + ); return lock; }, @@ -273,35 +324,10 @@ SettingsManager.prototype = { } }, - // If we have either observer callbacks or an event handler, - // register for messages from the main thread. Otherwise, if no one - // is listening, unregister to reduce parent load. - checkMessageRegistration: function checkRegistration() { - let handler = this.__DOM_IMPL__.getEventHandler("onsettingchange"); - if (!this._isRegistered) { - if (DEBUG) debug("Registering for messages"); - cpmm.sendAsyncMessage("Settings:RegisterForMessages", - undefined, - undefined, - this._window.document.nodePrincipal); - this._isRegistered = true; - } else { - if ((!this._callbacks || Object.keys(this._callbacks).length == 0) && - !handler) { - if (DEBUG) debug("Unregistering for messages"); - cpmm.sendAsyncMessage("Settings:UnregisterForMessages", - undefined, - undefined, - this._window.document.nodePrincipal); - this._isRegistered = false; - this._callbacks = null; - } - } - }, - addObserver: function addObserver(aName, aCallback) { if (DEBUG) debug("addObserver " + aName); if (!this._callbacks) { + cpmm.sendAsyncMessage("Settings:RegisterForMessages"); this._callbacks = {}; } if (!this._callbacks[aName]) { @@ -309,35 +335,43 @@ SettingsManager.prototype = { } else { this._callbacks[aName].push(aCallback); } - this.checkMessageRegistration(); }, removeObserver: function removeObserver(aName, aCallback) { if (DEBUG) debug("deleteObserver " + aName); if (this._callbacks && this._callbacks[aName]) { - let index = this._callbacks[aName].indexOf(aCallback); + let index = this._callbacks[aName].indexOf(aCallback) if (index != -1) { - this._callbacks[aName].splice(index, 1); - if (this._callbacks[aName].length == 0) { - delete this._callbacks[aName]; - } + this._callbacks[aName].splice(index, 1) } else { if (DEBUG) debug("Callback not found for: " + aName); } } else { if (DEBUG) debug("No observers stored for " + aName); } - this.checkMessageRegistration(); }, init: function(aWindow) { - if (DEBUG) debug("SettingsManager init"); mrm.registerStrongReporter(this); cpmm.addMessageListener("Settings:Change:Return:OK", this); this._window = aWindow; Services.obs.addObserver(this, "inner-window-destroyed", false); let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); this.innerWindowID = util.currentInnerWindowID; + + let readPerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-read"); + let writePerm = Services.perms.testExactPermissionFromPrincipal(aWindow.document.nodePrincipal, "settings-write"); + this.hasReadPrivileges = readPerm == Ci.nsIPermissionManager.ALLOW_ACTION; + this.hasWritePrivileges = writePerm == Ci.nsIPermissionManager.ALLOW_ACTION; + + if (this.hasReadPrivileges) { + cpmm.sendAsyncMessage("Settings:RegisterForMessages"); + } + + if (!this.hasReadPrivileges && !this.hasWritePrivileges) { + dump("No settings permission for: " + aWindow.document.nodePrincipal.origin + "\n"); + Cu.reportError("No settings permission for: " + aWindow.document.nodePrincipal.origin); + } }, observe: function(aSubject, aTopic, aData) { @@ -351,7 +385,7 @@ SettingsManager.prototype = { }, collectReports: function(aCallback, aData, aAnonymize) { - for (let topic in this._callbacks) { + for (var topic in this._callbacks) { let length = this._callbacks[topic].length; if (length == 0) { continue; @@ -381,6 +415,7 @@ SettingsManager.prototype = { this._requests = null; this._window = null; this._innerWindowID = null; + this._settingsDB.close(); }, classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"), @@ -391,4 +426,4 @@ SettingsManager.prototype = { Ci.nsIMemoryReporter]), }; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]) diff --git a/dom/settings/SettingsRequestManager.jsm b/dom/settings/SettingsRequestManager.jsm deleted file mode 100644 index 6406a3f71e8a..000000000000 --- a/dom/settings/SettingsRequestManager.jsm +++ /dev/null @@ -1,912 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const DEBUG = false; -function debug(s) { dump("-*- SettingsRequestManager: " + s + "\n"); } -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -this.EXPORTED_SYMBOLS = ["SettingsRequestManager"]; - -Cu.import("resource://gre/modules/SettingsDB.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/PermissionsTable.jsm"); - -const kXpcomShutdownObserverTopic = "xpcom-shutdown"; -const kMozSettingsChangedObserverTopic = "mozsettings-changed"; -const kSettingsReadSuffix = "-read"; -const kSettingsWriteSuffix = "-write"; -const kSettingsClearPermission = "settings-clear"; -const kAllSettingsReadPermission = "settings" + kSettingsReadSuffix; -const kAllSettingsWritePermission = "settings" + kSettingsWriteSuffix; -// Any application with settings permissions, be it for all settings -// or a single one, will need to be able to access the settings API. -// The settings-api permission allows an app to see the mozSettings -// API in order to create locks and queue tasks. Whether these tasks -// will be allowed depends on the exact permissions the app has. -const kSomeSettingsReadPermission = "settings-api" + kSettingsReadSuffix; -const kSomeSettingsWritePermission = "settings-api" + kSettingsWriteSuffix; - -XPCOMUtils.defineLazyServiceGetter(this, "ppmm", - "@mozilla.org/parentprocessmessagemanager;1", - "nsIMessageBroadcaster"); -XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", - "@mozilla.org/uuid-generator;1", - "nsIUUIDGenerator"); - -let SettingsPermissions = { - _mmPermissions: {}, - addManager: function(aMessage) { - if (DEBUG) debug("Adding message manager permissions"); - let mm = aMessage.target; - // In order for mochitests to work, we have to update permissions on every - // lock creation or observer addition. This still means we can cache - // permissions. - if (this._mmPermissions[mm]) { - if (DEBUG) debug("Manager already added, updating permissions"); - } - let perms = []; - let principal; - let isSystemPrincipal = false; - if (aMessage.principal.origin == "[System Principal]") { - isSystemPrincipal = true; - } else { - let uri = Services.io.newURI(aMessage.principal.origin, null, null); - principal = Services.scriptSecurityManager.getAppCodebasePrincipal(uri, - aMessage.principal.appId, - aMessage.principal.isInBrowserElement); - } - for (let i in AllPossiblePermissions) { - let permName = AllPossiblePermissions[i]; - // We only care about permissions starting with the word "settings" - if (permName.indexOf("settings") != 0) { - continue; - } - if (isSystemPrincipal || Services.perms.testExactPermissionFromPrincipal(principal, permName) == Ci.nsIPermissionManager.ALLOW_ACTION) { - perms.push(permName); - } - } - this._mmPermissions[mm] = perms; - }, - removeManager: function(aMsgMgr) { - if (DEBUG) debug("Removing message manager permissions for " + aMsgMgr); - if (!this._mmPermissions[aMsgMgr]) { - if (DEBUG) debug("Manager not added!"); - return; - } - delete this._mmPermissions[aMsgMgr]; - }, - checkPermission: function(aMsgMgr, aPerm) { - if (!this._mmPermissions[aMsgMgr]) { - if (DEBUG) debug("Manager not added!"); - return false; - } - return (this._mmPermissions[aMsgMgr].indexOf(aPerm) != -1); - }, - hasAllReadPermission: function(aMsgMgr) { - return this.checkPermission(aMsgMgr, kAllSettingsReadPermission); - }, - hasAllWritePermission: function(aMsgMgr) { - return this.checkPermission(aMsgMgr, kAllSettingsWritePermission); - }, - hasSomeReadPermission: function(aMsgMgr) { - return this.checkPermission(aMsgMgr, kSomeSettingsReadPermission); - }, - hasSomeWritePermission: function(aMsgMgr) { - return this.checkPermission(aMsgMgr, kSomeSettingsWritePermission); - }, - hasClearPermission: function(aMsgMgr) { - return this.checkPermission(aMsgMgr, kSettingsClearPermission); - }, - assertSomeReadPermission: function(aMsgMgr) { - aMsgMgr.assertPermission(kSomeSettingsReadPermission); - }, - hasReadPermission: function(aMsgMgr, aSettingsName) { - return this.hasAllReadPermission(aMsgMgr) || this.checkPermission(aMsgMgr, "settings:" + aSettingsName + kSettingsReadSuffix); - }, - hasWritePermission: function(aMsgMgr, aSettingsName) { - return this.hasAllWritePermission(aMsgMgr) || this.checkPermission(aMsgMgr, "settings:" + aSettingsName + kSettingsWriteSuffix); - } -}; - - -function SettingsLockInfo(aDB, aMsgMgr, aLockID, aIsServiceLock) { - return { - // ID Shared with the object on the child side - lockID: aLockID, - // Is this a content lock or a settings service lock? - isServiceLock: aIsServiceLock, - // Tasks to be run once the lock is at the head of the queue - tasks: [], - // This is set to true once a transaction is ready to run, but is not at the - // head of the lock queue. - consumable: false, - // Holds values that are requested to be set until the lock lifetime ends, - // then commits them to the DB. - queuedSets: {}, - // Internal transaction object - _transaction: undefined, - // Message manager that controls the lock - _mm: aMsgMgr, - // If true, it means a permissions check failed, so just fail everything now - _failed: false, - // If we're slated to run finalize, set this to make sure we don't - // somehow run other events afterward. - finalizing: false, - // Lets us know if we can use this lock for a clear command - canClear: true, - // Lets us know if this lock has been used to clear at any point. - hasCleared: false, - getObjectStore: function() { - if (DEBUG) debug("Getting transaction for " + this.lockID); - let store; - // Test for transaction validity via trying to get the - // datastore. If it doesn't work, assume the transaction is - // closed, create a new transaction and try again. - if (this._transaction) { - try { - store = this._transaction.objectStore(SETTINGSSTORE_NAME); - } catch (e) { - if (e.name == "InvalidStateError") { - if (DEBUG) debug("Current transaction for " + this.lockID + " closed, trying to create new one."); - } else { - throw e; - } - } - } - // Create one transaction with a global permission. This may be - // slightly slower on apps with full settings permissions, but - // it means we don't have to do our own transaction order - // bookkeeping. - if (!SettingsPermissions.hasSomeWritePermission(this._mm)) { - this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly"); - } else { - this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite"); - } - this._transaction.oncomplete = function() { - if (DEBUG) debug("Transaction for lock " + this.lockID + " closed"); - }.bind(this); - this._transaction.onabort = function () { - if (DEBUG) debug("Transaction for lock " + this.lockID + " aborted"); - this._failed = true; - }.bind(this); - try { - store = this._transaction.objectStore(SETTINGSSTORE_NAME); - } catch (e) { - if (e.name == "InvalidStateError") { - if (DEBUG) debug("Cannot create objectstore on transaction for " + this.lockID); - return null; - } else { - throw e; - } - } - return store; - }, - get objectStore() { - return this.getObjectStore(); - } - }; -} - -let SettingsRequestManager = { - // Access to the settings DB - settingsDB: new SettingsDB(), - // Remote messages to listen for from child - messages: ["child-process-shutdown", "Settings:Get", "Settings:Set", - "Settings:Clear", "Settings:Run", "Settings:Finalize", - "Settings:CreateLock", "Settings:RegisterForMessages"], - // Map of LockID to SettingsLockInfo objects - lockInfo: {}, - // Queue of LockIDs. The LockID on the front of the queue is the only lock - // that will have requests processed, all other locks will queue requests - // until they hit the front of the queue. - settingsLockQueue: [], - children: [], - init: function() { - if (DEBUG) debug("init"); - this.settingsDB.init(); - this.messages.forEach((function(msgName) { - ppmm.addMessageListener(msgName, this); - }).bind(this)); - Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); - }, - - _serializePreservingBinaries: function _serializePreservingBinaries(aObject) { - function needsUUID(aValue) { - if (!aValue || !aValue.constructor) { - return false; - } - return (aValue.constructor.name == "Date") || (aValue instanceof Ci.nsIDOMFile) || - (aValue instanceof Ci.nsIDOMBlob); - } - // We need to serialize settings objects, otherwise they can change between - // the set() call and the enqueued request being processed. We can't simply - // parse(stringify(obj)) because that breaks things like Blobs, Files and - // Dates, so we use stringify's replacer and parse's reviver parameters to - // preserve binaries. - let binaries = Object.create(null); - let stringified = JSON.stringify(aObject, function(key, value) { - value = this.settingsDB.prepareValue(value); - if (needsUUID(value)) { - let uuid = uuidgen.generateUUID().toString(); - binaries[uuid] = value; - return uuid; - } - return value; - }.bind(this)); - return JSON.parse(stringified, function(key, value) { - if (value in binaries) { - return binaries[value]; - } - return value; - }); - }, - - queueTask: function(aOperation, aData) { - if (DEBUG) debug("Queueing task: " + aOperation); - - let defer = {}; - - if (aOperation == "set") { - aData.settings = this._serializePreservingBinaries(aData.settings); - } - - this.lockInfo[aData.lockID].tasks.push({ - operation: aOperation, - data: aData, - defer: defer - }); - - let promise = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - - return promise; - }, - - // Due to the fact that we're skipping the database in some places - // by keeping a local "set" value cache, resolving some calls - // without a call to the database would mean we could potentially - // receive promise responses out of expected order if a get is - // called before a set. Therefore, we wrap our resolve in a null - // get, which means it will resolves afer the rest of the calls - // queued to the DB. - queueTaskReturn: function(aTask, aReturnValue) { - if (DEBUG) debug("Making task queuing transaction request."); - let data = aTask.data; - let lock = this.lockInfo[data.lockID]; - let store = lock.objectStore; - if (!store) { - if (DEBUG) debug("Rejecting task queue on lock " + aTask.data.lockID); - return Promise.reject({task: aTask, error: "Cannot get object store"}); - } - // Due to the fact that we're skipping the database, resolving - // this without a call to the database would mean we could - // potentially receive promise responses out of expected order if - // a get is called before a set. Therefore, we wrap our resolve in - // a null get, which means it will resolves afer the rest of the - // calls queued to the DB. - let getReq = store.get(0); - - let defer = {}; - let promiseWrapper = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - - getReq.onsuccess = function(event) { - return defer.resolve(aReturnValue); - }; - getReq.onerror = function() { - return defer.reject({task: aTask, error: getReq.error.name}); - }; - return promiseWrapper; - }, - - taskGet: function(aTask) { - if (DEBUG) debug("Running Get task on lock " + aTask.data.lockID); - - // Check that we have permissions for getting the value - let data = aTask.data; - let lock = this.lockInfo[data.lockID]; - - if (lock._failed) { - if (DEBUG) debug("Lock failed. All subsequent requests will fail."); - return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."}); - } - - if (lock.hasCleared) { - if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail."); - return Promise.reject({task: aTask, error: "Lock was used for a clear command. All subsequent requests will fail."}); - } - - lock.canClear = false; - - if (!SettingsPermissions.hasReadPermission(lock._mm, data.name)) { - if (DEBUG) debug("get not allowed for " + data.name); - lock._failed = true; - return Promise.reject({task: aTask, error: "No permission to get " + data.name}); - } - - // If the value was set during this transaction, use the cached value - if (data.name in lock.queuedSets) { - if (DEBUG) debug("Returning cached set value " + lock.queuedSets[data.name] + " for " + data.name); - let local_results = {}; - local_results[data.name] = lock.queuedSets[data.name]; - return this.queueTaskReturn(aTask, {task: aTask, results: local_results}); - } - - // Create/Get transaction and make request - if (DEBUG) debug("Making get transaction request for " + data.name); - let store = lock.objectStore; - if (!store) { - if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID); - return Promise.reject({task: aTask, error: "Cannot get object store"}); - } - - if (DEBUG) debug("Making get request for " + data.name); - let getReq = (data.name === "*") ? store.mozGetAll() : store.mozGetAll(data.name); - - let defer = {}; - let promiseWrapper = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - - getReq.onsuccess = function(event) { - if (DEBUG) debug("Request for '" + data.name + "' successful. " + - "Record count: " + event.target.result.length); - - if (event.target.result.length == 0) { - if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + data.name + " is not in the database.\n"); - } - - let results = {}; - - for (let i in event.target.result) { - let result = event.target.result[i]; - let name = result.settingName; - if (DEBUG) debug(name + ": " + result.userValue +", " + result.defaultValue); - let value = result.userValue !== undefined ? result.userValue : result.defaultValue; - results[name] = value; - } - return defer.resolve({task: aTask, results: results}); - }; - getReq.onerror = function() { - return defer.reject({task: aTask, error: getReq.error.name}); - }; - return promiseWrapper; - }, - - taskSet: function(aTask) { - let data = aTask.data; - let lock = this.lockInfo[data.lockID]; - let keys = Object.getOwnPropertyNames(data.settings); - - if (lock._failed) { - if (DEBUG) debug("Lock failed. All subsequent requests will fail."); - return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."}); - } - - if (lock.hasCleared) { - if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail."); - return Promise.reject({task: aTask, error: "Lock was used for a clear command. All other requests will fail."}); - } - - lock.canClear = false; - - // If we have no keys, resolve - if (keys.length === 0) { - if (DEBUG) debug("No keys to change entered!"); - return Promise.resolve({task: aTask}); - } - - for (let i = 0; i < keys.length; i++) { - if (!SettingsPermissions.hasWritePermission(lock._mm, keys[i])) { - if (DEBUG) debug("set not allowed on " + keys[i]); - lock._failed = true; - return Promise.reject({task: aTask, error: "No permission to set " + keys[i]}); - } - } - - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - if (DEBUG) debug("key: " + key + ", val: " + JSON.stringify(data.settings[key]) + ", type: " + typeof(data.settings[key])); - lock.queuedSets[key] = data.settings[key]; - } - - return this.queueTaskReturn(aTask, {task: aTask}); - }, - - queueConsume: function() { - if (this.settingsLockQueue.length > 0 && this.lockInfo[this.settingsLockQueue[0]].consumable) { - Services.tm.currentThread.dispatch(SettingsRequestManager.consumeTasks.bind(this), Ci.nsIThread.DISPATCH_NORMAL); - } - }, - - // Removes the current lock from the queue, and starts transactions for the - // next lock, assuming there is one. - removeCurrentLock: function() { - let lock = this.settingsLockQueue.shift(); - delete this.lockInfo[lock.lockID]; - this.queueConsume(); - }, - - finalizeSets: function(aTask) { - let data = aTask.data; - if (DEBUG) debug("Finalizing tasks for lock " + data.lockID); - let lock = this.lockInfo[data.lockID]; - lock.finalizing = true; - if (lock._failed) { - this.removeCurrentLock(); - return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."}); - } - // If we have cleared, there is no reason to continue finalizing - // this lock. Just resolve promise with task and move on. - if (lock.hasCleared) { - if (DEBUG) debug("Clear was called on lock, skipping finalize"); - this.removeCurrentLock(); - return Promise.resolve({task: aTask}); - } - let keys = Object.getOwnPropertyNames(lock.queuedSets); - if (keys.length === 0) { - if (DEBUG) debug("Nothing to finalize. Exiting."); - this.removeCurrentLock(); - return Promise.resolve({task: aTask}); - } - - let store = lock.objectStore; - if (!store) { - if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID); - return Promise.reject({task: aTask, error: "Cannot get object store"}); - } - - // Due to the fact there may have multiple set operations to clear, and - // they're all async, callbacks are gathered into promises, and the promises - // are processed with Promises.all(). - let checkPromises = []; - let finalValues = {}; - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - if (DEBUG) debug("key: " + key + ", val: " + lock.queuedSets[key] + ", type: " + typeof(lock.queuedSets[key])); - let checkDefer = {}; - let checkPromise = new Promise(function(resolve, reject) { - checkDefer.resolve = resolve; - checkDefer.reject = reject; - }); - - // Get operation is used to fill in the default value, assuming there is - // one. For the moment, if a value doesn't exist in the settings DB, we - // allow the user to add it, and just pass back a null default value. - let checkKeyRequest = store.get(key); - checkKeyRequest.onsuccess = function (event) { - let userValue = lock.queuedSets[key]; - let defaultValue; - if (!event.target.result) { - defaultValue = null; - if (DEBUG) debug("MOZSETTINGS-GET-WARNING: " + key + " is not in the database.\n"); - } else { - defaultValue = event.target.result.defaultValue; - } - let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue}; - finalValues[key] = {defaultValue: defaultValue, userValue: userValue}; - let setReq = store.put(obj); - setReq.onsuccess = function() { - if (DEBUG) debug("Set successful!"); - if (DEBUG) debug("key: " + key + ", val: " + finalValues[key] + ", type: " + typeof(finalValues[key])); - return checkDefer.resolve({task: aTask}); - }; - setReq.onerror = function() { - return checkDefer.reject({task: aTask, error: setReq.error.name}); - }; - }.bind(this); - checkKeyRequest.onerror = function(event) { - return checkDefer.reject({task: aTask, error: checkKeyRequest.error.name}); - }; - checkPromises.push(checkPromise); - } - - let defer = {}; - let promiseWrapper = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - - // Once all transactions are done, or any have failed, remove the lock and - // start processing the tasks from the next lock in the queue. - Promise.all(checkPromises).then(function() { - // If all commits were successful, notify observers - for (let i = 0; i < keys.length; i++) { - this.sendSettingsChange(keys[i], finalValues[keys[i]].userValue, lock.isServiceLock); - } - this.removeCurrentLock(); - defer.resolve({task: aTask}); - }.bind(this), function(ret) { - this.removeCurrentLock(); - defer.reject({task: aTask, error: "Set transaction failure"}); - }.bind(this)); - return promiseWrapper; - }, - - // Clear is only expected to be called via tests, and if a lock - // calls clear, it should be the only thing the lock does. This - // allows us to not have to deal with the possibility of query - // integrity checking. Clear should never be called in the wild, - // even by certified apps, which is why it has its own permission - // (settings-clear). - taskClear: function(aTask) { - if (DEBUG) debug("Clearing"); - let data = aTask.data; - let lock = this.lockInfo[data.lockID]; - - if (lock._failed) { - if (DEBUG) debug("Lock failed, all requests now failing."); - return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."}); - } - - if (!lock.canClear) { - if (DEBUG) debug("Lock tried to clear after queuing other tasks. Failing."); - lock._failed = true; - return Promise.reject({task: aTask, error: "Cannot call clear after queuing other tasks, all requests now failing."}); - } - - if (!SettingsPermissions.hasClearPermission(lock._mm)) { - if (DEBUG) debug("clear not allowed"); - lock._failed = true; - return Promise.reject({task: aTask, error: "No permission to clear DB"}); - } - - lock.hasCleared = true; - - let store = lock.objectStore; - if (!store) { - if (DEBUG) debug("Rejecting Clear task on lock " + aTask.data.lockID); - return Promise.reject({task: aTask, error: "Cannot get object store"}); - } - let defer = {}; - let promiseWrapper = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - - let clearReq = store.clear(); - clearReq.onsuccess = function() { - return defer.resolve({task: aTask}); - }; - clearReq.onerror = function() { - return defer.reject({task: aTask}); - }; - return promiseWrapper; - }, - - ensureConnection : function() { - if (DEBUG) debug("Ensuring Connection"); - let defer = {}; - let promiseWrapper = new Promise(function(resolve, reject) { - defer.resolve = resolve; - defer.reject = reject; - }); - this.settingsDB.ensureDB( - function() { defer.resolve(); }, - function(error) { - if (DEBUG) debug("Cannot open Settings DB. Trying to open an old version?\n"); - defer.reject(error); - } - ); - return promiseWrapper; - }, - - runTasks: function(aLockID) { - if (DEBUG) debug("Running tasks for " + aLockID); - let lock = this.lockInfo[aLockID]; - if (lock.finalizing) { - debug("TASK TRYING TO QUEUE AFTER FINALIZE CALLED. THIS IS BAD. Lock: " + aLockID); - return; - } - let currentTask = lock.tasks.shift(); - let promises = []; - while (currentTask) { - if (DEBUG) debug("Running Operation " + currentTask.operation); - let p; - switch (currentTask.operation) { - case "get": - p = this.taskGet(currentTask); - break; - case "set": - p = this.taskSet(currentTask); - break; - case "clear": - p = this.taskClear(currentTask); - break; - case "finalize": - p = this.finalizeSets(currentTask); - break; - default: - if (DEBUG) debug("Invalid operation: " + currentTask.operation); - p.reject("Invalid operation: " + currentTask.operation); - } - p.then(function(ret) { - ret.task.defer.resolve(ret.results); - }.bind(currentTask), function(ret) { - ret.task.defer.reject(ret.error); - }); - promises.push(p); - currentTask = lock.tasks.shift(); - } - }, - - consumeTasks: function() { - if (this.settingsLockQueue.length == 0) { - if (DEBUG) debug("Nothing to run!"); - return; - } - - let lockID = this.settingsLockQueue[0]; - if (DEBUG) debug("Consuming tasks for " + lockID); - let lock = this.lockInfo[lockID]; - - // If a process dies, we should clean up after it via the - // child-process-shutdown event. But just in case we don't, we want to make - // sure we never block on consuming. - if (!lock) { - if (DEBUG) debug("Lock not found"); - this.queueConsume(); - return; - } - - if (!lock.consumable || lock.tasks.length === 0) { - if (DEBUG) debug("No more tasks to run or not yet consuamble."); - return; - } - - lock.consumable = false; - this.ensureConnection().then( - function(task) { - this.runTasks(lockID); - }.bind(this), function(ret) { - dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n"); - }); - }, - - observe: function(aSubject, aTopic, aData) { - if (DEBUG) debug("observe"); - switch (aTopic) { - case kXpcomShutdownObserverTopic: - this.messages.forEach((function(msgName) { - ppmm.removeMessageListener(msgName, this); - }).bind(this)); - Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); - ppmm = null; - break; - default: - if (DEBUG) debug("Wrong observer topic: " + aTopic); - break; - } - }, - - sendSettingsChange: function(aKey, aValue, aIsServiceLock) { - this.broadcastMessage("Settings:Change:Return:OK", - { key: aKey, value: aValue }); - Services.obs.notifyObservers(this, kMozSettingsChangedObserverTopic, - JSON.stringify({ - key: aKey, - value: aValue, - isInternalChange: aIsServiceLock - })); - }, - - broadcastMessage: function broadcastMessage(aMsgName, aContent) { - if (DEBUG) debug("Broadcast"); - this.children.forEach(function(msgMgr) { - if (SettingsPermissions.hasReadPermission(msgMgr, aContent.key)) { - msgMgr.sendAsyncMessage(aMsgName, aContent); - } - }); - if (DEBUG) debug("Finished Broadcasting"); - }, - - addObserver: function(aMsgMgr) { - if (DEBUG) debug("Add observer for" + aMsgMgr); - if (this.children.indexOf(aMsgMgr) == -1) { - this.children.push(aMsgMgr); - } - }, - - removeObserver: function(aMsgMgr) { - if (DEBUG) debug("Remove observer for" + aMsgMgr); - let index = this.children.indexOf(aMsgMgr); - if (index != -1) { - this.children.splice(index, 1); - } - }, - - removeMessageManager: function(aMsgMgr){ - if (DEBUG) debug("Removing message manager " + aMsgMgr); - this.removeObserver(aMsgMgr); - SettingsPermissions.removeManager(aMsgMgr); - let closedLockIDs = []; - let lockIDs = Object.keys(this.lockInfo); - for (let i in lockIDs) { - if (this.lockInfo[lockIDs[i]]._mm == aMsgMgr) { - if (DEBUG) debug("Removing lock " + lockIDs[i] + " due to process close/crash"); - closedLockIDs.push(lockIDs[i]); - } - } - for (let i in closedLockIDs) { - let transaction = this.lockInfo[closedLockIDs[i]]._transaction; - if (transaction) { - transaction.abort(); - } - delete this.lockInfo[closedLockIDs[i]]; - let index = this.settingsLockQueue.indexOf(closedLockIDs[i]); - if (index > -1) { - this.settingsLockQueue.splice(index, 1); - } - // If index is 0, the lock we just removed was at the head of - // the queue, so possibly queue the next lock if it's - // consumable. - if (index == 0) { - this.queueConsume(); - } - } - }, - - receiveMessage: function(aMessage) { - if (DEBUG) debug("receiveMessage " + aMessage.name); - - let msg = aMessage.data; - let mm = aMessage.target; - - function returnMessage(name, data) { - try { - mm.sendAsyncMessage(name, data); - } catch (e) { - if (DEBUG) debug("Return message failed, " + name); - } - } - - // For all message types that expect a lockID, we check to make - // sure that we're accessing a lock that's part of our process. If - // not, consider it a security violation and kill the app. Killing - // based on creating a colliding lock ID happens as part of - // CreateLock check below. - switch (aMessage.name) { - case "Settings:Get": - case "Settings:Set": - case "Settings:Clear": - case "Settings:Run": - case "Settings:Finalize": - if (!msg.lockID || - !this.lockInfo[msg.lockID] || - mm != this.lockInfo[msg.lockID]._mm) { - Cu.reportError("Process trying to access settings lock from another process. Killing."); - // Kill the app by checking for a non-existent permission - aMessage.target.assertPermission("message-manager-mismatch-kill"); - return; - } - default: - break; - } - - switch (aMessage.name) { - case "child-process-shutdown": - if (DEBUG) debug("Child process shutdown received."); - this.removeMessageManager(mm); - break; - case "Settings:RegisterForMessages": - SettingsPermissions.addManager(aMessage); - if (!SettingsPermissions.hasSomeReadPermission(mm)) { - Cu.reportError("Settings message " + aMessage.name + - " from a content process with no 'settings-api-read' privileges."); - // Kill app after reporting error - SettingsPermissions.assertSomeReadPermission(mm); - return; - } - this.addObserver(mm); - break; - case "Settings:UnregisterForMessages": - this.removeObserver(mm); - break; - case "Settings:CreateLock": - if (DEBUG) debug("Received CreateLock for " + msg.lockID); - // If we try to create a lock ID that collides with one - // already in the system, consider it a security violation and - // kill. - if (msg.lockID in this.settingsLockQueue) { - Cu.reportError("Trying to queue a lock with the same ID as an already queued lock. Killing app."); - aMessage.target.assertPermission("lock-id-duplicate-kill"); - return; - } - this.settingsLockQueue.push(msg.lockID); - SettingsPermissions.addManager(aMessage); - this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB, mm, msg.lockID, msg.isServiceLock); - break; - case "Settings:Get": - if (DEBUG) debug("Received getRequest"); - this.queueTask("get", msg).then(function(settings) { - returnMessage("Settings:Get:OK", { - lockID: msg.lockID, - requestID: msg.requestID, - settings: settings - }); - }, function(error) { - if (DEBUG) debug("getRequest FAILED " + msg.name); - returnMessage("Settings:Get:KO", { - lockID: msg.lockID, - requestID: msg.requestID, - errorMsg: error - }); - }); - break; - case "Settings:Set": - if (DEBUG) debug("Received Set Request"); - this.queueTask("set", msg).then(function(settings) { - returnMessage("Settings:Set:OK", { - lockID: msg.lockID, - requestID: msg.requestID - }); - }, function(error) { - returnMessage("Settings:Set:KO", { - lockID: msg.lockID, - requestID: msg.requestID, - errorMsg: error - }); - }); - break; - case "Settings:Clear": - if (DEBUG) debug("Received Clear Request"); - this.queueTask("clear", msg).then(function() { - returnMessage("Settings:Clear:OK", { - lockID: msg.lockID, - requestID: msg.requestID - }); - }, function(error) { - returnMessage("Settings:Clear:KO", { - lockID: msg.lockID, - requestID: msg.requestID, - errorMsg: error - }); - }); - break; - case "Settings:Finalize": - if (DEBUG) debug("Received Finalize"); - this.queueTask("finalize", msg).then(function() { - returnMessage("Settings:Finalize:OK", { - lockID: msg.lockID - }); - }, function(error) { - returnMessage("Settings:Finalize:KO", { - lockID: msg.lockID, - errorMsg: error - }); - }); - // YES THIS IS SUPPOSED TO FALL THROUGH. Finalize is considered a task - // running situation, but it also needs to queue a task. - case "Settings:Run": - if (DEBUG) debug("Received Run"); - this.lockInfo[msg.lockID].consumable = true; - if (msg.lockID == this.settingsLockQueue[0]) { - // If a lock is currently at the head of the queue, run all tasks for - // it. - if (DEBUG) debug("Running tasks for " + msg.lockID); - this.queueConsume(); - } else { - // If a lock isn't at the head of the queue, but requests to be run, - // simply mark it as consumable, which means it will automatically run - // once it comes to the head of the queue. - if (DEBUG) debug("Queuing tasks for " + msg.lockID + " while waiting for " + this.settingsLockQueue[0]); - } - break; - default: - if (DEBUG) debug("Wrong message: " + aMessage.name); - } - } -}; - -SettingsRequestManager.init(); diff --git a/dom/settings/SettingsService.js b/dom/settings/SettingsService.js index 4478e25a6e13..dc8e6b32f2b9 100644 --- a/dom/settings/SettingsService.js +++ b/dom/settings/SettingsService.js @@ -5,24 +5,20 @@ "use strict" /* static functions */ -const DEBUG = false; -function debug(s) { - dump("-*- SettingsService: " + s + "\n"); -} +let DEBUG = 0; +let debug; +if (DEBUG) + debug = function (s) { dump("-*- SettingsService: " + s + "\n"); } +else + debug = function (s) {} const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://gre/modules/SettingsQueue.jsm"); +Cu.import("resource://gre/modules/SettingsDB.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import('resource://gre/modules/SettingsRequestManager.jsm'); - -XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", - "@mozilla.org/uuid-generator;1", - "nsIUUIDGenerator"); -XPCOMUtils.defineLazyServiceGetter(this, "cpmm", - "@mozilla.org/childprocessmessagemanager;1", - "nsIMessageSender"); const nsIClassInfo = Ci.nsIClassInfo; @@ -30,149 +26,18 @@ const SETTINGSSERVICELOCK_CONTRACTID = "@mozilla.org/settingsServiceLock;1"; const SETTINGSSERVICELOCK_CID = Components.ID("{d7a395a0-e292-11e1-834e-1761d57f5f99}"); const nsISettingsServiceLock = Ci.nsISettingsServiceLock; -function makeSettingsServiceRequest(aCallback, aName, aValue) { - return { - callback: aCallback, - name: aName, - value: aValue - }; -}; - -function SettingsServiceLock(aSettingsService, aTransactionCallback) { +function SettingsServiceLock(aSettingsService, aTransactionCallback) +{ if (DEBUG) debug("settingsServiceLock constr!"); this._open = true; + this._busy = false; + this._requests = new Queue(); this._settingsService = aSettingsService; - this._id = uuidgen.generateUUID().toString(); + this._transaction = null; this._transactionCallback = aTransactionCallback; - this._requests = {}; - let closeHelper = function() { - if (DEBUG) debug("closing lock " + this._id); - this._open = false; - this.runOrFinalizeQueries(); - }.bind(this); - - let msgs = ["Settings:Get:OK", "Settings:Get:KO", - "Settings:Clear:OK", "Settings:Clear:KO", - "Settings:Set:OK", "Settings:Set:KO", - "Settings:Finalize:OK", "Settings:Finalize:KO"]; - - for (let msg in msgs) { - cpmm.addMessageListener(msgs[msg], this); - } - - cpmm.sendAsyncMessage("Settings:CreateLock", {lockID: this._id, isServiceLock: true}, undefined, Services.scriptSecurityManager.getSystemPrincipal()); - Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL); } SettingsServiceLock.prototype = { - get closed() { - return !this._open; - }, - - runOrFinalizeQueries: function() { - if (!this._requests || Object.keys(this._requests).length == 0) { - cpmm.sendAsyncMessage("Settings:Finalize", {lockID: this._id}, undefined, Services.scriptSecurityManager.getSystemPrincipal()); - } else { - cpmm.sendAsyncMessage("Settings:Run", {lockID: this._id}, undefined, Services.scriptSecurityManager.getSystemPrincipal()); - } - }, - - receiveMessage: function(aMessage) { - if (DEBUG) debug("receiveMessage: " + aMessage.name); - let msg = aMessage.data; - // SettingsRequestManager broadcasts changes to all locks in the child. If - // our lock isn't being addressed, just return. - if(msg.lockID != this._id) { - return; - } - - // Finalizing a transaction does not return a request ID since we are - // supposed to fire callbacks. - if (!msg.requestID) { - switch (aMessage.name) { - case "Settings:Finalize:OK": - if (DEBUG) debug("Lock finalize ok!"); - this.callTransactionHandle(); - break; - case "Settings:Finalize:KO": - if (DEBUG) debug("Lock finalize failed!"); - this.callAbort(); - break; - default: - if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID"); - } - return; - } - - let req = this._requests[msg.requestID]; - if (!req) { - if (DEBUG) debug("Matching request not found."); - return; - } - delete this._requests[msg.requestID]; - switch (aMessage.name) { - case "Settings:Get:OK": - this._open = true; - let settings_names = Object.keys(msg.settings); - if (settings_names.length > 0) { - let name = settings_names[0]; - if (DEBUG && settings_names.length > 1) { - debug("Warning: overloaded setting:" + name); - } - let result = msg.settings[name]; - this.callHandle(req.callback, name, result); - } else { - this.callHandle(req.callback, req.name, null); - } - this._open = false; - break; - case "Settings:Set:OK": - this._open = true; - // We don't pass values back from sets in SettingsManager... - this.callHandle(req.callback, req.name, req.value); - this._open = false; - break; - case "Settings:Get:KO": - case "Settings:Set:KO": - if (DEBUG) debug("error:" + msg.errorMsg); - this.callError(req.callback, msg.error); - break; - default: - if (DEBUG) debug("Wrong message: " + aMessage.name); - } - this.runOrFinalizeQueries(); - }, - - get: function get(aName, aCallback) { - if (DEBUG) debug("get (" + this._id + "): " + aName); - if (!this._open) { - dump("Settings lock not open!\n"); - throw Components.results.NS_ERROR_ABORT; - } - let reqID = uuidgen.generateUUID().toString(); - this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName); - cpmm.sendAsyncMessage("Settings:Get", {requestID: reqID, - lockID: this._id, - name: aName}, - undefined, - Services.scriptSecurityManager.getSystemPrincipal()); - }, - - set: function set(aName, aValue, aCallback) { - if (DEBUG) debug("set: " + aName + " " + aValue); - if (!this._open) { - throw "Settings lock not open"; - } - let reqID = uuidgen.generateUUID().toString(); - this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName, aValue); - let settings = {}; - settings[aName] = aValue; - cpmm.sendAsyncMessage("Settings:Set", {requestID: reqID, - lockID: this._id, - settings: settings}, - undefined, - Services.scriptSecurityManager.getSystemPrincipal()); - }, callHandle: function callHandle(aCallback, aName, aValue) { try { @@ -206,6 +71,149 @@ SettingsServiceLock.prototype = { } }, + process: function process() { + debug("process!"); + let lock = this; + lock._open = false; + let store = lock._transaction.objectStore(SETTINGSSTORE_NAME); + + while (!lock._requests.isEmpty()) { + if (lock._isBusy) { + return; + } + let info = lock._requests.dequeue(); + if (DEBUG) debug("info:" + info.intent); + let callback = info.callback; + let name = info.name; + switch (info.intent) { + case "set": + let value = info.value; + let message = info.message; + if(DEBUG && typeof(value) == 'object') { + debug("object name:" + name + ", val: " + JSON.stringify(value)); + } + lock._isBusy = true; + let checkKeyRequest = store.get(name); + + checkKeyRequest.onsuccess = function (event) { + let defaultValue; + if (event.target.result) { + defaultValue = event.target.result.defaultValue; + } else { + defaultValue = null; + if (DEBUG) debug("MOZSETTINGS-SET-WARNING: " + name + " is not in the database.\n"); + } + let setReq = store.put({ settingName: name, defaultValue: defaultValue, userValue: value }); + + setReq.onsuccess = function() { + lock._isBusy = false; + lock._open = true; + lock.callHandle(callback, name, value); + Services.obs.notifyObservers(lock, "mozsettings-changed", JSON.stringify({ + key: name, + value: value, + message: message + })); + lock._open = false; + lock.process(); + }; + + setReq.onerror = function(event) { + lock._isBusy = false; + lock.callError(callback, event.target.errorMessage); + lock.process(); + }; + } + + checkKeyRequest.onerror = function(event) { + lock._isBusy = false; + lock.callError(callback, event.target.errorMessage); + lock.process(); + }; + break; + case "get": + let getReq = store.mozGetAll(name); + getReq.onsuccess = function(event) { + if (DEBUG) { + debug("Request successful. Record count:" + event.target.result.length); + debug("result: " + JSON.stringify(event.target.result)); + } + this._open = true; + if (callback) { + if (event.target.result[0]) { + if (event.target.result.length > 1) { + if (DEBUG) debug("Warning: overloaded setting:" + name); + } + let result = event.target.result[0]; + let value = result.userValue !== undefined + ? result.userValue + : result.defaultValue; + lock.callHandle(callback, name, value); + } else { + lock.callHandle(callback, name, null); + } + } else { + if (DEBUG) debug("no callback defined!"); + } + this._open = false; + }.bind(lock); + getReq.onerror = function error(event) { + lock.callError(callback, event.target.errorMessage); + }; + break; + } + } + lock._open = true; + }, + + createTransactionAndProcess: function() { + if (this._settingsService._settingsDB._db) { + let lock; + while (lock = this._settingsService._locks.dequeue()) { + if (!lock._transaction) { + lock._transaction = lock._settingsService._settingsDB._db.transaction(SETTINGSSTORE_NAME, "readwrite"); + if (lock._transactionCallback) { + lock._transaction.oncomplete = lock.callTransactionHandle.bind(lock); + lock._transaction.onabort = function(event) { + let message = ''; + if (event.target.error) { + message = event.target.error.name + ': ' + event.target.error.message; + } + this.callAbort(lock._transactionCallback.handleAbort, message); + }; + } + } + if (!lock._isBusy) { + lock.process(); + } else { + this._settingsService._locks.enqueue(lock); + return; + } + } + if (!this._requests.isEmpty() && !this._isBusy) { + this.process(); + } + } + }, + + get: function get(aName, aCallback) { + if (DEBUG) debug("get: " + aName + ", " + aCallback); + this._requests.enqueue({ callback: aCallback, intent:"get", name: aName }); + this.createTransactionAndProcess(); + }, + + set: function set(aName, aValue, aCallback, aMessage) { + debug("set: " + aName + ": " + JSON.stringify(aValue)); + if (aMessage === undefined) + aMessage = null; + this._requests.enqueue({ callback: aCallback, + intent: "set", + name: aName, + value: this._settingsService._settingsDB.prepareValue(aValue), + message: aMessage }); + this.createTransactionAndProcess(); + }, + classID : SETTINGSSERVICELOCK_CID, QueryInterface : XPCOMUtils.generateQI([nsISettingsServiceLock]) }; @@ -214,18 +222,34 @@ const SETTINGSSERVICE_CID = Components.ID("{f656f0c0-f776-11e1-a21f-08002 function SettingsService() { - if (DEBUG) debug("settingsService Constructor"); + debug("settingsService Constructor"); + this._locks = new Queue(); + this._settingsDB = new SettingsDB(); + this._settingsDB.init(); } SettingsService.prototype = { + nextTick: function nextTick(aCallback, thisObj) { + if (thisObj) + aCallback = aCallback.bind(thisObj); + + Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); + }, + createLock: function createLock(aCallback) { var lock = new SettingsServiceLock(this, aCallback); + this._locks.enqueue(lock); + this._settingsDB.ensureDB( + function() { lock.createTransactionAndProcess(); }, + function() { dump("SettingsService failed to open DB!\n"); } + ); + this.nextTick(function() { this._open = false; }, lock); return lock; }, classID : SETTINGSSERVICE_CID, QueryInterface : XPCOMUtils.generateQI([Ci.nsISettingsService]) -}; +} -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]) diff --git a/dom/settings/moz.build b/dom/settings/moz.build index c12a43e655eb..63cdc1ed4fd6 100644 --- a/dom/settings/moz.build +++ b/dom/settings/moz.build @@ -18,8 +18,9 @@ if CONFIG['MOZ_B2G']: MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] EXTRA_JS_MODULES += [ + 'SettingsChangeNotifier.jsm', 'SettingsDB.jsm', - 'SettingsRequestManager.jsm' + 'SettingsQueue.jsm', ] MOCHITEST_MANIFESTS += ['tests/mochitest.ini'] diff --git a/dom/settings/tests/mochitest.ini b/dom/settings/tests/mochitest.ini index 90f21987ece6..76b0281fb903 100644 --- a/dom/settings/tests/mochitest.ini +++ b/dom/settings/tests/mochitest.ini @@ -2,7 +2,6 @@ skip-if = (toolkit == 'gonk' && debug) || e10s #debug-only failure, bug 932878 [test_settings_basics.html] -[test_settings_permissions.html] [test_settings_blobs.html] [test_settings_data_uris.html] [test_settings_events.html] diff --git a/dom/settings/tests/test_settings_basics.html b/dom/settings/tests/test_settings_basics.html index 0f25b0399e17..0c51fb4fd913 100644 --- a/dom/settings/tests/test_settings_basics.html +++ b/dom/settings/tests/test_settings_basics.html @@ -22,14 +22,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id={678695} "use strict"; if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } -SpecialPowers.addPermission("settings-api-read", true, document); -SpecialPowers.addPermission("settings-api-write", true, document); SpecialPowers.addPermission("settings-read", true, document); SpecialPowers.addPermission("settings-write", true, document); -SpecialPowers.addPermission("settings-clear", true, document); function onUnwantedSuccess() { ok(false, "onUnwantedSuccess: shouldn't get here"); @@ -327,11 +324,8 @@ var steps = [ req = lock.clear(); req.onsuccess = function () { ok(true, "Deleted the database"); - next(); }; - }, - function () { - var lock = mozSettings.createLock(); + req.onerror = onFailure; req2 = lock.set(wifi); req2.onsuccess = function () { ok(true, "set done"); @@ -340,23 +334,23 @@ var steps = [ ok(true, "Get all settings"); var lock2 = mozSettings.createLock(); - req3 = lock2.get("*"); - req3.onsuccess = function () { - is(Object.keys(req3.result).length, 1, "length 1"); - check(req3.result, wifi); - ok(true, JSON.stringify(req3.result)); + req = lock2.get("*"); + req.onsuccess = function () { + is(Object.keys(req.result).length, 1, "length 1"); + check(wifi, req.result); + ok(true, JSON.stringify(req.result)); ok(true, "Get all settings Done"); }; - req3.onerror = onFailure; + req.onerror = onFailure; - req4 = lock2.get("net3g.apn"); - req4.onsuccess = function () { - is(Object.keys(req4.result).length, 1, "length 1"); - check(wifi, req4.result); + req2 = lock2.get("net3g.apn"); + req2.onsuccess = function () { + is(Object.keys(req2.result).length, 1, "length 1"); + check(wifi, req2.result); ok(true, "Get net3g.apn Done"); next(); }; - req4.onerror = onFailure; + req2.onerror = onFailure; }, function () { ok(true, "Change wifi1"); @@ -473,8 +467,8 @@ var steps = [ req5.onerror = onFailure; } - var lock6 = mozSettings.createLock(); - req6 = lock6.clear(); + var lock5 = mozSettings.createLock(); + req6 = lock5.clear(); req6.onsuccess = function () { ok(true, "Deleted the database"); next(); @@ -505,20 +499,18 @@ var steps = [ req = lock.get("wifi.enabled"); req.onsuccess = function() { - check(this.result, wifiEnabled); + check(req.result, wifiEnabled); ok(true, "Test2 locking result done"); } req.onerror = onFailure; - var lock2 = mozSettings.createLock(); - req2 = lock2.clear(); + req2 = lock.clear(); req2.onsuccess = function () { ok(true, "Deleted the database"); }; req2.onerror = onFailure; - var lock3 = mozSettings.createLock(); - req3 = lock3.set(wifi); + req3 = lock.set(wifi); req3.onsuccess = function () { ok(true, "set done"); next(); @@ -769,24 +761,6 @@ var steps = [ }; req.onerror = onFailure; }, - function () { - ok(true, "Call success callback when transaction commits"); - var lock = mozSettings.createLock(); - lock.onsettingstransactionsuccess = function () { - next(); - }; - req = lock.set({"setting-obj": {foo: {bar: 23}}}); - req.onsuccess = function() { - req2 = lock.get("setting-obj"); - req2.onsuccess = function(event) { - var result = event.target.result["setting-obj"]; - ok(result, "Got valid result"); - ok(typeof result == "object", "Result is object"); - ok("foo" in result && "bar" in result.foo, "Result has properties"); - ok(result.foo.bar == 23, "Result properties are set"); - }; - }; - }, function () { ok(true, "all done!\n"); SimpleTest.finish(); diff --git a/dom/settings/tests/test_settings_blobs.html b/dom/settings/tests/test_settings_blobs.html index 35986fa6fcbe..fa5c0cea76c5 100644 --- a/dom/settings/tests/test_settings_blobs.html +++ b/dom/settings/tests/test_settings_blobs.html @@ -22,14 +22,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=821630 "use strict"; if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } SpecialPowers.addPermission("settings-read", true, document); SpecialPowers.addPermission("settings-write", true, document); -SpecialPowers.addPermission("settings-api-read", true, document); -SpecialPowers.addPermission("settings-api-write", true, document); -SpecialPowers.addPermission("settings-clear", true, document); function onUnwantedSuccess() { ok(false, "onUnwantedSuccess: shouldn't get here"); diff --git a/dom/settings/tests/test_settings_data_uris.html b/dom/settings/tests/test_settings_data_uris.html index 9b4ef51a5df1..b6436a737471 100644 --- a/dom/settings/tests/test_settings_data_uris.html +++ b/dom/settings/tests/test_settings_data_uris.html @@ -22,14 +22,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=806374 "use strict"; if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } SpecialPowers.addPermission("settings-read", true, document); SpecialPowers.addPermission("settings-write", true, document); -SpecialPowers.addPermission("settings-api-read", true, document); -SpecialPowers.addPermission("settings-api-write", true, document); -SpecialPowers.addPermission("settings-clear", true, document); function onUnwantedSuccess() { ok(false, "onUnwantedSuccess: shouldn't get here"); diff --git a/dom/settings/tests/test_settings_events.html b/dom/settings/tests/test_settings_events.html index c96dff787201..5d85c9217a16 100644 --- a/dom/settings/tests/test_settings_events.html +++ b/dom/settings/tests/test_settings_events.html @@ -32,16 +32,8 @@ e = new MozSettingsEvent("settingchanged", {settingName: "a", settingValue: true is(e.settingName, "a", "Name should be a."); is(e.settingValue, true, "Value should be true."); -var e = new MozSettingsTransactionEvent("settingtransactionsuccess", {}); -ok(e, "Should have settings event!"); -is(e.error, "", "error should be null"); - -var e = new MozSettingsTransactionEvent("settingtransactionfailure", {error: "Test error."}); -ok(e, "Should have settings event!"); -is(e.error, "Test error.", "error should be 'Test error.'"); - - + \ No newline at end of file diff --git a/dom/settings/tests/test_settings_navigator_object.html b/dom/settings/tests/test_settings_navigator_object.html index 46ba5ec0f242..c5d5225cd25f 100644 --- a/dom/settings/tests/test_settings_navigator_object.html +++ b/dom/settings/tests/test_settings_navigator_object.html @@ -32,10 +32,7 @@ function testPref() { SpecialPowers.pushPermissions([ {type: "settings-read", allow: 0, context: document}, - {type: "settings-write", allow: 0, context: document}, - {type: "settings-api-read", allow: 0, context: document}, - {type: "settings-api-write", allow: 0, context: document}, - {type: "settings-clear", allow: 0, context: document} + {type: "settings-write", allow: 0, context: document} ], function() { ise(frames[0].navigator.mozSettings, null, "navigator.mozSettings is null when the page doesn't have permissions"); testPref(); diff --git a/dom/settings/tests/test_settings_onsettingchange.html b/dom/settings/tests/test_settings_onsettingchange.html index 311e4f7142bb..d24bdefcc969 100644 --- a/dom/settings/tests/test_settings_onsettingchange.html +++ b/dom/settings/tests/test_settings_onsettingchange.html @@ -22,14 +22,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=678695 "use strict"; if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } SpecialPowers.addPermission("settings-write", true, document); SpecialPowers.addPermission("settings-read", true, document); -SpecialPowers.addPermission("settings-api-read", true, document); -SpecialPowers.addPermission("settings-api-write", true, document); -SpecialPowers.addPermission("settings-clear", true, document); var screenBright = {"screen.brightness": 0.7}; @@ -92,12 +89,8 @@ var steps = [ req = lock.clear(); req.onsuccess = function () { ok(true, "Deleted the database"); - next(); }; req.onerror = onFailure; - }, - function () { - var lock = mozSettings.createLock(); req2 = lock.set(screenBright); req2.onsuccess = function () { ok(true, "set done"); @@ -274,19 +267,16 @@ var steps = [ req = lock.clear(); req.onsuccess = function () { ok(true, "Deleted the database"); - next(); + navigator.mozSettings.onsettingchange = onComplexSettingschangeWithNext; }; req.onerror = onFailure; - }, - function () { - var lock = mozSettings.createLock(); - navigator.mozSettings.onsettingchange = onComplexSettingschangeWithNext; req2 = navigator.mozSettings.createLock().set({'test.key': cset}); req2.onsuccess = function () { ok(true, "set done"); } req2.onerror = onFailure; }, + function () { ok(true, "all done!\n"); SimpleTest.finish(); diff --git a/dom/settings/tests/test_settings_permissions.html b/dom/settings/tests/test_settings_permissions.html deleted file mode 100644 index 946ef0985de0..000000000000 --- a/dom/settings/tests/test_settings_permissions.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - Test for Bug {678695} Settings API - - - - - - -Mozilla Bug {900551} -

- -
-
-
- - diff --git a/dom/settings/tests/test_settings_service.js b/dom/settings/tests/test_settings_service.js index 42f340acad12..dc8c6ea2be53 100644 --- a/dom/settings/tests/test_settings_service.js +++ b/dom/settings/tests/test_settings_service.js @@ -4,10 +4,6 @@ const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; -if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); -} - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); @@ -84,6 +80,8 @@ let tests = [ const TEST_OBSERVER_VALUE = true; const TEST_OBSERVER_MESSAGE = "test.observer.message"; + let observerCount = 2; + function observer(subject, topic, data) { if (topic !== MOZSETTINGS_CHANGED) { @@ -92,6 +90,7 @@ let tests = [ } data = JSON.parse(data); + function checkProp(name, type, value) { ok(name in data, "data." + name + " is present"); is(typeof data[name], type, "data." + name + " is " + type); @@ -100,16 +99,24 @@ let tests = [ checkProp("key", "string", TEST_OBSERVER_KEY); checkProp("value", "boolean", TEST_OBSERVER_VALUE); - checkProp("isInternalChange", "boolean", true); + if (observerCount === 2) { + checkProp("message", "object", null); + } else { + checkProp("message", "string", TEST_OBSERVER_MESSAGE); + } + --observerCount; + if (observerCount === 0) { Services.obs.removeObserver(this, MOZSETTINGS_CHANGED); next(); } + } Services.obs.addObserver(observer, MOZSETTINGS_CHANGED, false); let lock = SettingsService.createLock(); - lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null); + lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null, null); + lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null, TEST_OBSERVER_MESSAGE); } ]; diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js index 31b03ca66293..afcdb0dd2f08 100644 --- a/dom/system/gonk/RadioInterfaceLayer.js +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -3314,14 +3314,16 @@ RadioInterface.prototype = { * Set the setting value of "time.clock.automatic-update.available". */ setClockAutoUpdateAvailable: function(value) { - gSettingsService.createLock().set(kSettingsClockAutoUpdateAvailable, value, null); + gSettingsService.createLock().set(kSettingsClockAutoUpdateAvailable, value, null, + "fromInternalSetting"); }, /** * Set the setting value of "time.timezone.automatic-update.available". */ setTimezoneAutoUpdateAvailable: function(value) { - gSettingsService.createLock().set(kSettingsTimezoneAutoUpdateAvailable, value, null); + gSettingsService.createLock().set(kSettingsTimezoneAutoUpdateAvailable, value, null, + "fromInternalSetting"); }, /** @@ -3497,7 +3499,7 @@ RadioInterface.prototype = { switch (topic) { case kMozSettingsChangedObserverTopic: let setting = JSON.parse(data); - this.handleSettingsChange(setting.key, setting.value, setting.isInternalChange); + this.handleSettingsChange(setting.key, setting.value, setting.message); break; case kSysClockChangeObserverTopic: let offset = parseInt(data, 10); @@ -3578,11 +3580,11 @@ RadioInterface.prototype = { // ICC's mcc-mnc. _lastKnownHomeNetwork: null, - handleSettingsChange: function(aName, aResult, aIsInternalSetting) { + handleSettingsChange: function(aName, aResult, aMessage) { // Don't allow any content processes to modify the setting // "time.clock.automatic-update.available" except for the chrome process. if (aName === kSettingsClockAutoUpdateAvailable && - !aIsInternalSetting) { + aMessage !== "fromInternalSetting") { let isClockAutoUpdateAvailable = this._lastNitzMessage !== null || this._sntp.isAvailable(); if (aResult !== isClockAutoUpdateAvailable) { @@ -3598,7 +3600,7 @@ RadioInterface.prototype = { // "time.timezone.automatic-update.available" except for the chrome // process. if (aName === kSettingsTimezoneAutoUpdateAvailable && - !aIsInternalSetting) { + aMessage !== "fromInternalSetting") { let isTimezoneAutoUpdateAvailable = this._lastNitzMessage !== null; if (aResult !== isTimezoneAutoUpdateAvailable) { if (DEBUG) { diff --git a/dom/tests/mochitest/general/test_interfaces.html b/dom/tests/mochitest/general/test_interfaces.html index 7713754eb471..69ce1794cd44 100644 --- a/dom/tests/mochitest/general/test_interfaces.html +++ b/dom/tests/mochitest/general/test_interfaces.html @@ -706,8 +706,6 @@ var interfaceNamesInGlobalScope = {name: "mozRTCSessionDescription", pref: "media.peerconnection.enabled"}, // IMPORTANT: Do not change this list without review from a DOM peer! "MozSettingsEvent", -// IMPORTANT: Do not change this list without review from a DOM peer! - {name: "MozSettingsTransactionEvent", permission: "settings-api-read"}, // IMPORTANT: Do not change this list without review from a DOM peer! "MozSmsEvent", // IMPORTANT: Do not change this list without review from a DOM peer! diff --git a/dom/tests/mochitest/geolocation/test_mozsettings.html b/dom/tests/mochitest/geolocation/test_mozsettings.html index 4564aa6be142..6e3dcfeaf7b8 100644 --- a/dom/tests/mochitest/geolocation/test_mozsettings.html +++ b/dom/tests/mochitest/geolocation/test_mozsettings.html @@ -26,16 +26,12 @@ resume_geolocationProvider(function() { }); if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } function test1() { //This pushPermissions call is after pushPrefEnv call and pushPrefEnv calls follow after this - SpecialPowers.pushPermissions([{'type': 'settings-read', 'allow': true, 'context': document}, - {'type': 'settings-write', 'allow': true, 'context': document}, - {'type': 'settings-api-write', 'allow': true, 'context': document}, - {'type': 'settings-api-read', 'allow': true, 'context': document} - ], test2); + SpecialPowers.pushPermissions([{'type': 'settings-read', 'allow': true, 'context': document}, {'type': 'settings-write', 'allow': true, 'context': document}], test2); } function test2() { diff --git a/dom/tests/mochitest/geolocation/test_mozsettingsWatch.html b/dom/tests/mochitest/geolocation/test_mozsettingsWatch.html index b19f9a7d30e7..a99cd07e8cd9 100644 --- a/dom/tests/mochitest/geolocation/test_mozsettingsWatch.html +++ b/dom/tests/mochitest/geolocation/test_mozsettingsWatch.html @@ -26,16 +26,12 @@ resume_geolocationProvider(function() { }); if (SpecialPowers.isMainProcess()) { - SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); + SpecialPowers.Cu.import("resource://gre/modules/SettingsChangeNotifier.jsm"); } function test1() { //This pushPermissions call is after pushPrefEnv call and pushPrefEnv calls follow after this - SpecialPowers.pushPermissions([{'type': 'settings-read', 'allow': true, 'context': document}, - {'type': 'settings-write', 'allow': true, 'context': document}, - {'type': 'settings-api-write', 'allow': true, 'context': document}, - {'type': 'settings-api-read', 'allow': true, 'context': document} - ], test2); + SpecialPowers.pushPermissions([{'type': 'settings-read', 'allow': true, 'context': document}, {'type': 'settings-write', 'allow': true, 'context': document}], test2); } var watchId; diff --git a/dom/webidl/MozSettingsTransactionEvent.webidl b/dom/webidl/MozSettingsTransactionEvent.webidl deleted file mode 100644 index bc8955770558..000000000000 --- a/dom/webidl/MozSettingsTransactionEvent.webidl +++ /dev/null @@ -1,17 +0,0 @@ -/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -[Constructor(DOMString type, optional MozSettingsTransactionEventInit eventInitDict), - CheckPermissions="settings-api-read settings-api-write"] -interface MozSettingsTransactionEvent : Event -{ - readonly attribute DOMString? error; -}; - -dictionary MozSettingsTransactionEventInit : EventInit -{ - DOMString error = ""; -}; diff --git a/dom/webidl/SettingsManager.webidl b/dom/webidl/SettingsManager.webidl index e8c9ac196100..3ef2325f6378 100644 --- a/dom/webidl/SettingsManager.webidl +++ b/dom/webidl/SettingsManager.webidl @@ -6,7 +6,7 @@ [JSImplementation="@mozilla.org/settingsLock;1", Pref="dom.mozSettings.enabled"] -interface SettingsLock : EventTarget { +interface SettingsLock { // Whether this lock is invalid readonly attribute boolean closed; @@ -17,8 +17,6 @@ interface SettingsLock : EventTarget { DOMRequest get(DOMString name); DOMRequest clear(); - attribute EventHandler onsettingstransactionsuccess; - attribute EventHandler onsettingstransactionfailure; }; dictionary SettingChange { diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 4f1b1469a264..8e179f12e6fa 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -658,7 +658,6 @@ GENERATED_EVENTS_WEBIDL_FILES = [ 'MozMmsEvent.webidl', 'MozOtaStatusEvent.webidl', 'MozSettingsEvent.webidl', - 'MozSettingsTransactionEvent.webidl', 'MozSmsEvent.webidl', 'MozStkCommandEvent.webidl', 'PageTransitionEvent.webidl', diff --git a/dom/wifi/WifiWorker.js b/dom/wifi/WifiWorker.js index 4b6af426c8c3..4681796d3ea9 100644 --- a/dom/wifi/WifiWorker.js +++ b/dom/wifi/WifiWorker.js @@ -3511,7 +3511,8 @@ WifiWorker.prototype = { handleError: function(aErrorMessage) { self.requestDone(); } - }); + }, + "fromInternalSetting"); }, notifyTetheringOff: function notifyTetheringOff() { @@ -3531,7 +3532,8 @@ WifiWorker.prototype = { handleError: function(aErrorMessage) { self.requestDone(); } - }); + }, + "fromInternalSetting"); }, handleWifiEnabled: function(enabled) { @@ -3607,7 +3609,7 @@ WifiWorker.prototype = { let setting = JSON.parse(data); // To avoid WifiWorker setting the wifi again, don't need to deal with // the "mozsettings-changed" event fired from internal setting. - if (setting.isInternalChange) { + if (setting.message && setting.message === "fromInternalSetting") { return; }