gecko/dom/settings/SettingsManager.js

362 lines
13 KiB
JavaScript

/* 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"
/* static functions */
let DEBUG = 0;
if (DEBUG)
debug = function (s) { dump("-*- SettingsManager: " + s + "\n"); }
else
debug = function (s) {}
function Queue() {
this._queue = [];
this._index = 0;
}
Queue.prototype = {
getLength: function() { return (this._queue.length - this._index); },
isEmpty: function() { return (this._queue.length == 0); },
enqueue: function(item) { this._queue.push(item); },
dequeue: function() {
if(this.isEmpty())
return undefined;
var item = this._queue[this._index];
if (++this._index * 2 >= this._queue.length){
this._queue = this._queue.slice(this._index);
this._index = 0;
}
return item;
}
}
const DB_NAME = "settings";
const DB_VERSION = 1;
const STORE_NAME = "settings";
function SettingsDB() {}
SettingsDB.prototype = {
db: null,
close: function close() {
if (this.db)
this.db.close();
},
/**
* Prepare the database. This may include opening the database and upgrading
* it to the latest schema version.
*
* @return (via callback) a database ready for use.
*/
ensureDB: function ensureDB(aSuccessCb, aFailureCb, aGlobal) {
if (this.db) {
debug("ensureDB: already have a database, returning early.");
return;
}
let self = this;
debug("try to open database:" + DB_NAME + " " + DB_VERSION + " " + this._indexedDB);
let req = aGlobal.mozIndexedDB.open(DB_NAME, DB_VERSION);
req.onsuccess = function (event) {
debug("Opened database:", DB_NAME, DB_VERSION);
self.db = event.target.result;
self.db.onversionchange = function(event) {
debug("WARNING: DB modified from a different window.");
}
aSuccessCb();
};
req.onupgradeneeded = function (aEvent) {
debug("Database needs upgrade:" + DB_NAME + aEvent.oldVersion + aEvent.newVersion);
debug("Correct new database version:" + aEvent.newVersion == DB_VERSION);
let db = aEvent.target.result;
switch (aEvent.oldVersion) {
case 0:
debug("New database");
self.createSchema(db);
break;
default:
debug("No idea what to do with old database version:" + aEvent.oldVersion);
aFailureCb(aEvent.target.errorMessage);
break;
}
};
req.onerror = function (aEvent) {
debug("Failed to open database:", DB_NAME);
aFailureCb(aEvent.target.errorMessage);
};
req.onblocked = function (aEvent) {
debug("Opening database request is blocked.");
};
},
createSchema: function createSchema(aDb) {
let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "settingName"});
objectStore.createIndex("settingValue", "settingValue", { unique: false });
debug("Created object stores and indexes");
}
}
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");
XPCOMUtils.defineLazyGetter(Services, "DOMRequest", function() {
return Cc["@mozilla.org/dom/dom-request-service;1"].getService(Ci.nsIDOMRequestService);
});
const nsIClassInfo = Ci.nsIClassInfo;
const SETTINGSLOCK_CONTRACTID = "@mozilla.org/settingsLock;1";
const SETTINGSLOCK_CID = Components.ID("{ef95ddd0-6308-11e1-b86c-0800200c9a66}");
const nsIDOMSettingsLock = Ci.nsIDOMSettingsLock;
function SettingsLock(aSettingsManager)
{
this._open = true;
this._requests = new Queue();
this._settingsManager = aSettingsManager;
this._transaction = null;
}
SettingsLock.prototype = {
process: function process() {
let lock = this;
lock._open = false;
let store = lock._transaction.objectStore(STORE_NAME);
while (!lock._requests.isEmpty()) {
let info = lock._requests.dequeue();
debug("info:" + info.intent);
let request = info.request;
switch (info.intent) {
case "clear":
let req = store.clear();
req.onsuccess = function() { this._open = true;
Services.DOMRequest.fireSuccess(request, 0);
this._open = false; }.bind(lock);
req.onerror = function() { Services.DOMRequest.fireError(request, 0) };
break;
case "set":
for (var key in info.settings) {
debug("key: " + key + ", val: " + JSON.stringify(info.settings[key]) + "type: " + typeof(info.settings[key]));
if(typeof(info.settings[key]) != 'object') {
req = store.put({settingName: key, settingValue: info.settings[key]});
} else {
//Workaround for cloning issues
let obj = JSON.parse(JSON.stringify(info.settings[key]));
req = store.put({settingName: key, settingValue: obj});
}
req.onsuccess = function() { this._open = true;
if (this._settingsManager._onsettingchange) {
let event = new this._settingsManager._window.MozSettingsEvent("settingchanged", {settingName: key, settingValue: info.settings[key]});
this._settingsManager._onsettingchange.handleEvent(event);
}
Services.obs.notifyObservers(this, "mozsettings-changed-"+key, JSON.stringify(info.settings[key]));
Services.DOMRequest.fireSuccess(request, 0);
this._open = false; }.bind(lock);
req.onerror = function() { Services.DOMRequest.fireError(request, 0) };
}
break;
case "get":
if (info.name == "*") {
req = store.getAll();
} else {
req = store.getAll(info.name);
}
req.onsuccess = function(event) {
debug("Request successful. Record count:" + event.target.result.length);
debug("result: " + JSON.stringify(event.target.result));
var result = {};
for (var i in event.target.result)
result[event.target.result[i].settingName] = event.target.result[i].settingValue;
this._open = true;
Services.DOMRequest.fireSuccess(request, result);
this._open = false;
}.bind(lock);
req.onerror = function() { Services.DOMRequest.fireError(request, 0)};
break;
}
}
if (!lock._requests.isEmpty())
throw Components.results.NS_ERROR_ABORT;
lock._open = true;
},
createTransactionAndProcess: function() {
if (this._settingsManager._settingsDB.db) {
var lock;
while (lock = this._settingsManager._locks.dequeue()) {
if (!lock._transaction) {
lock._transaction = lock._settingsManager._settingsDB.db.transaction(STORE_NAME, "readwrite");
}
lock.process();
}
if (!this._requests.isEmpty())
this.process();
}
},
get: function get(aName) {
if (!this._open)
throw Components.results.NS_ERROR_ABORT;
if (this._settingsManager.hasReadPrivileges || this._settingsManager.hasReadWritePrivileges) {
let req = Services.DOMRequest.createRequest(this._settingsManager._window);
this._requests.enqueue({ request: req, intent:"get", name: aName });
this.createTransactionAndProcess();
return req;
} else {
debug("get not allowed");
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
},
set: function set(aSettings) {
if (!this._open)
throw Components.results.NS_ERROR_ABORT;
if (this._settingsManager.hasReadWritePrivileges) {
let req = Services.DOMRequest.createRequest(this._settingsManager._window);
debug("send: " + JSON.stringify(aSettings));
this._requests.enqueue({request: req, intent: "set", settings: aSettings});
this.createTransactionAndProcess();
return req;
} else {
debug("set not allowed");
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
},
clear: function clear() {
if (!this._open)
throw Components.results.NS_ERROR_ABORT;
if (this._settingsManager.hasReadWritePrivileges) {
let req = Services.DOMRequest.createRequest(this._settingsManager._window);
this._requests.enqueue({ request: req, intent: "clear"});
this.createTransactionAndProcess();
return req;
} else {
debug("clear not allowed");
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
},
classID : SETTINGSLOCK_CID,
QueryInterface : XPCOMUtils.generateQI([nsIDOMSettingsLock]),
classInfo : XPCOMUtils.generateCI({classID: SETTINGSLOCK_CID,
contractID: SETTINGSLOCK_CONTRACTID,
classDescription: "SettingsLock",
interfaces: [nsIDOMSettingsLock],
flags: nsIClassInfo.DOM_OBJECT})
};
const SETTINGSMANAGER_CONTRACTID = "@mozilla.org/settingsManager;1";
const SETTINGSMANAGER_CID = Components.ID("{5609d0a0-52a9-11e1-b86c-0800200c9a66}");
const nsIDOMSettingsManager = Ci.nsIDOMSettingsManager;
let myGlobal = this;
function SettingsManager()
{
this._locks = new Queue();
var idbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager);
idbManager.initWindowless(myGlobal);
this._settingsDB = new SettingsDB();
}
SettingsManager.prototype = {
_onsettingchange: null,
nextTick: function nextTick(aCallback, thisObj) {
if (thisObj)
aCallback = aCallback.bind(thisObj);
Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
},
set onsettingchange(aCallback) {
if (this.hasReadPrivileges || this.hasReadWritePrivileges)
this._onsettingchange = aCallback;
else
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
get onsettingchange() {
return this._onsettingchange;
},
getLock: function() {
debug("get lock!");
var lock = new SettingsLock(this);
this._locks.enqueue(lock);
this._settingsDB.ensureDB(
function() { lock.createTransactionAndProcess(); },
function() { dump("ensureDB error cb!\n"); },
myGlobal );
this.nextTick(function() { this._open = false; }, lock);
return lock;
},
init: function(aWindow) {
// Set navigator.mozSettings to null.
if (!Services.prefs.getBoolPref("dom.mozSettings.enabled"))
return null;
this._window = aWindow;
Services.obs.addObserver(this, "inner-window-destroyed", false);
let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
this.innerWindowID = util.currentInnerWindowID;
let principal = aWindow.document.nodePrincipal;
let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
let readPerm = principal == secMan.getSystemPrincipal() ? Ci.nsIPermissionManager.ALLOW_ACTION : Services.perms.testExactPermission(principal.URI, "websettings-read");
let readwritePerm = principal == secMan.getSystemPrincipal() ? Ci.nsIPermissionManager.ALLOW_ACTION : Services.perms.testExactPermission(principal.URI, "websettings-readwrite");
this.hasReadPrivileges = readPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
this.hasReadWritePrivileges = readwritePerm == Ci.nsIPermissionManager.ALLOW_ACTION;
debug("has read privileges :" + this.hasReadPrivileges + ", has read-write privileges: " + this.hasReadWritePrivileges);
},
observe: function(aSubject, aTopic, aData) {
debug("Topic: " + aTopic);
if (aTopic == "inner-window-destroyed") {
let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (wId == this.innerWindowID) {
Services.obs.removeObserver(this, "inner-window-destroyed");
this._requests = null;
this._window = null;
this._innerWindowID = null;
this._onsettingchange = null;
this._settingsDB.close();
}
}
},
classID : SETTINGSMANAGER_CID,
QueryInterface : XPCOMUtils.generateQI([nsIDOMSettingsManager, Ci.nsIDOMGlobalPropertyInitializer]),
classInfo : XPCOMUtils.generateCI({classID: SETTINGSMANAGER_CID,
contractID: SETTINGSMANAGER_CONTRACTID,
classDescription: "SettingsManager",
interfaces: [nsIDOMSettingsManager],
flags: nsIClassInfo.DOM_OBJECT})
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock])