summaryrefslogtreecommitdiffstats
path: root/dom/settings
diff options
context:
space:
mode:
Diffstat (limited to 'dom/settings')
-rw-r--r--dom/settings/SettingsDB.jsm249
-rw-r--r--dom/settings/SettingsManager.js506
-rw-r--r--dom/settings/SettingsManager.manifest5
-rw-r--r--dom/settings/SettingsRequestManager.jsm1224
-rw-r--r--dom/settings/SettingsService.js358
-rw-r--r--dom/settings/SettingsService.manifest5
-rw-r--r--dom/settings/moz.build28
-rw-r--r--dom/settings/tests/chrome.ini24
-rw-r--r--dom/settings/tests/file_bug1110872.html47
-rw-r--r--dom/settings/tests/file_bug1110872.js47
-rw-r--r--dom/settings/tests/file_loadserver.js17
-rw-r--r--dom/settings/tests/test_settings_basics.html816
-rw-r--r--dom/settings/tests/test_settings_blobs.html148
-rw-r--r--dom/settings/tests/test_settings_bug1110872.html17
-rw-r--r--dom/settings/tests/test_settings_data_uris.html149
-rw-r--r--dom/settings/tests/test_settings_events.html47
-rw-r--r--dom/settings/tests/test_settings_navigator_object.html37
-rw-r--r--dom/settings/tests/test_settings_observer_killer.html60
-rw-r--r--dom/settings/tests/test_settings_onsettingchange.html306
-rw-r--r--dom/settings/tests/test_settings_permissions.html184
-rw-r--r--dom/settings/tests/test_settings_service.js138
-rw-r--r--dom/settings/tests/test_settings_service.xul19
-rw-r--r--dom/settings/tests/test_settings_service_callback.js47
-rw-r--r--dom/settings/tests/test_settings_service_callback.xul19
-rw-r--r--dom/settings/tests/unit/test_settingsrequestmanager_messages.js174
-rw-r--r--dom/settings/tests/unit/xpcshell.ini6
26 files changed, 4677 insertions, 0 deletions
diff --git a/dom/settings/SettingsDB.jsm b/dom/settings/SettingsDB.jsm
new file mode 100644
index 000000000..b7d867a48
--- /dev/null
+++ b/dom/settings/SettingsDB.jsm
@@ -0,0 +1,249 @@
+/* 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";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.importGlobalProperties(['Blob', 'File']);
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"];
+
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsDB: " + s + "\n");
+}
+
+const TYPED_ARRAY_THINGS = new Set([
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+]);
+
+this.SETTINGSDB_NAME = "settings";
+this.SETTINGSDB_VERSION = 8;
+this.SETTINGSSTORE_NAME = "settings";
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+this.SettingsDB = function SettingsDB() {}
+
+SettingsDB.prototype = {
+
+ __proto__: IndexedDBHelper.prototype,
+
+ upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
+ let objectStore;
+ if (aOldVersion == 0) {
+ objectStore = aDb.createObjectStore(SETTINGSSTORE_NAME, { keyPath: "settingName" });
+ if (VERBOSE) debug("Created object stores");
+ } else if (aOldVersion == 1) {
+ if (VERBOSE) debug("Get object store for upgrade and remove old index");
+ objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME);
+ objectStore.deleteIndex("settingValue");
+ } else {
+ if (VERBOSE) debug("Get object store for upgrade");
+ objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME);
+ }
+
+ // Loading resource://app/defaults/settings.json doesn't work because
+ // settings.json is not in the omnijar.
+ // So we look for the app dir instead and go from here...
+ let settingsFile = FileUtils.getFile("DefRt", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ // On b2g desktop builds the settings.json file is moved in the
+ // profile directory by the build system.
+ settingsFile = FileUtils.getFile("ProfD", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ return;
+ }
+ }
+
+ let chan = NetUtil.newChannel({
+ uri: NetUtil.newURI(settingsFile),
+ loadUsingSystemPrincipal: true});
+ let stream = chan.open2();
+ // Obtain a converter to read from a UTF-8 encoded input stream.
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let rawstr = converter.ConvertToUnicode(NetUtil.readInputStreamToString(
+ stream,
+ stream.available()) || "");
+ let settings;
+ try {
+ settings = JSON.parse(rawstr);
+ } catch(e) {
+ if (DEBUG) debug("Error parsing " + settingsFile.path + " : " + e);
+ return;
+ }
+ stream.close();
+
+ objectStore.openCursor().onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ let value = cursor.value;
+ if (value.settingName in settings) {
+ if (VERBOSE) debug("Upgrade " +settings[value.settingName]);
+ value.defaultValue = this.prepareValue(settings[value.settingName]);
+ delete settings[value.settingName];
+ if ("settingValue" in value) {
+ value.userValue = this.prepareValue(value.settingValue);
+ delete value.settingValue;
+ }
+ cursor.update(value);
+ } else if ("userValue" in value || "settingValue" in value) {
+ value.defaultValue = undefined;
+ if (aOldVersion == 1 && value.settingValue) {
+ value.userValue = this.prepareValue(value.settingValue);
+ delete value.settingValue;
+ }
+ cursor.update(value);
+ } else {
+ cursor.delete();
+ }
+ cursor.continue();
+ } else {
+ for (let name in settings) {
+ let value = this.prepareValue(settings[name]);
+ if (VERBOSE) debug("Set new:" + name +", " + value);
+ objectStore.add({ settingName: name, defaultValue: value, userValue: undefined });
+ }
+ }
+ }.bind(this);
+ },
+
+ // If the value is a data: uri, convert it to a Blob.
+ convertDataURIToBlob: function(aValue) {
+ /* base64 to ArrayBuffer decoding, from
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
+ */
+ function b64ToUint6 (nChr) {
+ return nChr > 64 && nChr < 91 ?
+ nChr - 65
+ : nChr > 96 && nChr < 123 ?
+ nChr - 71
+ : nChr > 47 && nChr < 58 ?
+ nChr + 4
+ : nChr === 43 ?
+ 62
+ : nChr === 47 ?
+ 63
+ :
+ 0;
+ }
+
+ function base64DecToArr(sBase64, nBlocksSize) {
+ let sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""),
+ nInLen = sB64Enc.length,
+ nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize
+ : nInLen * 3 + 1 >> 2,
+ taBytes = new Uint8Array(nOutLen);
+
+ for (let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+ taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
+ }
+ nUint24 = 0;
+ }
+ }
+ return taBytes;
+ }
+
+ // Check if we have a data: uri, and if it's base64 encoded.
+ // data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...
+ if (typeof aValue == "string" && aValue.startsWith("data:")) {
+ try {
+ let uri = Services.io.newURI(aValue, null, null);
+ // XXX: that would be nice to reuse the c++ bits of the data:
+ // protocol handler instead.
+ let mimeType = "application/octet-stream";
+ let mimeDelim = aValue.indexOf(";");
+ if (mimeDelim !== -1) {
+ mimeType = aValue.substring(5, mimeDelim);
+ }
+ let start = aValue.indexOf(",") + 1;
+ let isBase64 = ((aValue.indexOf("base64") + 7) == start);
+ let payload = aValue.substring(start);
+
+ return new Blob([isBase64 ? base64DecToArr(payload) : payload],
+ { type: mimeType });
+ } catch(e) {
+ dump(e);
+ }
+ }
+ return aValue
+ },
+
+ getObjectKind: function(aObject) {
+ if (aObject === null || aObject === undefined) {
+ return "primitive";
+ } else if (Array.isArray(aObject)) {
+ return "array";
+ } else if (aObject instanceof File) {
+ return "file";
+ } else if (aObject instanceof Ci.nsIDOMBlob) {
+ return "blob";
+ } else if (aObject.constructor.name == "Date") {
+ return "date";
+ } else if (TYPED_ARRAY_THINGS.has(aObject.constructor.name)) {
+ return aObject.constructor.name;
+ } else if (typeof aObject == "object") {
+ return "object";
+ } else {
+ return "primitive";
+ }
+ },
+
+ // Makes sure any property that is a data: uri gets converted to a Blob.
+ prepareValue: function(aObject) {
+ let kind = this.getObjectKind(aObject);
+ if (kind == "array") {
+ let res = [];
+ aObject.forEach(function(aObj) {
+ res.push(this.prepareValue(aObj));
+ }, this);
+ return res;
+ } else if (kind == "file" || kind == "blob" || kind == "date") {
+ return aObject;
+ } else if (kind == "primitive") {
+ return this.convertDataURIToBlob(aObject);
+ }
+
+ // Fall-through, we now have a dictionary object.
+ let res = {};
+ for (let prop in aObject) {
+ res[prop] = this.prepareValue(aObject[prop]);
+ }
+ return res;
+ },
+
+ init: function init() {
+ this.initDBHelper(SETTINGSDB_NAME, SETTINGSDB_VERSION,
+ [SETTINGSSTORE_NAME]);
+ }
+}
diff --git a/dom/settings/SettingsManager.js b/dom/settings/SettingsManager.js
new file mode 100644
index 000000000..a73e5f312
--- /dev/null
+++ b/dom/settings/SettingsManager.js
@@ -0,0 +1,506 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsManager: " + s + "\n");
+}
+
+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");
+
+const kObserverSoftLimit = 10;
+
+/**
+ * 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 (VERBOSE) debug("settings lock init");
+ this._open = true;
+ this._settingsManager = aSettingsManager;
+ this._id = uuidgen.generateUUID().toString();
+
+ // 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"]);
+ let createLockPayload = {
+ lockID: this._id,
+ isServiceLock: false,
+ windowID: this._settingsManager.innerWindowID,
+ lockStack: (new Error).stack
+ };
+ this.sendMessage("Settings:CreateLock", createLockPayload);
+ Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+
+ // We only want to file closeHelper once per set of receiveMessage calls.
+ this._closeCalled = true;
+}
+
+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;
+ },
+
+ _closeHelper: function() {
+ if (VERBOSE) debug("closing lock " + this._id);
+ this._open = false;
+ this._closeCalled = false;
+ if (!this._requests || Object.keys(this._requests).length == 0) {
+ if (VERBOSE) debug("Requests exhausted, finalizing " + this._id);
+ this._settingsManager.unregisterLock(this._id);
+ this.sendMessage("Settings:Finalize", {lockID: this._id});
+ } else {
+ if (VERBOSE) debug("Requests left: " + Object.keys(this._requests).length);
+ this.sendMessage("Settings:Run", {lockID: this._id});
+ }
+ },
+
+
+ _wrap: function _wrap(obj) {
+ return Cu.cloneInto(obj, this._settingsManager._window);
+ },
+
+ sendMessage: function(aMessageName, aData) {
+ // sendMessage can be called after our window has died, or get
+ // queued to run later in a thread via _closeHelper, but the
+ // SettingsManager may have died in between the time it was
+ // scheduled and the time it runs. Make sure our window is valid
+ // before sending, otherwise just ignore.
+ if (!this._settingsManager._window) {
+ Cu.reportError(
+ "SettingsManager window died, cannot run settings transaction." +
+ " SettingsMessage: " + aMessageName +
+ " SettingsData: " + JSON.stringify(aData));
+ return;
+ }
+ cpmm.sendAsyncMessage(aMessageName,
+ aData,
+ undefined,
+ this._settingsManager._window.document.nodePrincipal);
+ },
+
+ 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;
+ }
+ if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
+
+ // Finalizing a transaction does not return a request ID since we are
+ // supposed to fire callbacks.
+ //
+ // We also destroy the DOMRequestHelper after we've received the
+ // finalize message. At this point, we will be guarenteed no more
+ // request returns are coming from the SettingsRequestManager.
+
+ if (!msg.requestID) {
+ let event;
+ switch (aMessage.name) {
+ case "Settings:Finalize:OK":
+ if (VERBOSE) debug("Lock finalize ok: " + this._id);
+ event = new this._window.MozSettingsTransactionEvent("settingstransactionsuccess", {});
+ this.__DOM_IMPL__.dispatchEvent(event);
+ this.destroyDOMRequestHelper();
+ break;
+ case "Settings:Finalize:KO":
+ if (DEBUG) debug("Lock finalize failed: " + this._id);
+ event = new this._window.MozSettingsTransactionEvent("settingstransactionfailure", {
+ error: msg.errorMsg
+ });
+ this.__DOM_IMPL__.dispatchEvent(event);
+ this.destroyDOMRequestHelper();
+ break;
+ default:
+ if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID");
+ }
+ return;
+ }
+
+
+ let req = this.getRequest(msg.requestID);
+ if (!req) {
+ if (DEBUG) debug("Matching request not found.");
+ return;
+ }
+ this.removeRequest(msg.requestID);
+ // DOMRequest callbacks called from here can die due to having
+ // things like marionetteScriptFinished in them. Make sure we file
+ // our call to run/finalize BEFORE opening the lock and fulfilling
+ // DOMRequests.
+ if (!this._closeCalled) {
+ // We only want to file closeHelper once per set of receiveMessage calls.
+ Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ this._closeCalled = true;
+ }
+ if (VERBOSE) 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);
+ }
+ },
+
+ get: function get(aName) {
+ if (VERBOSE) 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});
+ return req;
+ },
+
+ set: function set(aSettings) {
+ if (VERBOSE) 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});
+ return req;
+ },
+
+ clear: function clear() {
+ if (VERBOSE) 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});
+ return req;
+ },
+
+ classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"),
+ contractID: "@mozilla.org/settingsLock;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+function SettingsManager() {
+ this._callbacks = null;
+ this._isRegistered = false;
+ this._locks = [];
+ this._createdLocks = 0;
+ this._unregisteredLocks = 0;
+}
+
+SettingsManager.prototype = {
+ _wrap: function _wrap(obj) {
+ return Cu.cloneInto(obj, this._window);
+ },
+
+ set onsettingchange(aHandler) {
+ this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler);
+ this.checkMessageRegistration();
+ },
+
+ get onsettingchange() {
+ return this.__DOM_IMPL__.getEventHandler("onsettingchange");
+ },
+
+ createLock: function() {
+ let lock = new SettingsLock(this);
+ if (VERBOSE) debug("creating lock " + lock._id);
+ this._locks.push(lock._id);
+ this._createdLocks++;
+ return lock;
+ },
+
+ unregisterLock: function(aLockID) {
+ let lock_index = this._locks.indexOf(aLockID);
+ if (lock_index != -1) {
+ if (VERBOSE) debug("Unregistering lock " + aLockID);
+ this._locks.splice(lock_index, 1);
+ this._unregisteredLocks++;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("Settings::receiveMessage: " + aMessage.name);
+ let msg = aMessage.json;
+
+ switch (aMessage.name) {
+ case "Settings:Change:Return:OK":
+ if (VERBOSE) debug('data:' + msg.key + ':' + msg.value + '\n');
+
+ let event = new this._window.MozSettingsEvent("settingchange", this._wrap({
+ settingName: msg.key,
+ settingValue: msg.value
+ }));
+ this.__DOM_IMPL__.dispatchEvent(event);
+
+ if (this._callbacks && this._callbacks[msg.key]) {
+ if (VERBOSE) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length);
+ this._callbacks[msg.key].forEach(function(cb) {
+ cb(this._wrap({settingName: msg.key, settingValue: msg.value}));
+ }.bind(this));
+ } else {
+ if (VERBOSE) debug("no observers stored!");
+ }
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ },
+
+ // 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 (VERBOSE) 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 (VERBOSE) 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 (VERBOSE) debug("addObserver " + aName);
+
+ if (!this._callbacks) {
+ this._callbacks = {};
+ }
+
+ if (!this._callbacks[aName]) {
+ this._callbacks[aName] = [aCallback];
+ } else {
+ this._callbacks[aName].push(aCallback);
+ }
+
+ let length = this._callbacks[aName].length;
+ if (length >= kObserverSoftLimit) {
+ debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " +
+ aName + ": " + length + " FROM" + (new Error).stack);
+#ifdef DEBUG
+ debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!");
+ throw Components.results.NS_ERROR_ABORT;
+#endif
+ }
+
+ this.checkMessageRegistration();
+ },
+
+ removeObserver: function removeObserver(aName, aCallback) {
+ if (VERBOSE) debug("deleteObserver " + aName);
+ if (this._callbacks && this._callbacks[aName]) {
+ 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];
+ }
+ } else {
+ if (VERBOSE) debug("Callback not found for: " + aName);
+ }
+ } else {
+ if (VERBOSE) debug("No observers stored for " + aName);
+ }
+ this.checkMessageRegistration();
+ },
+
+ init: function(aWindow) {
+ if (VERBOSE) debug("SettingsManager init");
+ mrm.registerStrongReporter(this);
+ cpmm.addMessageListener("Settings:Change:Return:OK", this);
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+ let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ this.innerWindowID = util.currentInnerWindowID;
+ this._window = aWindow;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("Topic: " + aTopic);
+ if (aTopic === "inner-window-destroyed") {
+ let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (wId === this.innerWindowID) {
+ if (DEBUG) debug("Received: inner-window-destroyed for valid innerWindowID=" + wId + ", cleanup.");
+ this.cleanup();
+ }
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ for (let topic in this._callbacks) {
+ let length = this._callbacks[topic].length;
+ if (length == 0) {
+ continue;
+ }
+
+ let path;
+ if (length < kObserverSoftLimit) {
+ path = "settings-observers";
+ } else {
+ path = "settings-observers-suspect/referent(topic=" +
+ (aAnonymize ? "<anonymized>" : topic) + ")";
+ }
+
+ aCallback.callback("", path,
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ length,
+ "The number of settings observers for this topic.",
+ aData);
+ }
+
+ aCallback.callback("",
+ "settings-locks/alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._locks.length,
+ "The number of locks that are currently alives.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/created",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._createdLocks,
+ "The number of locks that were created.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/deleted",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._unregisteredLocks,
+ "The number of locks that were deleted.",
+ aData);
+ },
+
+ cleanup: function() {
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ // At this point, the window is dying, so there's nothing left
+ // that we could do with our lock. Go ahead and run finalize on
+ // it to make sure changes are commited.
+ for (let i = 0; i < this._locks.length; ++i) {
+ if (DEBUG) debug("Lock alive at destroy, finalizing: " + this._locks[i]);
+ // Due to bug 1105511 we should be able to send this without
+ // cached principals. However, this is scary because any iframe
+ // in the process could run this?
+ cpmm.sendAsyncMessage("Settings:Finalize",
+ {lockID: this._locks[i]});
+ }
+ cpmm.removeMessageListener("Settings:Change:Return:OK", this);
+ mrm.unregisterStrongReporter(this);
+ this.innerWindowID = null;
+ this._window = null;
+ },
+
+ classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"),
+ contractID: "@mozilla.org/settingsManager;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIObserver,
+ Ci.nsIMemoryReporter]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]);
diff --git a/dom/settings/SettingsManager.manifest b/dom/settings/SettingsManager.manifest
new file mode 100644
index 000000000..fc9612928
--- /dev/null
+++ b/dom/settings/SettingsManager.manifest
@@ -0,0 +1,5 @@
+component {c40b1c70-00fb-11e2-a21f-0800200c9a66} SettingsManager.js
+contract @mozilla.org/settingsManager;1 {c40b1c70-00fb-11e2-a21f-0800200c9a66}
+
+component {60c9357c-3ae0-4222-8f55-da01428470d5} SettingsManager.js
+contract @mozilla.org/settingsLock;1 {60c9357c-3ae0-4222-8f55-da01428470d5}
diff --git a/dom/settings/SettingsRequestManager.jsm b/dom/settings/SettingsRequestManager.jsm
new file mode 100644
index 000000000..fced80247
--- /dev/null
+++ b/dom/settings/SettingsRequestManager.jsm
@@ -0,0 +1,1224 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.importGlobalProperties(['File']);
+
+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");
+
+var DEBUG = false;
+var VERBOSE = false;
+var TRACK = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.verbose.enabled");
+ TRACK =
+ Services.prefs.getBoolPref("dom.mozSettings.trackTasksUsage");
+} catch (ex) { }
+
+var allowForceReadOnly = false;
+try {
+ allowForceReadOnly = Services.prefs.getBoolPref("dom.mozSettings.allowForceReadOnly");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsRequestManager: " + s + "\n");
+}
+
+var inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const kXpcomShutdownObserverTopic = "xpcom-shutdown";
+const kInnerWindowDestroyed = "inner-window-destroyed";
+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;
+
+// Time, in seconds, to consider the API is starting to jam
+var kSoftLockupDelta = 30;
+try {
+ kSoftLockupDelta = Services.prefs.getIntPref("dom.mozSettings.softLockupDelta");
+} catch (ex) { }
+
+XPCOMUtils.defineLazyServiceGetter(this, "mrm",
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager");
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var SettingsPermissions = {
+ checkPermission: function(aPrincipal, aPerm) {
+ if (!aPrincipal) {
+ Cu.reportError("SettingsPermissions.checkPermission was passed a null principal. Denying all permissions.");
+ return false;
+ }
+ if (aPrincipal.origin == "[System Principal]" ||
+ Services.perms.testExactPermissionFromPrincipal(aPrincipal, aPerm) == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ return true;
+ }
+ return false;
+ },
+ hasAllReadPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kAllSettingsReadPermission);
+ },
+ hasAllWritePermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kAllSettingsWritePermission);
+ },
+ hasSomeReadPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSomeSettingsReadPermission);
+ },
+ hasSomeWritePermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSomeSettingsWritePermission);
+ },
+ hasClearPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSettingsClearPermission);
+ },
+ hasReadPermission: function(aPrincipal, aSettingsName) {
+ return this.hasAllReadPermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsReadSuffix);
+ },
+ hasWritePermission: function(aPrincipal, aSettingsName) {
+ return this.hasAllWritePermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsWriteSuffix);
+ }
+};
+
+
+function SettingsLockInfo(aDB, aMsgMgr, aPrincipal, aLockID, aIsServiceLock, aWindowID, aLockStack) {
+ return {
+ // ID Shared with the object on the child side
+ lockID: aLockID,
+ // Is this a content lock or a settings service lock?
+ isServiceLock: aIsServiceLock,
+ // Which inner window ID
+ windowID: aWindowID,
+ // Where does this lock comes from
+ lockStack: aLockStack,
+ // 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,
+ // forceReadOnly sets whether we want to do a read only transaction. Define
+ // true by default, and let queueTask() set this to false if we queue any
+ // "set" task. Since users of settings locks will queue all tasks before
+ // any idb transaction is created, we know we will have all needed
+ // information to set this before creating a transaction.
+ forceReadOnly: true,
+ // Principal the lock was created under. We assume that the lock
+ // will continue to exist under this principal for the duration of
+ // its lifetime.
+ principal: aPrincipal,
+ getObjectStore: function() {
+ if (VERBOSE) 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 (VERBOSE) debug("Current transaction for " + this.lockID + " closed, trying to create new one.");
+ } else {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ 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.
+ let canReadOnly = allowForceReadOnly && this.forceReadOnly;
+ if (canReadOnly || !SettingsPermissions.hasSomeWritePermission(this.principal)) {
+ if (VERBOSE) debug("Making READONLY transaction for " + this.lockID);
+ this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly");
+ } else {
+ if (VERBOSE) debug("Making READWRITE transaction for " + this.lockID);
+ this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite");
+ }
+ this._transaction.oncomplete = function() {
+ if (VERBOSE) 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 {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ throw e;
+ }
+ }
+ return store;
+ }
+ };
+}
+
+var 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: {},
+ // Storing soft lockup detection infos
+ softLockup: {
+ lockId: null, // last lock dealt with
+ lockTs: null // last time of dealing with
+ },
+ // 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: [],
+ // Since we need to call observers at times when we may not have
+ // just received a message from a child process, we cache principals
+ // for message managers and check permissions on them before we send
+ // settings notifications to child processes.
+ observerPrincipalCache: new Map(),
+ totalProcessed: 0,
+ tasksConsumed: {},
+ totalSetProcessed: 0,
+ tasksSetConsumed: {},
+ totalGetProcessed: 0,
+ tasksGetConsumed: {},
+
+ init: function() {
+ if (VERBOSE) debug("init");
+ this.settingsDB.init();
+ this.messages.forEach((function(msgName) {
+ ppmm.addMessageListener(msgName, this);
+ }).bind(this));
+ Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
+ Services.obs.addObserver(this, kInnerWindowDestroyed, false);
+ mrm.registerStrongReporter(this);
+ },
+
+ _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
+ function needsUUID(aValue) {
+ if (!aValue || !aValue.constructor) {
+ return false;
+ }
+ return (aValue.constructor.name == "Date") || (aValue instanceof File) ||
+ (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 (VERBOSE) debug("Queueing task: " + aOperation);
+
+ let defer = {};
+
+ let lock = this.lockInfo[aData.lockID];
+
+ if (!lock) {
+ return Promise.reject({error: "Lock already dead, cannot queue task"});
+ }
+
+ if (aOperation == "set") {
+ aData.settings = this._serializePreservingBinaries(aData.settings);
+ }
+
+ if (aOperation === "set" || aOperation === "clear") {
+ lock.forceReadOnly = false;
+ }
+
+ lock.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 (VERBOSE) debug("Making task queuing transaction request.");
+ let data = aTask.data;
+ let lock = this.lockInfo[data.lockID];
+ let store = lock.getObjectStore(lock.principal);
+ 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 (VERBOSE) 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) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ 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.principal, 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 (VERBOSE) 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 (VERBOSE) debug("Making get transaction request for " + data.name);
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID);
+ return Promise.reject({task: aTask, error: "Cannot get object store"});
+ }
+
+ if (VERBOSE) 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 (VERBOSE) debug("Request for '" + data.name + "' successful. " +
+ "Record count: " + event.target.result.length);
+
+ if (event.target.result.length == 0) {
+ if (VERBOSE) 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 (VERBOSE) 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) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ 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.principal, 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 (VERBOSE) 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});
+ },
+
+ startRunning: function(aLockID) {
+ let lock = this.lockInfo[aLockID];
+
+ if (!lock) {
+ if (DEBUG) debug("Lock no longer alive, cannot start running");
+ return;
+ }
+
+ lock.consumable = true;
+ if (aLockID == this.settingsLockQueue[0] || this.settingsLockQueue.length == 0) {
+ // If a lock is currently at the head of the queue, run all tasks for
+ // it.
+ if (VERBOSE) debug("Start running tasks for " + aLockID);
+ 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 (VERBOSE) debug("Queuing tasks for " + aLockID + " while waiting for " + this.settingsLockQueue[0]);
+ }
+ },
+
+ 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);
+ }
+ },
+
+ finalizeSets: function(aTask) {
+ let data = aTask.data;
+ if (VERBOSE) debug("Finalizing tasks for lock " + data.lockID);
+ let lock = this.lockInfo[data.lockID];
+
+ if (!lock) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ lock.finalizing = true;
+ if (lock._failed) {
+ this.removeLock(data.lockID);
+ 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 (VERBOSE) debug("Clear was called on lock, skipping finalize");
+ this.removeLock(data.lockID);
+ return Promise.resolve({task: aTask});
+ }
+ let keys = Object.getOwnPropertyNames(lock.queuedSets);
+ if (keys.length === 0) {
+ if (VERBOSE) debug("Nothing to finalize. Exiting.");
+ this.removeLock(data.lockID);
+ return Promise.resolve({task: aTask});
+ }
+
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID);
+ this.removeLock(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 (VERBOSE) 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 (VERBOSE) 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 (VERBOSE) debug("Set successful!");
+ if (VERBOSE) 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.removeLock(data.lockID);
+ defer.resolve({task: aTask});
+ }.bind(this), function(ret) {
+ this.removeLock(data.lockID);
+ 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 (VERBOSE) 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.principal)) {
+ 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.getObjectStore(lock.principal);
+ 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 (VERBOSE) 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 (VERBOSE) debug("Running tasks for " + aLockID);
+ let lock = this.lockInfo[aLockID];
+ if (!lock) {
+ if (DEBUG) debug("Lock no longer alive, cannot run tasks");
+ return;
+ }
+ let currentTask = lock.tasks.shift();
+ let promises = [];
+ if (TRACK) {
+ if (this.tasksConsumed[aLockID] === undefined) {
+ this.tasksConsumed[aLockID] = 0;
+ this.tasksGetConsumed[aLockID] = 0;
+ this.tasksSetConsumed[aLockID] = 0;
+ }
+ }
+ while (currentTask) {
+ if (VERBOSE) debug("Running Operation " + currentTask.operation);
+ if (lock.finalizing) {
+ // We should really never get to this point, but if we do,
+ // fail every task that happens.
+ Cu.reportError("Settings lock trying to run more tasks after finalizing. Ignoring tasks, but this is bad. Lock: " + aLockID);
+ currentTask.defer.reject("Cannot call new task after finalizing");
+ } else {
+ let p;
+ this.totalProcessed++;
+ if (TRACK) {
+ this.tasksConsumed[aLockID]++;
+ }
+ switch (currentTask.operation) {
+ case "get":
+ this.totalGetProcessed++;
+ if (TRACK) {
+ this.tasksGetConsumed[aLockID]++;
+ }
+ p = this.taskGet(currentTask);
+ break;
+ case "set":
+ this.totalSetProcessed++;
+ if (TRACK) {
+ this.tasksSetConsumed[aLockID]++;
+ }
+ 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("results" in ret ? ret.results : null);
+ }.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 (VERBOSE) debug("Nothing to run!");
+ return;
+ }
+
+ let lockID = this.settingsLockQueue[0];
+ if (VERBOSE) 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 no longer alive, cannot consume tasks");
+ this.queueConsume();
+ return;
+ }
+
+ if (!lock.consumable || lock.tasks.length === 0) {
+ if (VERBOSE) debug("No more tasks to run or not yet consuamble.");
+ return;
+ }
+
+ lock.consumable = false;
+ this.ensureConnection().then(
+ function(task) {
+ this.runTasks(lockID);
+ this.updateSoftLockup(lockID);
+ }.bind(this), function(ret) {
+ dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n");
+ });
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("observe: " + aTopic);
+ switch (aTopic) {
+ case kXpcomShutdownObserverTopic:
+ this.messages.forEach((function(msgName) {
+ ppmm.removeMessageListener(msgName, this);
+ }).bind(this));
+ Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
+ ppmm = null;
+ mrm.unregisterStrongReporter(this);
+ break;
+
+ case kInnerWindowDestroyed:
+ let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ this.forceFinalizeChildLocksNonOOP(wId);
+ break;
+
+ default:
+ if (DEBUG) debug("Wrong observer topic: " + aTopic);
+ break;
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ let length = lock.tasks.length;
+
+ if (length === 0) {
+ continue;
+ }
+
+ let path = "settings-locks/tasks/lock(id=" + lockId + ")/";
+
+ aCallback.callback("", path + "alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ length,
+ "Alive tasks for this lock",
+ aData);
+ }
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/processed",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalProcessed,
+ "The total number of tasks that were executed.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/set",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalSetProcessed,
+ "The total number of set tasks that were executed.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/get",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalGetProcessed,
+ "The total number of get tasks that were executed.",
+ aData);
+
+ // if TRACK is not enabled, then, no details are available
+ if (!TRACK) {
+ return;
+ }
+
+ for (let lockId of Object.keys(this.tasksConsumed)) {
+ let lock = this.lockInfo[lockId];
+ let length = 0;
+ if (lock) {
+ length = lock.tasks.length;
+ }
+
+ let path = "settings-locks/tasks/lock(id=" + lockId + ")/";
+
+ aCallback.callback("", path + "set",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksSetConsumed[lockId],
+ "Set tasks for this lock.",
+ aData);
+
+ aCallback.callback("", path + "get",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksGetConsumed[lockId],
+ "Get tasks for this lock.",
+ aData);
+
+ aCallback.callback("", path + "processed",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksConsumed[lockId],
+ "Number of tasks that were executed.",
+ aData);
+ }
+ },
+
+ sendSettingsChange: function(aKey, aValue, aIsServiceLock) {
+ this.broadcastMessage("Settings:Change:Return:OK",
+ { key: aKey, value: aValue });
+ var setting = {
+ key: aKey,
+ value: aValue,
+ isInternalChange: aIsServiceLock
+ };
+ setting.wrappedJSObject = setting;
+ Services.obs.notifyObservers(setting, kMozSettingsChangedObserverTopic, "");
+ },
+
+ broadcastMessage: function broadcastMessage(aMsgName, aContent) {
+ if (VERBOSE) debug("Broadcast");
+ this.children.forEach(function(msgMgr) {
+ let principal = this.observerPrincipalCache.get(msgMgr);
+ if (!principal) {
+ if (DEBUG) debug("Cannot find principal for message manager to check permissions");
+ }
+ else if (SettingsPermissions.hasReadPermission(principal, aContent.key)) {
+ try {
+ msgMgr.sendAsyncMessage(aMsgName, aContent);
+ } catch (e) {
+ if (DEBUG) debug("Failed sending message: " + aMsgName);
+ }
+ }
+ }.bind(this));
+ if (VERBOSE) debug("Finished Broadcasting");
+ },
+
+ addObserver: function(aMsgMgr, aPrincipal) {
+ if (VERBOSE) debug("Add observer for " + aPrincipal.origin);
+ if (this.children.indexOf(aMsgMgr) == -1) {
+ this.children.push(aMsgMgr);
+ this.observerPrincipalCache.set(aMsgMgr, aPrincipal);
+ }
+ },
+
+ removeObserver: function(aMsgMgr) {
+ if (VERBOSE) {
+ let principal = this.observerPrincipalCache.get(aMsgMgr);
+ if (principal) {
+ debug("Remove observer for " + principal.origin);
+ }
+ }
+ let index = this.children.indexOf(aMsgMgr);
+ if (index != -1) {
+ this.children.splice(index, 1);
+ this.observerPrincipalCache.delete(aMsgMgr);
+ }
+ if (VERBOSE) debug("Principal/MessageManager pairs left in observer cache: " + this.observerPrincipalCache.size);
+ },
+
+ removeLock: function(aLockID) {
+ if (VERBOSE) debug("Removing lock " + aLockID);
+ if (this.lockInfo[aLockID]) {
+ let transaction = this.lockInfo[aLockID]._transaction;
+ if (transaction) {
+ try {
+ transaction.abort();
+ } catch (e) {
+ if (e.name == "InvalidStateError") {
+ if (VERBOSE) debug("Transaction for " + aLockID + " closed already");
+ } else {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ throw e;
+ }
+ }
+ }
+ delete this.lockInfo[aLockID];
+ }
+ let index = this.settingsLockQueue.indexOf(aLockID);
+ 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();
+ }
+ },
+
+ hasLockFinalizeTask: function(lock) {
+ // Go in reverse order because finalize should be the last one
+ for (let task_index = lock.tasks.length; task_index >= 0; task_index--) {
+ if (lock.tasks[task_index]
+ && lock.tasks[task_index].operation === "finalize") {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ enqueueForceFinalize: function(lock) {
+ if (!this.hasLockFinalizeTask(lock)) {
+ if (VERBOSE) debug("Alive lock has pending tasks: " + lock.lockID);
+ this.queueTask("finalize", {lockID: lock.lockID}).then(
+ function() {
+ if (VERBOSE) debug("Alive lock " + lock.lockID + " succeeded to force-finalize");
+ },
+ function(error) {
+ if (DEBUG) debug("Alive lock " + lock.lockID + " failed to force-finalize due to error: " + error);
+ }
+ );
+ // Finalize is considered a task running situation, but it also needs to
+ // queue a task.
+ this.startRunning(lock.lockID);
+ }
+ },
+
+ forceFinalizeChildLocksNonOOP: function(windowId) {
+ if (VERBOSE) debug("Forcing finalize on child locks, non OOP");
+
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ if (lock.windowID === windowId) {
+ this.enqueueForceFinalize(lock);
+ }
+ }
+ },
+
+ forceFinalizeChildLocksOOP: function(aMsgMgr) {
+ if (VERBOSE) debug("Forcing finalize on child locks, OOP");
+
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ if (lock._mm === aMsgMgr) {
+ this.enqueueForceFinalize(lock);
+ }
+ }
+ },
+
+ updateSoftLockup: function(aLockId) {
+ if (VERBOSE) debug("Treating lock " + aLockId + ", so updating soft lockup infos ...");
+
+ this.softLockup = {
+ lockId: aLockId,
+ lockTs: new Date()
+ };
+ },
+
+ checkSoftLockup: function() {
+ if (VERBOSE) debug("Checking for soft lockup ...");
+
+ if (this.settingsLockQueue.length === 0) {
+ if (VERBOSE) debug("Empty settings lock queue, no soft lockup ...");
+ return;
+ }
+
+ let head = this.settingsLockQueue[0];
+ if (head !== this.softLockup.lockId) {
+ if (VERBOSE) debug("Non matching head of settings lock queue, no soft lockup ...");
+ return;
+ }
+
+ let delta = (new Date() - this.softLockup.lockTs) / 1000;
+ if (delta < kSoftLockupDelta) {
+ if (VERBOSE) debug("Matching head of settings lock queue, but delta (" + delta + ") < 30 secs, no soft lockup ...");
+ return;
+ }
+
+ let msgBlocked = "Settings queue head blocked at " + head +
+ " for " + delta + " secs, Settings API may be soft lockup. Lock from: " +
+ this.lockInfo[head].lockStack;
+ Cu.reportError(msgBlocked);
+ if (DEBUG) debug(msgBlocked);
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("receiveMessage " + aMessage.name + ": " + JSON.stringify(aMessage.data));
+
+ let msg = aMessage.data;
+ let mm = aMessage.target;
+
+ function returnMessage(name, data) {
+ if (mm) {
+ try {
+ mm.sendAsyncMessage(name, data);
+ } catch (e) {
+ if (DEBUG) debug("Return message failed, " + name + ": " + e);
+ }
+ } else {
+ try {
+ gSettingsService.receiveMessage({ name: name, data: data });
+ } catch (e) {
+ if (DEBUG) debug("Direct return message failed, " + name + ": " + e);
+ }
+ }
+ }
+
+ // 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":
+ this.checkSoftLockup();
+ let kill_process = false;
+ if (!msg.lockID) {
+ Cu.reportError("Process sending request for lock that does not exist. Killing.");
+ kill_process = true;
+ }
+ else if (!this.lockInfo[msg.lockID]) {
+ if (DEBUG) debug("Cannot find lock ID " + msg.lockID);
+ // This doesn't kill, because we can have things that file
+ // finalize, then die, and we may get the observer
+ // notification before we get the IPC messages.
+ return;
+ }
+ else if (mm != this.lockInfo[msg.lockID]._mm) {
+ Cu.reportError("Process trying to access settings lock from another process. Killing.");
+ kill_process = true;
+ }
+ if (kill_process) {
+ // 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 (VERBOSE) debug("Child process shutdown received.");
+ this.forceFinalizeChildLocksOOP(mm);
+ this.removeObserver(mm);
+ break;
+ case "Settings:RegisterForMessages":
+ if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) {
+ Cu.reportError("Settings message " + aMessage.name +
+ " from a content process with no 'settings-api-read' privileges.");
+ aMessage.target.assertPermission("message-manager-no-read-kill");
+ return;
+ }
+ this.addObserver(mm, aMessage.principal);
+ break;
+ case "Settings:UnregisterForMessages":
+ this.removeObserver(mm);
+ break;
+ case "Settings:CreateLock":
+ if (VERBOSE) debug("Received CreateLock for " + msg.lockID + " from " + aMessage.principal.origin + " window: " + msg.windowID);
+ // 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;
+ }
+
+ if (this.softLockup.lockId === null) {
+ this.updateSoftLockup(msg.lockID);
+ }
+
+ this.settingsLockQueue.push(msg.lockID);
+ this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB,
+ mm,
+ aMessage.principal,
+ msg.lockID,
+ msg.isServiceLock,
+ msg.windowID,
+ msg.lockStack);
+ break;
+ case "Settings:Get":
+ if (VERBOSE) debug("Received getRequest from " + msg.lockID);
+ 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 (VERBOSE) debug("Received Set Request from " + msg.lockID);
+ 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 (VERBOSE) debug("Received Clear Request from " + msg.lockID);
+ 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 (VERBOSE) 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 (VERBOSE) debug("Received Run");
+ this.startRunning(msg.lockID);
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ }
+};
+
+// This code should ALWAYS be living only on the parent side.
+if (!inParent) {
+ debug("SettingsRequestManager should be living on parent side.");
+ throw Cr.NS_ERROR_ABORT;
+} else {
+ this.SettingsRequestManager = SettingsRequestManager;
+ SettingsRequestManager.init();
+}
diff --git a/dom/settings/SettingsService.js b/dom/settings/SettingsService.js
new file mode 100644
index 000000000..09bd3ca72
--- /dev/null
+++ b/dom/settings/SettingsService.js
@@ -0,0 +1,358 @@
+/* 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/SettingsRequestManager.jsm');
+
+/* static functions */
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsService.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsService.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsService: " + s + "\n");
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+XPCOMUtils.defineLazyServiceGetter(this, "mrm",
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager");
+
+const nsIClassInfo = Ci.nsIClassInfo;
+const kXpcomShutdownObserverTopic = "xpcom-shutdown";
+
+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
+ };
+};
+
+const kLockListeners = ["Settings:Get:OK", "Settings:Get:KO",
+ "Settings:Clear:OK", "Settings:Clear:KO",
+ "Settings:Set:OK", "Settings:Set:KO",
+ "Settings:Finalize:OK", "Settings:Finalize:KO"];
+
+function SettingsServiceLock(aSettingsService, aTransactionCallback) {
+ if (VERBOSE) debug("settingsServiceLock constr!");
+ this._open = true;
+ this._settingsService = aSettingsService;
+ this._id = uuidgen.generateUUID().toString();
+ this._transactionCallback = aTransactionCallback;
+ this._requests = {};
+ let closeHelper = function() {
+ if (VERBOSE) debug("closing lock " + this._id);
+ this._open = false;
+ this.runOrFinalizeQueries();
+ }.bind(this);
+
+ this.addListeners();
+
+ let createLockPayload = {
+ lockID: this._id,
+ isServiceLock: true,
+ windowID: undefined,
+ lockStack: (new Error).stack
+ };
+
+ this.returnMessage("Settings:CreateLock", createLockPayload);
+ Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+SettingsServiceLock.prototype = {
+ get closed() {
+ return !this._open;
+ },
+
+ addListeners: function() {
+ for (let msg of kLockListeners) {
+ cpmm.addMessageListener(msg, this);
+ }
+ },
+
+ removeListeners: function() {
+ for (let msg of kLockListeners) {
+ cpmm.removeMessageListener(msg, this);
+ }
+ },
+
+ returnMessage: function(aMessage, aData) {
+ SettingsRequestManager.receiveMessage({
+ name: aMessage,
+ data: aData,
+ target: undefined,
+ principal: Services.scriptSecurityManager.getSystemPrincipal()
+ });
+ },
+
+ runOrFinalizeQueries: function() {
+ if (!this._requests || Object.keys(this._requests).length == 0) {
+ this.returnMessage("Settings:Finalize", {lockID: this._id});
+ } else {
+ this.returnMessage("Settings:Run", {lockID: this._id});
+ }
+ },
+
+ 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;
+ }
+ if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
+ // 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 (VERBOSE) 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");
+ }
+
+ this._settingsService.unregisterLock(this._id);
+ 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 (VERBOSE) debug("get (" + this._id + "): " + aName);
+ if (!this._open) {
+ if (DEBUG) debug("Settings lock not open!\n");
+ throw Components.results.NS_ERROR_ABORT;
+ }
+ let reqID = uuidgen.generateUUID().toString();
+ this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName);
+ this.returnMessage("Settings:Get", {requestID: reqID,
+ lockID: this._id,
+ name: aName});
+ },
+
+ set: function set(aName, aValue, aCallback) {
+ if (VERBOSE) 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;
+ this.returnMessage("Settings:Set", {requestID: reqID,
+ lockID: this._id,
+ settings: settings});
+ },
+
+ callHandle: function callHandle(aCallback, aName, aValue) {
+ try {
+ aCallback && aCallback.handle ? aCallback.handle(aName, aValue) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'handle' for " + aName + " callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callAbort: function callAbort(aCallback, aMessage) {
+ try {
+ aCallback && aCallback.handleAbort ? aCallback.handleAbort(aMessage) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'abort' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callError: function callError(aCallback, aMessage) {
+ try {
+ aCallback && aCallback.handleError ? aCallback.handleError(aMessage) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'error' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callTransactionHandle: function callTransactionHandle() {
+ try {
+ this._transactionCallback && this._transactionCallback.handle ? this._transactionCallback.handle() : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'Transaction handle' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ classID : SETTINGSSERVICELOCK_CID,
+ QueryInterface : XPCOMUtils.generateQI([nsISettingsServiceLock])
+};
+
+const SETTINGSSERVICE_CID = Components.ID("{f656f0c0-f776-11e1-a21f-0800200c9a66}");
+
+function SettingsService()
+{
+ if (VERBOSE) debug("settingsService Constructor");
+ this._locks = [];
+ this._serviceLocks = {};
+ this._createdLocks = 0;
+ this._unregisteredLocks = 0;
+ this.init();
+}
+
+SettingsService.prototype = {
+
+ init: function() {
+ Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
+ mrm.registerStrongReporter(this);
+ },
+
+ uninit: function() {
+ Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
+ mrm.unregisterStrongReporter(this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("observe: " + aTopic);
+ if (aTopic === kXpcomShutdownObserverTopic) {
+ this.uninit();
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("Entering receiveMessage");
+
+ let lockID = aMessage.data.lockID;
+ if (!lockID) {
+ if (DEBUG) debug("No lock ID");
+ return;
+ }
+
+ if (!(lockID in this._serviceLocks)) {
+ if (DEBUG) debug("Received message for lock " + lockID + " but no lock");
+ return;
+ }
+
+ if (VERBOSE) debug("Delivering message");
+ this._serviceLocks[lockID].receiveMessage(aMessage);
+ },
+
+ createLock: function createLock(aCallback) {
+ if (VERBOSE) debug("Calling createLock");
+ var lock = new SettingsServiceLock(this, aCallback);
+ if (VERBOSE) debug("Created lock " + lock._id);
+ this.registerLock(lock);
+ return lock;
+ },
+
+ registerLock: function(aLock) {
+ if (VERBOSE) debug("Registering lock " + aLock._id);
+ this._locks.push(aLock._id);
+ this._serviceLocks[aLock._id] = aLock;
+ this._createdLocks++;
+ },
+
+ unregisterLock: function(aLockID) {
+ let lock_index = this._locks.indexOf(aLockID);
+ if (lock_index != -1) {
+ if (VERBOSE) debug("Unregistering lock " + aLockID);
+ this._locks.splice(lock_index, 1);
+ this._serviceLocks[aLockID].removeListeners();
+ this._serviceLocks[aLockID] = null;
+ delete this._serviceLocks[aLockID];
+ this._unregisteredLocks++;
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ aCallback.callback("",
+ "settings-service-locks/alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._locks.length,
+ "The number of service locks that are currently alives.",
+ aData);
+
+ aCallback.callback("",
+ "settings-service-locks/created",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._createdLocks,
+ "The number of service locks that were created.",
+ aData);
+
+ aCallback.callback("",
+ "settings-service-locks/deleted",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._unregisteredLocks,
+ "The number of service locks that were deleted.",
+ aData);
+ },
+
+ classID : SETTINGSSERVICE_CID,
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsISettingsService,
+ Ci.nsIObserver,
+ Ci.nsIMemoryReporter])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]);
diff --git a/dom/settings/SettingsService.manifest b/dom/settings/SettingsService.manifest
new file mode 100644
index 000000000..ae464f921
--- /dev/null
+++ b/dom/settings/SettingsService.manifest
@@ -0,0 +1,5 @@
+component {d7a395a0-e292-11e1-834e-1761d57f5f99} SettingsService.js
+contract @mozilla.org/settingsServiceLock;1 {d7a395a0-e292-11e1-834e-1761d57f5f99}
+
+component {f656f0c0-f776-11e1-a21f-0800200c9a66} SettingsService.js
+contract @mozilla.org/settingsService;1 {f656f0c0-f776-11e1-a21f-0800200c9a66}
diff --git a/dom/settings/moz.build b/dom/settings/moz.build
new file mode 100644
index 000000000..426edcbce
--- /dev/null
+++ b/dom/settings/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'SettingsManager.manifest'
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'SettingsManager.js'
+]
+
+if CONFIG['MOZ_B2G']:
+ EXTRA_COMPONENTS += [
+ 'SettingsService.js',
+ 'SettingsService.manifest',
+ ]
+
+EXTRA_JS_MODULES += [
+ 'SettingsDB.jsm',
+ 'SettingsRequestManager.jsm'
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
diff --git a/dom/settings/tests/chrome.ini b/dom/settings/tests/chrome.ini
new file mode 100644
index 000000000..92b1554a0
--- /dev/null
+++ b/dom/settings/tests/chrome.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # Bug 1287455: takes too long to complete on Android
+support-files =
+ file_loadserver.js
+ file_bug1110872.js
+ file_bug1110872.html
+ test_settings_service.js
+ test_settings_service_callback.js
+
+[test_settings_service.xul]
+run-if = buildapp == 'b2g' || buildapp == 'mulet'
+[test_settings_service_callback.xul]
+run-if = buildapp == 'b2g' || buildapp == 'mulet'
+[test_settings_basics.html]
+[test_settings_permissions.html]
+[test_settings_blobs.html]
+[test_settings_data_uris.html]
+[test_settings_events.html]
+[test_settings_navigator_object.html]
+[test_settings_onsettingchange.html]
+[test_settings_bug1110872.html]
+skip-if = !e10s
+[test_settings_observer_killer.html]
+skip-if = !debug
diff --git a/dom/settings/tests/file_bug1110872.html b/dom/settings/tests/file_bug1110872.html
new file mode 100644
index 000000000..3dcc45b82
--- /dev/null
+++ b/dom/settings/tests/file_bug1110872.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for Bug {1110872} Settings API Reloads</title>
+ </head>
+ <body>
+ <script type="application/javascript;version=1.7">
+
+ var createLock = function (msg) {
+ var lock = navigator.mozSettings.createLock();
+ var req = lock.get("wallpaper.image");
+ // We don't actually care about success or failure here, we just
+ // want to know the queue gets processed at all.
+ req.onsuccess = function () {
+ parent.postMessage({name:"done" + msg.data.step}, "*");
+ }
+ req.onerror = function () {
+ parent.postMessage({name:"done" + msg.data.step}, "*");
+ };
+ return req;
+ }
+ window.onload = function() {
+ window.addEventListener("message", function (msg) {
+ var i;
+ var reqs = [];
+ if (msg.data.step == 1) {
+ for (i = 0; i < 100; ++i) {
+ reqs.push(createLock(msg));
+ }
+ } else {
+ reqs.push(createLock(msg));
+ }
+ // If this is our first time through, reload
+ // before the SettingsManager has a chance to get a response
+ // to our query.
+ if (msg.data.step == 1) {
+ location.reload();
+ }
+ });
+ }
+ </script>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={1110872}">Mozilla Bug {1110872} Inner Window for Reload Test</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ </body>
+</html>
diff --git a/dom/settings/tests/file_bug1110872.js b/dom/settings/tests/file_bug1110872.js
new file mode 100644
index 000000000..d31b6c2f3
--- /dev/null
+++ b/dom/settings/tests/file_bug1110872.js
@@ -0,0 +1,47 @@
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+var iframe;
+var loadedEvents = 0;
+
+function loadServer() {
+ var url = SimpleTest.getTestFileURL("file_loadserver.js");
+ var script = SpecialPowers.loadChromeScript(url);
+}
+
+function runTest() {
+ iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', mozbrowserLoaded);
+ iframe.src = 'file_bug1110872.html';
+}
+
+function iframeBodyRecv(msg) {
+ switch (loadedEvents) {
+ case 1:
+ // If we get a message back before we've seen 2 loads, that means
+ // something went wrong with the test. Fail immediately.
+ ok(true, 'got response from first test!');
+ break;
+ case 2:
+ // If we get a message back after 2 loads (initial load, reload),
+ // it means the callback for the last lock fired, which means the
+ // SettingsRequestManager queue has to have been cleared
+ // correctly.
+ ok(true, 'further queries returned ok after SettingsManager death');
+ SimpleTest.finish();
+ break;
+ }
+}
+
+function mozbrowserLoaded() {
+ loadedEvents++;
+ iframe.contentWindow.postMessage({name: "start", step: loadedEvents}, '*');
+ window.addEventListener('message', iframeBodyRecv);
+}
+
+window.addEventListener("load", function() {
+ loadServer();
+ runTest();
+});
diff --git a/dom/settings/tests/file_loadserver.js b/dom/settings/tests/file_loadserver.js
new file mode 100644
index 000000000..a919d9690
--- /dev/null
+++ b/dom/settings/tests/file_loadserver.js
@@ -0,0 +1,17 @@
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+// Stolen from SpecialPowers, since at this point we don't know we're in a test.
+var isMainProcess = function() {
+ try {
+ return Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULRuntime).
+ processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+ } catch (e) { }
+ return true;
+};
+
+if (isMainProcess()) {
+ Components.utils.import("resource://gre/modules/SettingsRequestManager.jsm");
+}
diff --git a/dom/settings/tests/test_settings_basics.html b/dom/settings/tests/test_settings_basics.html
new file mode 100644
index 000000000..a14650390
--- /dev/null
+++ b/dom/settings/tests/test_settings_basics.html
@@ -0,0 +1,816 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={678695}
+-->
+<head>
+ <title>Test for Bug {678695} Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={678695}">Mozilla Bug {678695}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+function onUnwantedSuccess() {
+ ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+ ok(false, "in on Failure!");
+}
+
+const wifi = {"net3g.apn": "internet.mnc012.mcc345.gprs"};
+const wifi2 = {"net3g.apn": "internet.mnc012.mcc345.test"};
+var wifi3 = {"net3g.apn2": "internet.mnc012.mcc345.test3"};
+var wifiEnabled = {"wifi.enabled": true};
+var wifiDisabled = {"wifi.enabled": false};
+var screenBright = {"screen.brightness": 0.7};
+var screenBright2 = {"screen.brightness": 0.1};
+var wifiNetworks0 = { "wifi.networks[0]": { ssid: "myfreenetwork", mac: "01:23:45:67:89:ab", passwd: "secret"}};
+var wifiNetworks1 = { "wifi.networks[1]": { ssid: "myfreenetwork2", mac: "01:23:45:67:89:ab", passwd: "secret2"}};
+
+var combination = {
+ "wifi.enabled": false,
+ "screen.brightness": 0.7,
+ "wifi.networks[0]": { ssid: "myfreenetwork", mac: "01:23:45:67:89:ab", passwd: "secret" },
+ "test.test": true,
+ "net3g.apn2": "internet.mnc012.mcc345.gprs"
+}
+
+function equals(o1, o2) {
+ var k1 = Object.keys(o1).sort();
+ var k2 = Object.keys(o2).sort();
+ if (k1.length != k2.length) return false;
+ return k1.zip(k2, function(keyPair) {
+ if(typeof o1[keyPair[0]] == typeof o2[keyPair[1]] == "object"){
+ return equals(o1[keyPair[0]], o2[keyPair[1]])
+ } else {
+ return o1[keyPair[0]] == o2[keyPair[1]];
+ }
+ }).all();
+};
+
+function observer1(setting) {
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+};
+
+function observer2(setting) {
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+};
+
+function observerWithNext(setting) {
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+ next();
+};
+
+function onsettingschangeWithNext(event) {
+ is(event.settingName, "screen.brightness", "Same settingName");
+ is(event.settingValue, 0.7, "Same settingvalue");
+ next();
+};
+
+function check(o1, o2) {
+ is(JSON.stringify(o1), JSON.stringify(o2), "same");
+}
+
+var req, req2, req3, req4, req5, req6;
+var index = 0;
+
+var steps = [
+ function () {
+ ok(true, "Deleting database");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ },
+ function () {
+ ok(true, "Setting wifi");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifi);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ }
+ req.onerror = onFailure;
+
+ var lock2 = navigator.mozSettings.createLock();
+ 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();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Change wifi1");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifi2);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ };
+ req.onerror = onFailure;
+ ok(true, "Get changed net3g.apn");
+ req2 = lock.get("net3g.apn");
+ req2.onsuccess = function () {
+ is(Object.keys(req2.result).length, 1, "length 1");
+ check(wifi2, req2.result);
+ ok(true, "Get net3g.apn Done");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Set Combination");
+ var lock = navigator.mozSettings.createLock();
+ req3 = lock.set(combination);
+ req3.onsuccess = function () {
+ ok(true, "set done");
+ req4 = lock.get("net3g.apn2");
+ req4.onsuccess = function() {
+ ok(true, "Done");
+ check(combination["net3g.apn2"], req4.result["net3g.apn2"]);
+ next();
+ }
+ }
+ req3.onerror = onFailure;
+ },
+ function() {
+ var lock = navigator.mozSettings.createLock();
+ req4 = lock.get("net3g.apn2");
+ req4.onsuccess = function() {
+ ok(true, "Done");
+ check(combination["net3g.apn2"], req4.result["net3g.apn2"]);
+ next();
+ }
+ req4.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Get unknown key");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("abc.def");
+ req.onsuccess = function() {
+ is(req.result["abc.def"], undefined, "no result");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "adding onsettingchange");
+ navigator.mozSettings.onsettingchange = onsettingschangeWithNext;
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.get("screen.brightness");
+ req2.onsuccess = function() {
+ ok(true, "end adding onsettingchange");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Test onsettingchange");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done, observer has to call next");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "delete onsettingschange");
+ var lock = navigator.mozSettings.createLock();
+ navigator.mozSettings.onsettingchange = null;
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Waiting for all set callbacks");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function() {
+ ok(true, "Done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "adding Observers 1");
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer2);
+ navigator.mozSettings.addObserver("screen.brightness", observerWithNext);
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.get("screen.brightness");
+ req2.onsuccess = function() {
+ ok(true, "set observeSetting done!");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test observers");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ navigator.mozSettings.removeObserver("screen.brightness", observer2);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ navigator.mozSettings.removeObserver("screen.brightness", observerWithNext);
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ navigator.mozSettings.removeObserver("screen.brightness", observer2);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ ok(true, "get done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Nested test");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ req3 = lock.set({"screen.brightness": req.result["screen.brightness"] + 1})
+ req3.onsuccess = function () {
+ req4 = lock.get("screen.brightness");
+ req4.onsuccess = function() {
+ is(req4.result["screen.brightness"], 1.7, "same Value");
+ }
+ req4.onerror = onFailure;
+ }
+ req3.onerror = onFailure;
+ };
+ req.onerror = onFailure;
+
+ req2 = lock.get("screen.brightness");
+ req2.onsuccess = function () {
+ is(req2.result["screen.brightness"], 0.7, "same Value");
+ }
+ req2.onerror = onFailure;
+
+ var lock2 = navigator.mozSettings.createLock();
+ req5 = lock2.get("screen.brightness");
+ req5.onsuccess = function () {
+ is(req5.result["screen.brightness"], 1.7, "same Value");
+ next();
+ }
+ req5.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Deleting database");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ },
+ function () {
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.set(wifi);
+ req2.onsuccess = function () {
+ ok(true, "set done");
+ }
+ req2.onerror = onFailure;
+
+ ok(true, "Get all settings");
+ var lock2 = navigator.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));
+ ok(true, "Get all settings Done");
+ };
+ req3.onerror = onFailure;
+
+ req4 = lock2.get("net3g.apn");
+ req4.onsuccess = function () {
+ is(Object.keys(req4.result).length, 1, "length 1");
+ check(wifi, req4.result);
+ ok(true, "Get net3g.apn Done");
+ next();
+ };
+ req4.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Change wifi1");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifi2);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ };
+ req.onerror = onFailure;
+
+ ok(true, "Get changed net3g.apn");
+ req2 = lock.get("net3g.apn");
+ req2.onsuccess = function () {
+ is(Object.keys(req2.result).length, 1, "length 1");
+ check(wifi2, req2.result);
+ ok(true, "Get net3g.apn Done");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test locking");
+ var lock = navigator.mozSettings.createLock();
+ var lock2 = navigator.mozSettings.createLock();
+ req = lock.set(wifiEnabled);
+ req.onsuccess = function () {
+ ok(true, "Test Locking Done");
+ };
+ req.onerror = onFailure;
+
+ req2 = lock2.set(wifiDisabled);
+ req2.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test locking result");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("wifi.enabled");
+ req.onsuccess = function() {
+ check(req.result, wifiDisabled);
+ ok(true, "Test1 locking result done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test locking heavy");
+ for (var i=0; i<30; i++) {
+ // only new locks!
+ var lock = navigator.mozSettings.createLock();
+ var obj = {};
+ obj["wifi.enabled" + i] = true;
+ req = lock.set( obj );
+ req.onsuccess = function () {
+ ok(true, "Set1 Done");
+ };
+ req.onerror = onFailure;
+ };
+ {
+ var lock2 = navigator.mozSettings.createLock();
+ req2 = lock2.get("*");
+ req2.onsuccess = function () {
+ is(Object.keys(req2.result).length, 32, "length 12");
+ ok(true, JSON.stringify(req2.result));
+ ok(true, "Get all settings Done");
+ };
+ req2.onerror = onFailure;
+ }
+ var lock2 = navigator.mozSettings.createLock();
+ var obj = {};
+ obj["wifi.enabled" + 30] = true;
+ req3 = lock2.set( obj );
+ req3.onsuccess = function () {
+ ok(true, "Set12 Done");
+ };
+ req3.onerror = onFailure;
+
+ var lock3 = navigator.mozSettings.createLock();
+ // with one lock
+ for (var i = 0; i < 30; i++) {
+ req4 = lock3.get("wifi.enabled" + i);
+ var testObj = {};
+ testObj["wifi.enabled" + i] = true;
+ req4.onsuccess = function () {
+ check(this.request.result, this.testObj);
+ ok(true, "Get1 Done");
+ }.bind({testObj: testObj, request: req4});
+ req4.onerror = onFailure;
+ }
+
+ ok(true, "start next2!");
+ var lock4 = navigator.mozSettings.createLock();
+ for (var i=0; i<30; i++) {
+ var obj = {};
+ obj["wifi.enabled" + i] = false;
+ req4 = lock4.set( obj );
+ req4.onsuccess = function () {
+ ok(true, "Set2 Done");
+ };
+ req4.onerror = onFailure;
+ }
+ var lock5 = navigator.mozSettings.createLock();
+ for (var i=0; i<30; i++) {
+ req5 = lock5.get("wifi.enabled" + i);
+ var testObj = {};
+ testObj["wifi.enabled" + i] = false;
+ req5.onsuccess = function () {
+ check(this.request.result, this.testObj);
+ ok(true, "Get2 Done");
+ }.bind({testObj: testObj, request: req5});
+ req5.onerror = onFailure;
+ }
+
+ var lock6 = navigator.mozSettings.createLock();
+ req6 = lock6.clear();
+ req6.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req6.onerror = onFailure;
+ },
+ function () {
+ ok(true, "reverse Test locking");
+ var lock2 = navigator.mozSettings.createLock();
+ var lock = navigator.mozSettings.createLock();
+
+ req = lock.set(wifiEnabled);
+ req.onsuccess = function () {
+ ok(true, "Test Locking Done");
+ next();
+ };
+ req.onerror = onFailure;
+
+ req2 = lock2.set(wifiDisabled);
+ req2.onsuccess = function () {
+ ok(true, "Set Done");
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test locking result");
+ var lock = navigator.mozSettings.createLock();
+
+ req = lock.get("wifi.enabled");
+ req.onsuccess = function() {
+ check(req.result, wifiEnabled);
+ ok(true, "Test2 locking result done");
+ }
+ req.onerror = onFailure;
+
+ var lock2 = navigator.mozSettings.createLock();
+ req2 = lock2.clear();
+ req2.onsuccess = function () {
+ ok(true, "Deleted the database");
+ };
+ req2.onerror = onFailure;
+
+ var lock3 = navigator.mozSettings.createLock();
+ req3 = lock3.set(wifi);
+ req3.onsuccess = function () {
+ ok(true, "set done");
+ next();
+ }
+ req3.onerror = onFailure;
+
+ },
+ function () {
+ ok(true, "Get all settings");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("*");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 1, "length 1");
+ check(wifi, req.result);
+ ok(true, "Get all settings Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get net3g.apn");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("net3g.apn");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 1, "length 1");
+ check(wifi, req.result);
+ ok(true, "Get net3g.apn Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Change wifi2");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifi2);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get net3g.apn");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("net3g.apn");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 1, "length 1");
+ check(wifi2, req.result);
+ ok(true, "Get net3g.apn Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Add wifi.enabled");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifiEnabled);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get Wifi Enabled");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("wifi.enabled");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 1, "length 1");
+ check(wifiEnabled, req.result);
+ ok(true, "Get wifi.enabledDone");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get all");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("*");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 2, "length 2");
+ check(wifiEnabled["wifi.enabled"], req.result["wifi.enabled"]);
+ check(wifi2["net3g.apn"], req.result["net3g.apn"]);
+ ok(true, "Get all Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Add wifiNetworks");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifiNetworks0);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ };
+ req.onerror = onFailure;
+
+ req2 = lock.set(wifiNetworks1);
+ req2.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get Wifi Networks");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("wifi.networks[0]");
+ req.onsuccess = function () {
+ is(Object.keys(req.result).length, 1, "length 1");
+ check(wifiNetworks0, req.result);
+ ok(true, "Get wifi.networks[0]");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Clear DB, multiple locks");
+ var lock4 = navigator.mozSettings.createLock();
+ var lock3 = navigator.mozSettings.createLock();
+ var lock2 = navigator.mozSettings.createLock();
+ var lock = navigator.mozSettings.createLock();
+ var lock6 = navigator.mozSettings.createLock();
+ var lock7 = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Add wifiNetworks");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifiNetworks0);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test set after lock closed");
+ var lockx = navigator.mozSettings.createLock();
+ var cb = function() {
+ var reqx = null;
+ try {
+ reqx = lockx.set(wifiNetworks0);
+ ok(false, "should have thrown");
+ } catch (ex) {
+ ok(reqx == null, "request is still null");
+ ok(true, "Caught Exception");
+ next();
+ }
+ }
+ SimpleTest.executeSoon(cb);
+ },
+ function() {
+ ok(true, "Clear DB");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Set with multiple arguments");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(combination);
+ req.onsuccess = function () {
+ ok(true, "Set Done");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "request argument from multiple set");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ check(req.result["screen.brightness"], 0.7, "get done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Test closed attribute on a valid lock");
+ var lock = navigator.mozSettings.createLock();
+ is(lock.closed, false, "closed attribute is false on creation");
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ is(lock.closed, false, "closed attribute is false on success callback");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Test closed attribute on invalid lock");
+ var lockx = navigator.mozSettings.createLock();
+ var cb = function() {
+ var reqx = null;
+ try {
+ reqx = lockx.set(wifiNetworks0);
+ ok(false, "should have thrown");
+ } catch (ex) {
+ is(lockx.closed, true, "closed attribute is true");
+ ok(true, "Caught Exception");
+ next();
+ }
+ }
+ SimpleTest.executeSoon(cb);
+ },
+ function() {
+ ok(true, "Clear DB");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Set object value");
+ var lock = navigator.mozSettings.createLock();
+ 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");
+ next();
+ };
+ };
+ },
+ function() {
+ ok(true, "Clear DB");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Call success callback when transaction commits");
+ var lock = navigator.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, "Clear DB");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "all done!\n");
+ SimpleTest.finish();
+ }
+];
+
+function next() {
+ ok(true, "Begin!");
+ if (index >= steps.length) {
+ ok(false, "Shouldn't get here!");
+ return;
+ }
+ try {
+ steps[index]();
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+ index += 1;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_blobs.html b/dom/settings/tests/test_settings_blobs.html
new file mode 100644
index 000000000..6d24111cf
--- /dev/null
+++ b/dom/settings/tests/test_settings_blobs.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=821630
+-->
+<head>
+ <title>Test for Bug 821630 Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=821630">Mozilla Bug 821630</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+"use strict";
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+function onUnwantedSuccess() {
+ ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+ return function(s) {
+ if (s) {
+ ok(false, "in on Failure! - " + s);
+ } else {
+ ok(false, "in on Failure!");
+ }
+ }
+}
+
+let req;
+
+let storedBlob = new Blob(['12345'], {"type": "text/plain"});
+
+function checkBlob(blob) {
+ try {
+ let url = URL.createObjectURL(blob);
+ ok(true, "Valid blob");
+ } catch (e) {
+ ok(false, "Valid blob");
+ }
+}
+
+let steps = [
+ function() {
+ let lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = next;
+ req.onerror = onFailure("Deleting database");
+ },
+ function() {
+ function obs(e) {
+ checkBlob(e.settingValue);
+ navigator.mozSettings.removeObserver("test1", obs);
+ next();
+ }
+ navigator.mozSettings.addObserver("test1", obs);
+ next();
+ },
+ function() {
+ // next is called by the observer above
+ let req = navigator.mozSettings.createLock().set({"test1": storedBlob});
+ req.onerror = onFailure("Saving blob");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test1");
+ req.onsuccess = function(event) {
+ checkBlob(event.target.result["test1"]);
+ next();
+ };
+ req.onerror = onFailure("Getting blob");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().set({"test2": [1, 2, storedBlob, 4]});
+ req.onsuccess = next;
+ req.onerror = onFailure("Saving array");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test2");
+ req.onsuccess = function(event) {
+ let val = event.target.result["test2"];
+ ok(Array.isArray(val), "Result is an array");
+ ok(val[0] == 1 && val[1] == 2 && val[3] == 4, "Primitives are preserved");
+ checkBlob(val[2]);
+ next();
+ };
+ req.onerror = onFailure("Getting array");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().set({"test3": {foo: "bar", baz: {number: 1, arr: [storedBlob]}}});
+ req.onsuccess = next();
+ req.onerror = onFailure("Saving object");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test3");
+ req.onsuccess = function(event) {
+ let val = event.target.result["test3"];
+ ok(typeof(val) == "object", "Result is an object");
+ ok("foo" in val && typeof(val.foo) == "string", "String property preserved");
+ ok("baz" in val && typeof(val.baz) == "object", "Object property preserved");
+ let baz = val.baz;
+ ok("number" in baz && baz.number == 1, "Primite inside object preserved");
+ ok("arr" in baz && Array.isArray(baz.arr), "Array inside object is preserved");
+ checkBlob(baz.arr[0]);
+ next();
+ };
+ req.onerror = onFailure("Getting object");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().clear();
+ req.onsuccess = function() {
+ next();
+ };
+ req.onerror = onFailure("Deleting database");
+ },
+ function () {
+ ok(true, "all done!\n");
+ SimpleTest.finish();
+ }
+];
+
+function next() {
+ try {
+ let step = steps.shift();
+ if (step) {
+ step();
+ }
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_bug1110872.html b/dom/settings/tests/test_settings_bug1110872.html
new file mode 100644
index 000000000..296c71a8c
--- /dev/null
+++ b/dom/settings/tests/test_settings_bug1110872.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for Bug {1110872} Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <script type="application/javascript;version=1.7" src="file_bug1110872.js">
+ </script>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={1110872}">Mozilla Bug {1110872}</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ </body>
+</html>
diff --git a/dom/settings/tests/test_settings_data_uris.html b/dom/settings/tests/test_settings_data_uris.html
new file mode 100644
index 000000000..7d9165fb3
--- /dev/null
+++ b/dom/settings/tests/test_settings_data_uris.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=806374
+-->
+<head>
+ <title>Test for Bug 806374 Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=821630">Mozilla Bug 821630</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+"use strict";
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+function onUnwantedSuccess() {
+ ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+ return function(s) {
+ if (s) {
+ ok(false, "in on Failure! - " + s);
+ } else {
+ ok(false, "in on Failure!");
+ }
+ }
+}
+
+let req;
+
+// A simple data URI that will be converted to a blob.
+let dataURI = "data:text/html;charset=utf-8,%3C!DOCTYPE html>%3Cbody style='background:black;";
+
+function checkBlob(blob) {
+ try {
+ let url = URL.createObjectURL(blob);
+ ok(true, "Valid blob");
+ } catch (e) {
+ ok(false, "Valid blob");
+ }
+}
+
+let steps = [
+ function() {
+ let lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = next;
+ req.onerror = onFailure("Deleting database");
+ },
+ function() {
+ function obs(e) {
+ checkBlob(e.settingValue);
+ navigator.mozSettings.removeObserver("test1", obs);
+ next();
+ }
+ navigator.mozSettings.addObserver("test1", obs);
+ next();
+ },
+ function() {
+ // next is called by the observer above
+ let req = navigator.mozSettings.createLock().set({"test1": dataURI});
+ req.onerror = onFailure("Saving blob");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test1");
+ req.onsuccess = function(event) {
+ checkBlob(event.target.result["test1"]);
+ next();
+ };
+ req.onerror = onFailure("Getting blob");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().set({"test2": [1, 2, dataURI, 4]});
+ req.onsuccess = next;
+ req.onerror = onFailure("Saving array");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test2");
+ req.onsuccess = function(event) {
+ let val = event.target.result["test2"];
+ ok(Array.isArray(val), "Result is an array");
+ ok(val[0] == 1 && val[1] == 2 && val[3] == 4, "Primitives are preserved");
+ checkBlob(val[2]);
+ next();
+ };
+ req.onerror = onFailure("Getting array");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().set({"test3": {foo: "bar", baz: {number: 1, arr: [dataURI]}}});
+ req.onsuccess = next();
+ req.onerror = onFailure("Saving object");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().get("test3");
+ req.onsuccess = function(event) {
+ let val = event.target.result["test3"];
+ ok(typeof(val) == "object", "Result is an object");
+ ok("foo" in val && typeof(val.foo) == "string", "String property preserved");
+ ok("baz" in val && typeof(val.baz) == "object", "Object property preserved");
+ let baz = val.baz;
+ ok("number" in baz && baz.number == 1, "Primite inside object preserved");
+ ok("arr" in baz && Array.isArray(baz.arr), "Array inside object is preserved");
+ checkBlob(baz.arr[0]);
+ next();
+ };
+ req.onerror = onFailure("Getting object");
+ },
+ function() {
+ let req = navigator.mozSettings.createLock().clear();
+ req.onsuccess = function() {
+ next();
+ };
+ req.onerror = onFailure("Deleting database");
+ },
+ function () {
+ ok(true, "all done!\n");
+ SimpleTest.finish();
+ }
+];
+
+function next() {
+ try {
+ let step = steps.shift();
+ if (step) {
+ step();
+ }
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_events.html b/dom/settings/tests/test_settings_events.html
new file mode 100644
index 000000000..a8bf851be
--- /dev/null
+++ b/dom/settings/tests/test_settings_events.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678695
+-->
+<head>
+ <title>Test for Bug 678695</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=678695">Mozilla Bug 678695</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 678695 **/
+
+var e = new MozSettingsEvent("settingchanged", {settingName: "a", settingValue: 1});
+ok(e, "Should have settings event!");
+is(e.settingName, "a", "Name should be a.");
+is(e.settingValue, 1, "Value should be 1.");
+
+e = new MozSettingsEvent("settingchanged", {settingName: "test", settingValue: {test: "test"}});
+is(e.settingName, "test", "Name should be 'test'.");
+is(e.settingValue.test, "test", "Name should be 'test'.");
+
+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.'");
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_navigator_object.html b/dom/settings/tests/test_settings_navigator_object.html
new file mode 100644
index 000000000..2f666aee0
--- /dev/null
+++ b/dom/settings/tests/test_settings_navigator_object.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=898512
+-->
+<head>
+ <title>Test for Bug 898512 Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=898512">Mozilla Bug 898512</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+SimpleTest.waitForExplicitFinish();
+
+function testPref() {
+ SpecialPowers.pushPrefEnv({
+ set: [["dom.mozSettings.enabled", false]]
+ }, function() {
+ is(navigator.mozSettings, undefined, "navigator.mozSettings is undefined");
+ SimpleTest.finish();
+ });
+}
+
+testPref();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_observer_killer.html b/dom/settings/tests/test_settings_observer_killer.html
new file mode 100644
index 000000000..8e7ed973c
--- /dev/null
+++ b/dom/settings/tests/test_settings_observer_killer.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1193469
+-->
+<head>
+ <title>Test for Bug 1193469 Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1193469">Mozilla Bug 1193469</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.mozSettings.enabled", true]]
+},
+ function () {
+ ok(true, "abusing observers");
+
+ var obs;
+ for (obs = 0; obs < 9; obs++) {
+ navigator.mozSettings.addObserver("fake.setting.key", function(v) {
+ // nothing to do for real ...
+ ok(false, "should not be called");
+ });
+ ok(true, "first: added observer #" + obs);
+ }
+ ok(true, "adding first observers, should not have thrown");
+
+ try {
+ ok(true, "second: adding new observer");
+ navigator.mozSettings.addObserver("fake.setting.key", function(v) {
+ // nothing to do for real ...
+ ok(false, "should not be called");
+ });
+ ok(false, "adding too many observers should have thrown");
+ } catch (ex) {
+ ok(true, "got exception when trying to add too many observers");
+ }
+
+ SimpleTest.finish();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_onsettingchange.html b/dom/settings/tests/test_settings_onsettingchange.html
new file mode 100644
index 000000000..974da0c63
--- /dev/null
+++ b/dom/settings/tests/test_settings_onsettingchange.html
@@ -0,0 +1,306 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678695
+-->
+<head>
+ <title>Test for Bug 678695 Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=678695">Mozilla Bug 678695</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+var screenBright = {"screen.brightness": 0.7};
+
+function onFailure() {
+ ok(false, "in on Failure!");
+}
+
+function observer1(setting) {
+ dump("observer 1 called!\n");
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+};
+
+function observer2(setting) {
+ dump("observer 2 called!\n");
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+};
+
+var calls = 0;
+function observerOnlyCalledOnce(setting) {
+ is(++calls, 1, "Observer only called once!");
+};
+
+
+function observerWithNext(setting) {
+ dump("observer with next called!\n");
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, 0.7, "Same settingvalue");
+ next();
+};
+
+function onsettingschangeWithNext(event) {
+ dump("onsettingschangewithnext called!\n");
+ is(event.settingName, "screen.brightness", "Same settingName");
+ is(event.settingValue, 0.7, "Same settingvalue");
+ next();
+};
+
+var cset = {'a':'b','c':[{'d':'e'}]};
+
+function onComplexSettingschangeWithNext(event) {
+ is(event.settingName, "test.key", "Same settingName");
+ is(event.settingValue['a'], "b", "Same settingvalue");
+ var c = event.settingValue['c'];
+ ok(Array.isArray(c), "c is array!");
+ is(c[0]['d'], 'e', "Right settingValue!");
+ next();
+};
+
+var req, req2;
+var index = 0;
+
+var steps = [
+ function () {
+ ok(true, "Deleting database");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.set(screenBright);
+ req2.onsuccess = function () {
+ ok(true, "set done");
+ navigator.mozSettings.onsettingchange = onsettingschangeWithNext;
+ next();
+ }
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "testing");
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.set(screenBright);
+ req2.onsuccess = function() {
+ ok(true, "end adding onsettingchange");
+ };
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test observers");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ ok(true, "get done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "adding Observers 1");
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer2);
+ navigator.mozSettings.addObserver("screen.brightness", observerOnlyCalledOnce);
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.get("screen.brightness");
+ req2.onsuccess = function() {
+ ok(true, "set observeSetting done!");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test observers");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set1 done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test observers");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ navigator.mozSettings.removeObserver("screen.brightness", observerOnlyCalledOnce);
+ req.onsuccess = function () {
+ ok(true, "set1 done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set2 done");
+ navigator.mozSettings.removeObserver("screen.brightness", observer2);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ }
+ req.onerror = onFailure;
+ },
+
+ function() {
+ ok(true, "delete onsettingschange");
+ var lock = navigator.mozSettings.createLock();
+ navigator.mozSettings.onsettingchange = null;
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set0 done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Waiting for all set callbacks");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function() {
+ ok(true, "Done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "adding Observers 1");
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer1);
+ navigator.mozSettings.addObserver("screen.brightness", observer2);
+ navigator.mozSettings.addObserver("screen.brightness", observerWithNext);
+ var lock = navigator.mozSettings.createLock();
+ req2 = lock.get("screen.brightness");
+ req2.onsuccess = function() {
+ ok(true, "set observeSetting done!");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test observers");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set1 done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set2 done");
+ navigator.mozSettings.removeObserver("screen.brightness", observer2);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "test Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set3 done");
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ navigator.mozSettings.removeObserver("screen.brightness", observerWithNext);
+ req = lock.set(screenBright);
+ req.onsuccess = function () {
+ ok(true, "set4 done");
+ navigator.mozSettings.removeObserver("screen.brightness", observer2);
+ navigator.mozSettings.removeObserver("screen.brightness", observer1);
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "removing Event Listener");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("screen.brightness");
+ req.onsuccess = function () {
+ ok(true, "get5 done");
+ next();
+ }
+ req.onerror = onFailure;
+ },
+ function() {
+ ok(true, "Clear DB");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Deleting database");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.clear();
+ req.onsuccess = function () {
+ ok(true, "Deleted the database");
+ next();
+ };
+ req.onerror = onFailure;
+ },
+ function () {
+ var lock = navigator.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();
+ }
+];
+
+function next() {
+ ok(true, "Begin!");
+ if (index >= steps.length) {
+ ok(false, "Shouldn't get here!");
+ return;
+ }
+ try {
+ steps[index]();
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+ index += 1;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_permissions.html b/dom/settings/tests/test_settings_permissions.html
new file mode 100644
index 000000000..4cc02385a
--- /dev/null
+++ b/dom/settings/tests/test_settings_permissions.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={678695}
+-->
+<head>
+ <title>Test for Bug {678695} Settings API</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={900551}">Mozilla Bug {900551}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+var url = SimpleTest.getTestFileURL("file_loadserver.js");
+var script = SpecialPowers.loadChromeScript(url);
+
+function onUnwantedSuccess() {
+ ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+// No more permissions, so failure cannot happen
+function onFailure() {
+ ok(true, "in on Failure!");
+ next();
+}
+
+const wifi = {"wifi.enabled": false}
+const wallpaper = {"wallpaper.image": "test-image"};
+
+var combination = {
+ "wifi.enabled": false,
+ "wallpaper.image": "test-image"
+}
+
+function equals(o1, o2) {
+ var k1 = Object.keys(o1).sort();
+ var k2 = Object.keys(o2).sort();
+ if (k1.length != k2.length) return false;
+ return k1.zip(k2, function(keyPair) {
+ if(typeof o1[keyPair[0]] == typeof o2[keyPair[1]] == "object"){
+ return equals(o1[keyPair[0]], o2[keyPair[1]])
+ } else {
+ return o1[keyPair[0]] == o2[keyPair[1]];
+ }
+ }).all();
+};
+
+function observer1(setting) {
+ is(setting.settingName, "screen.brightness", "Same settingName");
+ is(setting.settingValue, "0.7", "Same settingvalue");
+};
+
+function onsettingschangeWithNext(event) {
+ is(event.settingName, "screen.brightness", "Same settingName");
+ is(event.settingValue, "0.7", "Same settingvalue");
+ next();
+};
+
+function check(o1, o2) {
+ is(JSON.stringify(o1), JSON.stringify(o2), "same");
+}
+
+var req, req2, req3, req4, req5, req6;
+var index = 0;
+
+var steps = [
+ // Can't delete database here since that requires permissions we don't want
+ // to give the page.
+ function () {
+ ok(true, "Setting wallpaper");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wallpaper);
+ req.onsuccess = function () {
+ ok(true, "set done");
+ }
+ req.onerror = onFailure;
+
+ var lock2 = navigator.mozSettings.createLock();
+ req2 = lock2.get("wallpaper.image");
+ req2.onsuccess = function () {
+ is(Object.keys(req2.result).length, 1, "length 1");
+ check(wallpaper, req2.result);
+ ok(true, "Get wallpaper Done");
+ next();
+ };
+ req2.onerror = onFailure;
+ },
+ function () {
+ ok(true, "Get Wifi");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.get("wifi.enabled");
+ req.onerror = function () {
+ ok(true, "get failed (expected)");
+ next();
+ }
+ req.onsuccess = onFailure;
+ },
+ function () {
+ ok(true, "Set Wifi");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(wifi);
+ req.onerror = function () {
+ ok(true, "set failed (expected)");
+ next();
+ }
+ req.onsuccess = onFailure;
+ },
+ function () {
+ ok(true, "Set combination (1 valid 1 not valid)");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(combination);
+ req.onerror = function () {
+ ok(true, "set failed (expected)");
+ next();
+ }
+ req.onsuccess = onFailure;
+ },
+ function () {
+ ok(true, "All requests on a failed lock should fail");
+ var lock = navigator.mozSettings.createLock();
+ lock.onsettingstransactionfailure = function (evt) {
+ ok(evt.error == "Lock failed a permissions check, all requests now failing.", "transaction failure on permissions error message correct.");
+ ok(true, "transaction failed (expected) ");
+ next();
+ };
+ lock.onsettingstransactionsuccess = onFailure;
+
+ req = lock.set(wifi);
+ req.onerror = function () {
+ ok(true, "set failed (expected)");
+ }
+ req.onsuccess = onFailure;
+ req2 = lock.get("wallpaper.image");
+ req2.onerror = function () {
+ ok(true, "get failed (expected)");
+ }
+ req2.onsuccess = onFailure;
+ },
+ function () {
+ ok(true, "Set combination (1 valid 1 not valid)");
+ var lock = navigator.mozSettings.createLock();
+ req = lock.set(combination);
+ req.onerror = function () {
+ ok(true, "set failed (expected)");
+ next();
+ }
+ req.onsuccess = onFailure;
+ },
+ function () {
+ ok(true, "all done!\n");
+ SimpleTest.finish();
+ }
+];
+
+function next() {
+ ok(true, "Begin!");
+ if (index >= steps.length) {
+ ok(false, "Shouldn't get here!");
+ return;
+ }
+ try {
+ steps[index]();
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+ index += 1;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/settings/tests/test_settings_service.js b/dom/settings/tests/test_settings_service.js
new file mode 100644
index 000000000..132877a5d
--- /dev/null
+++ b/dom/settings/tests/test_settings_service.js
@@ -0,0 +1,138 @@
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var 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");
+
+SimpleTest.waitForExplicitFinish();
+
+XPCOMUtils.defineLazyServiceGetter(this, "SettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var tests = [
+ /* Callback tests */
+ function() {
+ let callbackCount = 10;
+
+ let callback = {
+ handle: function(name, result) {
+ switch (callbackCount) {
+ case 10:
+ case 9:
+ is(result, true, "result is true");
+ break;
+ case 8:
+ case 7:
+ is(result, false, "result is false");
+ break;
+ case 6:
+ case 5:
+ is(result, 9, "result is 9");
+ break;
+ case 4:
+ case 3:
+ is(result, 9.4, "result is 9.4");
+ break;
+ case 2:
+ is(result, false, "result is false");
+ break;
+ case 1:
+ is(result, null, "result is null");
+ break;
+ default:
+ ok(false, "Unexpected call: " + callbackCount);
+ }
+
+ --callbackCount;
+ if (callbackCount === 0) {
+ next();
+ }
+ },
+
+ handleError: function(name) {
+ ok(false, "error: " + name);
+ }
+ };
+
+ let lock = SettingsService.createLock();
+ let lock1 = SettingsService.createLock();
+
+ lock.set("asdf", true, callback, null);
+ lock1.get("asdf", callback);
+ lock.get("asdf", callback);
+ lock.set("asdf", false, callback, null);
+ lock.get("asdf", callback);
+ lock.set("int", 9, callback, null);
+ lock.get("int", callback);
+ lock.set("doub", 9.4, callback, null);
+ lock.get("doub", callback);
+ lock1.get("asdfxxx", callback);
+ },
+
+ /* Observer tests */
+ function() {
+ const MOZSETTINGS_CHANGED = "mozsettings-changed";
+ const TEST_OBSERVER_KEY = "test.observer.key";
+ const TEST_OBSERVER_VALUE = true;
+ const TEST_OBSERVER_MESSAGE = "test.observer.message";
+
+ var obs = {
+ observe: function (subject, topic, data) {
+
+ if (topic !== MOZSETTINGS_CHANGED) {
+ ok(false, "Event is not mozsettings-changed.");
+ return;
+ }
+ // Data is now stored in subject
+ if ("wrappedJSObject" in subject) {
+ ok(true, "JS object wrapped into subject");
+ subject = subject.wrappedJSObject;
+ }
+ if (subject["key"] != TEST_OBSERVER_KEY) {
+ return;
+ }
+
+ function checkProp(name, type, value) {
+ ok(name in subject, "subject." + name + " is present");
+ is(typeof subject[name], type, "subject." + name + " is " + type);
+ is(subject[name], value, "subject." + name + " is " + value);
+ }
+
+ checkProp("key", "string", TEST_OBSERVER_KEY);
+ checkProp("value", "boolean", TEST_OBSERVER_VALUE);
+ checkProp("isInternalChange", "boolean", true);
+
+ Services.obs.removeObserver(this, MOZSETTINGS_CHANGED);
+ next();
+ }
+ };
+
+ Services.obs.addObserver(obs, MOZSETTINGS_CHANGED, false);
+
+ let lock = SettingsService.createLock();
+ lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null);
+ }
+];
+
+function next() {
+ let step = tests.shift();
+ if (step) {
+ try {
+ step();
+ } catch(e) {
+ ok(false, "Test threw: " + e);
+ }
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+next();
diff --git a/dom/settings/tests/test_settings_service.xul b/dom/settings/tests/test_settings_service.xul
new file mode 100644
index 000000000..58a9efad9
--- /dev/null
+++ b/dom/settings/tests/test_settings_service.xul
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678695
+-->
+<window title="Mozilla Bug 678695"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=678695"
+ target="_blank">Mozilla Bug 678695</a>
+ </body>
+
+ <script type="application/javascript;version=1.7" src="test_settings_service.js" />
+</window>
diff --git a/dom/settings/tests/test_settings_service_callback.js b/dom/settings/tests/test_settings_service_callback.js
new file mode 100644
index 000000000..a780bb9c3
--- /dev/null
+++ b/dom/settings/tests/test_settings_service_callback.js
@@ -0,0 +1,47 @@
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+SimpleTest.waitForExplicitFinish();
+
+XPCOMUtils.defineLazyServiceGetter(this, "SettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var tests = [
+ function () {
+ let callback = {
+ handle: function() {
+ ok(true, "handle called!");
+ next();
+ },
+
+ handleAbort: function(name) {
+ ok(false, "error: " + name);
+ next();
+ }
+ }
+ let lock = SettingsService.createLock(callback);
+ lock.set("xasdf", true, null, null);
+ }
+];
+
+function next() {
+ let step = tests.shift();
+ if (step) {
+ try {
+ step();
+ } catch(e) {
+ ok(false, "Test threw: " + e);
+ }
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+next();
diff --git a/dom/settings/tests/test_settings_service_callback.xul b/dom/settings/tests/test_settings_service_callback.xul
new file mode 100644
index 000000000..3e4d27751
--- /dev/null
+++ b/dom/settings/tests/test_settings_service_callback.xul
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1012214
+-->
+<window title="Mozilla Bug 1012214"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1012214"
+ target="_blank">Mozilla Bug 1012214</a>
+ </body>
+
+ <script type="application/javascript;version=1.7" src="test_settings_service_callback.js" />
+</window>
diff --git a/dom/settings/tests/unit/test_settingsrequestmanager_messages.js b/dom/settings/tests/unit/test_settingsrequestmanager_messages.js
new file mode 100644
index 000000000..e5fb08475
--- /dev/null
+++ b/dom/settings/tests/unit/test_settingsrequestmanager_messages.js
@@ -0,0 +1,174 @@
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+var principal = Services.scriptSecurityManager.getSystemPrincipal();
+var lockID = "{435d2192-4f21-48d4-90b7-285f147a56be}";
+
+// Helper to start the Settings Request Manager
+function startSettingsRequestManager() {
+ Cu.import("resource://gre/modules/SettingsRequestManager.jsm");
+}
+
+function handlerHelper(reply, callback, runNext = true) {
+ let handler = {
+ receiveMessage: function(message) {
+ if (message.name === reply) {
+ cpmm.removeMessageListener(reply, handler);
+ callback(message);
+ if (runNext) {
+ run_next_test();
+ }
+ }
+ }
+ };
+ cpmm.addMessageListener(reply, handler);
+}
+
+// Helper function to add a listener, send message and treat the reply
+function addAndSend(msg, reply, callback, payload, runNext = true) {
+ handlerHelper(reply, callback, runNext);
+ cpmm.sendAsyncMessage(msg, payload, undefined, principal);
+}
+
+function errorHandler(reply, str) {
+ let errHandler = function(message) {
+ ok(true, str);
+ };
+
+ handlerHelper(reply, errHandler);
+}
+
+// We need to trigger a Settings:Run message to make the queue progress
+function send_settingsRun() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("Settings:Run", msg, undefined, principal);
+}
+
+function kill_child() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("child-process-shutdown", msg, undefined, principal);
+}
+
+function run_test() {
+ do_get_profile();
+ startSettingsRequestManager();
+ run_next_test();
+}
+
+add_test(function test_createLock() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("Settings:CreateLock", msg, undefined, principal);
+ cpmm.sendAsyncMessage(
+ "Settings:RegisterForMessages", undefined, undefined, principal);
+ ok(true);
+ run_next_test();
+});
+
+add_test(function test_get_empty() {
+ let requestID = 10;
+ let msgReply = "Settings:Get:OK";
+ let msgHandler = function(message) {
+ equal(requestID, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ ok(Object.keys(message.data.settings).length >= 0);
+ };
+
+ errorHandler("Settings:Get:KO", "Settings GET failed");
+
+ addAndSend("Settings:Get", msgReply, msgHandler, {
+ requestID: requestID,
+ lockID: lockID,
+ name: "language.current"
+ });
+
+ send_settingsRun();
+});
+
+add_test(function test_set_get_nonempty() {
+ let settings = { "language.current": "fr-FR:XPC" };
+ let requestIDSet = 20;
+ let msgReplySet = "Settings:Set:OK";
+ let msgHandlerSet = function(message) {
+ equal(requestIDSet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ };
+
+ errorHandler("Settings:Set:KO", "Settings SET failed");
+
+ addAndSend("Settings:Set", msgReplySet, msgHandlerSet, {
+ requestID: requestIDSet,
+ lockID: lockID,
+ settings: settings
+ }, false);
+
+ let requestIDGet = 25;
+ let msgReplyGet = "Settings:Get:OK";
+ let msgHandlerGet = function(message) {
+ equal(requestIDGet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ for(let p in settings) {
+ equal(settings[p], message.data.settings[p]);
+ }
+ };
+
+ addAndSend("Settings:Get", msgReplyGet, msgHandlerGet, {
+ requestID: requestIDGet,
+ lockID: lockID,
+ name: Object.keys(settings)[0]
+ });
+
+ // Set and Get have been push into the queue, let's run
+ send_settingsRun();
+});
+
+// This test exposes bug 1076597 behavior
+add_test(function test_wait_for_finalize() {
+ let settings = { "language.current": "en-US:XPC" };
+ let requestIDSet = 30;
+ let msgReplySet = "Settings:Set:OK";
+ let msgHandlerSet = function(message) {
+ equal(requestIDSet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ };
+
+ errorHandler("Settings:Set:KO", "Settings SET failed");
+
+ addAndSend("Settings:Set", msgReplySet, msgHandlerSet, {
+ requestID: requestIDSet,
+ lockID: lockID,
+ settings: settings
+ }, false);
+
+ let requestIDGet = 35;
+ let msgReplyGet = "Settings:Get:OK";
+ let msgHandlerGet = function(message) {
+ equal(requestIDGet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ for(let p in settings) {
+ equal(settings[p], message.data.settings[p]);
+ }
+ };
+
+ errorHandler("Settings:Get:KO", "Settings GET failed");
+
+ addAndSend("Settings:Get", msgReplyGet, msgHandlerGet, {
+ requestID: requestIDGet,
+ lockID: lockID,
+ name: Object.keys(settings)[0]
+ });
+
+ // We simulate a child death, which will force previous requests to be set
+ // into finalize state
+ kill_child();
+
+ // Then when we issue Settings:Run, those finalized should be triggered
+ send_settingsRun();
+});
diff --git a/dom/settings/tests/unit/xpcshell.ini b/dom/settings/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..9669a1ed0
--- /dev/null
+++ b/dom/settings/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+
+[test_settingsrequestmanager_messages.js]
+skip-if = (buildapp != 'b2g')