mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 20:47:44 +00:00
494 lines
16 KiB
JavaScript
494 lines
16 KiB
JavaScript
/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* 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 */
|
|
|
|
function debug(s) {
|
|
//dump('DEBUG DataStoreService: ' + s + '\n');
|
|
}
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|
|
|
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
|
Cu.import('resource://gre/modules/Services.jsm');
|
|
Cu.import('resource://gre/modules/DataStore.jsm');
|
|
Cu.import("resource://gre/modules/DataStoreDB.jsm");
|
|
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
|
|
"@mozilla.org/childprocessmessagemanager;1",
|
|
"nsIMessageSender");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
|
|
"@mozilla.org/parentprocessmessagemanager;1",
|
|
"nsIMessageBroadcaster");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
|
|
"@mozilla.org/permissionmanager;1",
|
|
"nsIPermissionManager");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "secMan",
|
|
"@mozilla.org/scriptsecuritymanager;1",
|
|
"nsIScriptSecurityManager");
|
|
|
|
/* DataStoreService */
|
|
|
|
const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}');
|
|
const REVISION_VOID = "void";
|
|
|
|
function DataStoreService() {
|
|
debug('DataStoreService Constructor');
|
|
|
|
this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
|
|
.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
|
|
if (this.inParent) {
|
|
let obs = Services.obs;
|
|
if (!obs) {
|
|
debug("DataStore Error: observer-service is null!");
|
|
return;
|
|
}
|
|
|
|
obs.addObserver(this, 'webapps-clear-data', false);
|
|
}
|
|
|
|
let self = this;
|
|
cpmm.addMessageListener("datastore-first-revision-created",
|
|
function(aMsg) { self.receiveMessage(aMsg); });
|
|
}
|
|
|
|
DataStoreService.prototype = {
|
|
inParent: false,
|
|
|
|
// Hash of DataStores
|
|
stores: {},
|
|
accessStores: {},
|
|
pendingRequests: {},
|
|
|
|
installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
|
|
debug('installDataStore - appId: ' + aAppId + ', aName: ' +
|
|
aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
|
|
', aReadOnly: ' + aReadOnly);
|
|
|
|
this.checkIfInParent();
|
|
|
|
if (aName in this.stores && aAppId in this.stores[aName]) {
|
|
debug('This should not happen');
|
|
return;
|
|
}
|
|
|
|
if (!(aName in this.stores)) {
|
|
this.stores[aName] = {};
|
|
}
|
|
|
|
// A DataStore is enabled when it has a first valid revision.
|
|
this.stores[aName][aAppId] = { origin: aOrigin, owner: aOwner,
|
|
readOnly: aReadOnly, enabled: false };
|
|
|
|
this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
|
|
|
|
this.createFirstRevisionId(aAppId, aName, aOwner);
|
|
},
|
|
|
|
installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
|
|
debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' +
|
|
aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
|
|
', aReadOnly: ' + aReadOnly);
|
|
|
|
this.checkIfInParent();
|
|
|
|
if (aName in this.accessStores && aAppId in this.accessStores[aName]) {
|
|
debug('This should not happen');
|
|
return;
|
|
}
|
|
|
|
if (!(aName in this.accessStores)) {
|
|
this.accessStores[aName] = {};
|
|
}
|
|
|
|
this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner,
|
|
readOnly: aReadOnly };
|
|
this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
|
|
},
|
|
|
|
checkIfInParent: function() {
|
|
if (!this.inParent) {
|
|
throw "DataStore can execute this operation just in the parent process";
|
|
}
|
|
},
|
|
|
|
createFirstRevisionId: function(aAppId, aName, aOwner) {
|
|
debug("createFirstRevisionId database: " + aName);
|
|
|
|
let self = this;
|
|
let db = new DataStoreDB();
|
|
db.init(aOwner, aName);
|
|
db.revisionTxn(
|
|
'readwrite',
|
|
function(aTxn, aRevisionStore) {
|
|
debug("createFirstRevisionId - transaction success");
|
|
|
|
let request = aRevisionStore.openCursor(null, 'prev');
|
|
request.onsuccess = function(aEvent) {
|
|
let cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
debug("First revision already created.");
|
|
self.enableDataStore(aAppId, aName, aOwner);
|
|
} else {
|
|
// If the revision doesn't exist, let's create the first one.
|
|
db.addRevision(aRevisionStore, 0, REVISION_VOID, function() {
|
|
debug("First revision created.");
|
|
self.enableDataStore(aAppId, aName, aOwner);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
);
|
|
},
|
|
|
|
enableDataStore: function(aAppId, aName, aOwner) {
|
|
if (aName in this.stores && aAppId in this.stores[aName]) {
|
|
this.stores[aName][aAppId].enabled = true;
|
|
ppmm.broadcastAsyncMessage('datastore-first-revision-created',
|
|
{ name: aName, owner: aOwner });
|
|
}
|
|
},
|
|
|
|
addPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
|
|
// When a new DataStore is installed, the permissions must be set for the
|
|
// owner app.
|
|
let permission = "indexedDB-chrome-" + aName + '|' + aOwner;
|
|
this.resetPermissions(aAppId, aOrigin, aOwner, permission, aReadOnly);
|
|
|
|
// For any app that wants to have access to this DataStore we add the
|
|
// permissions.
|
|
if (aName in this.accessStores) {
|
|
for (let appId in this.accessStores[aName]) {
|
|
// ReadOnly is decided by the owner first.
|
|
let readOnly = aReadOnly || this.accessStores[aName][appId].readOnly;
|
|
this.resetPermissions(appId, this.accessStores[aName][appId].origin,
|
|
this.accessStores[aName][appId].owner,
|
|
permission, readOnly);
|
|
}
|
|
}
|
|
},
|
|
|
|
addAccessPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
|
|
// When an app wants to have access to a DataStore, the permissions must be
|
|
// set.
|
|
if (!(aName in this.stores)) {
|
|
return;
|
|
}
|
|
|
|
for (let appId in this.stores[aName]) {
|
|
let permission = "indexedDB-chrome-" + aName + '|' + this.stores[aName][appId].owner;
|
|
// The ReadOnly is decied by the owenr first.
|
|
let readOnly = this.stores[aName][appId].readOnly || aReadOnly;
|
|
this.resetPermissions(aAppId, aOrigin, aOwner, permission, readOnly);
|
|
}
|
|
},
|
|
|
|
resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) {
|
|
debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin +
|
|
" - owner: " + aOwner + " - permissions: " + aPermission +
|
|
" - readOnly: " + aReadOnly);
|
|
|
|
let uri = Services.io.newURI(aOrigin, null, null);
|
|
let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false);
|
|
|
|
let result = permissionManager.testExactPermissionFromPrincipal(principal,
|
|
aPermission + '-write');
|
|
|
|
if (aReadOnly && result == Ci.nsIPermissionManager.ALLOW_ACTION) {
|
|
debug("Write permission removed");
|
|
permissionManager.removeFromPrincipal(principal, aPermission + '-write');
|
|
} else if (!aReadOnly && result != Ci.nsIPermissionManager.ALLOW_ACTION) {
|
|
debug("Write permission added");
|
|
permissionManager.addFromPrincipal(principal, aPermission + '-write',
|
|
Ci.nsIPermissionManager.ALLOW_ACTION);
|
|
}
|
|
|
|
result = permissionManager.testExactPermissionFromPrincipal(principal,
|
|
aPermission + '-read');
|
|
if (result != Ci.nsIPermissionManager.ALLOW_ACTION) {
|
|
debug("Read permission added");
|
|
permissionManager.addFromPrincipal(principal, aPermission + '-read',
|
|
Ci.nsIPermissionManager.ALLOW_ACTION);
|
|
}
|
|
|
|
result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission);
|
|
if (result != Ci.nsIPermissionManager.ALLOW_ACTION) {
|
|
debug("Generic permission added");
|
|
permissionManager.addFromPrincipal(principal, aPermission,
|
|
Ci.nsIPermissionManager.ALLOW_ACTION);
|
|
}
|
|
},
|
|
|
|
getDataStores: function(aWindow, aName) {
|
|
debug('getDataStores - aName: ' + aName);
|
|
|
|
let self = this;
|
|
return new aWindow.Promise(function(resolve, reject) {
|
|
// If this request comes from the main process, we have access to the
|
|
// window, so we can skip the ipc communication.
|
|
if (self.inParent) {
|
|
let stores = self.getDataStoresInfo(aName, aWindow.document.nodePrincipal.appId);
|
|
if (stores === null) {
|
|
reject(new aWindow.DOMError("SecurityError", "Access denied"));
|
|
return;
|
|
}
|
|
self.getDataStoreCreate(aWindow, resolve, stores);
|
|
} else {
|
|
// This method can be called in the child so we need to send a request
|
|
// to the parent and create DataStore object here.
|
|
new DataStoreServiceChild(aWindow, aName, function(aStores) {
|
|
debug("DataStoreServiceChild success callback!");
|
|
self.getDataStoreCreate(aWindow, resolve, aStores);
|
|
}, function() {
|
|
debug("DataStoreServiceChild error callback!");
|
|
reject(new aWindow.DOMError("SecurityError", "Access denied"));
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
getDataStoresInfo: function(aName, aAppId) {
|
|
debug('GetDataStoresInfo');
|
|
|
|
let appsService = Cc["@mozilla.org/AppsService;1"]
|
|
.getService(Ci.nsIAppsService);
|
|
let app = appsService.getAppByLocalId(aAppId);
|
|
if (!app) {
|
|
return null;
|
|
}
|
|
|
|
let prefName = "dom.testing.datastore_enabled_for_hosted_apps";
|
|
if (app.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
|
|
(Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID ||
|
|
!Services.prefs.getBoolPref(prefName))) {
|
|
return null;
|
|
}
|
|
|
|
let results = [];
|
|
|
|
if (aName in this.stores) {
|
|
if (aAppId in this.stores[aName]) {
|
|
results.push({ name: aName,
|
|
owner: this.stores[aName][aAppId].owner,
|
|
readOnly: false,
|
|
enabled: this.stores[aName][aAppId].enabled });
|
|
}
|
|
|
|
for (var i in this.stores[aName]) {
|
|
if (i == aAppId) {
|
|
continue;
|
|
}
|
|
|
|
let access = this.getDataStoreAccess(aName, aAppId);
|
|
if (!access) {
|
|
continue;
|
|
}
|
|
|
|
let readOnly = this.stores[aName][i].readOnly || access.readOnly;
|
|
results.push({ name: aName,
|
|
owner: this.stores[aName][i].owner,
|
|
readOnly: readOnly,
|
|
enabled: this.stores[aName][i].enabled });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
getDataStoreCreate: function(aWindow, aResolve, aStores) {
|
|
debug("GetDataStoreCreate");
|
|
|
|
let results = [];
|
|
|
|
if (!aStores.length) {
|
|
aResolve(results);
|
|
return;
|
|
}
|
|
|
|
let pendingDataStores = [];
|
|
|
|
for (let i = 0; i < aStores.length; ++i) {
|
|
if (!aStores[i].enabled) {
|
|
pendingDataStores.push(aStores[i].owner);
|
|
}
|
|
}
|
|
|
|
if (!pendingDataStores.length) {
|
|
this.getDataStoreResolve(aWindow, aResolve, aStores);
|
|
return;
|
|
}
|
|
|
|
if (!(aStores[0].name in this.pendingRequests)) {
|
|
this.pendingRequests[aStores[0].name] = [];
|
|
}
|
|
|
|
this.pendingRequests[aStores[0].name].push({ window: aWindow,
|
|
resolve: aResolve,
|
|
stores: aStores,
|
|
pendingDataStores: pendingDataStores });
|
|
},
|
|
|
|
getDataStoreResolve: function(aWindow, aResolve, aStores) {
|
|
debug("GetDataStoreResolve");
|
|
|
|
let callbackPending = aStores.length;
|
|
let results = [];
|
|
|
|
if (!callbackPending) {
|
|
aResolve(results);
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < aStores.length; ++i) {
|
|
let obj = new DataStore(aWindow, aStores[i].name,
|
|
aStores[i].owner, aStores[i].readOnly);
|
|
let exposedObj = aWindow.DataStore._create(aWindow, obj);
|
|
obj.exposedObject = exposedObj;
|
|
|
|
results.push(exposedObj);
|
|
|
|
obj.retrieveRevisionId(
|
|
function() {
|
|
--callbackPending;
|
|
if (!callbackPending) {
|
|
aResolve(results);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
},
|
|
|
|
getDataStoreAccess: function(aName, aAppId) {
|
|
if (!(aName in this.accessStores) ||
|
|
!(aAppId in this.accessStores[aName])) {
|
|
return null;
|
|
}
|
|
|
|
return this.accessStores[aName][aAppId];
|
|
},
|
|
|
|
observe: function observe(aSubject, aTopic, aData) {
|
|
debug('getDataStores - aTopic: ' + aTopic);
|
|
if (aTopic != 'webapps-clear-data') {
|
|
return;
|
|
}
|
|
|
|
let params =
|
|
aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
|
|
|
|
// DataStore is explosed to apps, not browser content.
|
|
if (params.browserOnly) {
|
|
return;
|
|
}
|
|
|
|
for (let key in this.stores) {
|
|
if (params.appId in this.stores[key]) {
|
|
this.deleteDatabase(key, this.stores[key][params.appId].owner);
|
|
delete this.stores[key][params.appId];
|
|
}
|
|
|
|
if (!this.stores[key].length) {
|
|
delete this.stores[key];
|
|
}
|
|
}
|
|
},
|
|
|
|
deleteDatabase: function(aName, aOwner) {
|
|
debug("delete database: " + aName);
|
|
|
|
let db = new DataStoreDB();
|
|
db.init(aOwner, aName);
|
|
db.delete();
|
|
},
|
|
|
|
receiveMessage: function(aMsg) {
|
|
debug("receiveMessage");
|
|
let data = aMsg.json;
|
|
|
|
if (!(data.name in this.pendingRequests)) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < this.pendingRequests[data.name].length;) {
|
|
let pos = this.pendingRequests[data.name][i].pendingDataStores.indexOf(data.owner);
|
|
if (pos != -1) {
|
|
this.pendingRequests[data.name][i].pendingDataStores.splice(pos, 1);
|
|
if (!this.pendingRequests[data.name][i].pendingDataStores.length) {
|
|
this.getDataStoreResolve(this.pendingRequests[data.name][i].window,
|
|
this.pendingRequests[data.name][i].resolve,
|
|
this.pendingRequests[data.name][i].stores);
|
|
this.pendingRequests[data.name].splice(i, 1);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++i;
|
|
}
|
|
|
|
if (!this.pendingRequests[data.name].length) {
|
|
delete this.pendingRequests[data.name];
|
|
}
|
|
},
|
|
|
|
classID : DATASTORESERVICE_CID,
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService,
|
|
Ci.nsIObserver]),
|
|
classInfo: XPCOMUtils.generateCI({
|
|
classID: DATASTORESERVICE_CID,
|
|
contractID: '@mozilla.org/datastore-service;1',
|
|
interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver],
|
|
flags: Ci.nsIClassInfo.SINGLETON
|
|
})
|
|
};
|
|
|
|
/* DataStoreServiceChild */
|
|
|
|
function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) {
|
|
debug("DataStoreServiceChild created");
|
|
this.init(aWindow, aName, aSuccessCb, aErrorCb);
|
|
}
|
|
|
|
DataStoreServiceChild.prototype = {
|
|
__proto__: DOMRequestIpcHelper.prototype,
|
|
|
|
init: function(aWindow, aName, aSuccessCb, aErrorCb) {
|
|
debug("DataStoreServiceChild init");
|
|
this._successCb = aSuccessCb;
|
|
this._errorCb = aErrorCb;
|
|
|
|
this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK",
|
|
"DataStore:Get:Return:KO" ]);
|
|
|
|
cpmm.sendAsyncMessage("DataStore:Get",
|
|
{ name: aName }, null, aWindow.document.nodePrincipal );
|
|
},
|
|
|
|
receiveMessage: function(aMessage) {
|
|
debug("DataStoreServiceChild receiveMessage");
|
|
|
|
switch (aMessage.name) {
|
|
case 'DataStore:Get:Return:OK':
|
|
this.destroyDOMRequestHelper();
|
|
this._successCb(aMessage.data.stores);
|
|
break;
|
|
|
|
case 'DataStore:Get:Return:KO':
|
|
this.destroyDOMRequestHelper();
|
|
this._errorCb();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);
|