dom/datastore/DataStoreService.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
     2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 'use strict'
     9 /* static functions */
    11 function debug(s) {
    12   //dump('DEBUG DataStoreService: ' + s + '\n');
    13 }
    15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    17 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    18 Cu.import('resource://gre/modules/Services.jsm');
    19 Cu.import('resource://gre/modules/DataStoreImpl.jsm');
    20 Cu.import("resource://gre/modules/DataStoreDB.jsm");
    21 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
    23 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
    24                                    "@mozilla.org/childprocessmessagemanager;1",
    25                                    "nsIMessageSender");
    27 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
    28                                    "@mozilla.org/parentprocessmessagemanager;1",
    29                                    "nsIMessageBroadcaster");
    31 XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
    32                                    "@mozilla.org/permissionmanager;1",
    33                                    "nsIPermissionManager");
    35 XPCOMUtils.defineLazyServiceGetter(this, "secMan",
    36                                    "@mozilla.org/scriptsecuritymanager;1",
    37                                    "nsIScriptSecurityManager");
    39 /* DataStoreService */
    41 const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}');
    42 const REVISION_VOID = "void";
    44 function DataStoreService() {
    45   debug('DataStoreService Constructor');
    47   this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
    48                     .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
    50   if (this.inParent) {
    51     let obs = Services.obs;
    52     if (!obs) {
    53       debug("DataStore Error: observer-service is null!");
    54       return;
    55     }
    57     obs.addObserver(this, 'webapps-clear-data', false);
    58   }
    60   let self = this;
    61   cpmm.addMessageListener("datastore-first-revision-created",
    62                           function(aMsg) { self.receiveMessage(aMsg); });
    63 }
    65 DataStoreService.prototype = {
    66   inParent: false,
    68   // Hash of DataStores
    69   stores: {},
    70   accessStores: {},
    71   pendingRequests: {},
    73   installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
    74     debug('installDataStore - appId: ' + aAppId + ', aName: ' +
    75           aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
    76           ', aReadOnly: ' + aReadOnly);
    78     this.checkIfInParent();
    80     if (aName in this.stores && aAppId in this.stores[aName]) {
    81       debug('This should not happen');
    82       return;
    83     }
    85     if (!(aName in this.stores)) {
    86       this.stores[aName] = {};
    87     }
    89     // A DataStore is enabled when it has a first valid revision.
    90     this.stores[aName][aAppId] = { origin: aOrigin, owner: aOwner,
    91                                    readOnly: aReadOnly, enabled: false };
    93     this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
    95     this.createFirstRevisionId(aAppId, aName, aOwner);
    96   },
    98   installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
    99     debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' +
   100           aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
   101           ', aReadOnly: ' + aReadOnly);
   103     this.checkIfInParent();
   105     if (aName in this.accessStores && aAppId in this.accessStores[aName]) {
   106       debug('This should not happen');
   107       return;
   108     }
   110     if (!(aName in this.accessStores)) {
   111       this.accessStores[aName] = {};
   112     }
   114     this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner,
   115                                          readOnly: aReadOnly };
   116     this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
   117   },
   119   checkIfInParent: function() {
   120     if (!this.inParent) {
   121       throw "DataStore can execute this operation just in the parent process";
   122     }
   123   },
   125   createFirstRevisionId: function(aAppId, aName, aOwner) {
   126     debug("createFirstRevisionId database: " + aName);
   128     let self = this;
   129     let db = new DataStoreDB();
   130     db.init(aOwner, aName);
   131     db.revisionTxn(
   132       'readwrite',
   133       function(aTxn, aRevisionStore) {
   134         debug("createFirstRevisionId - transaction success");
   136         let request = aRevisionStore.openCursor(null, 'prev');
   137         request.onsuccess = function(aEvent) {
   138           let cursor = aEvent.target.result;
   139           if (cursor) {
   140             debug("First revision already created.");
   141             self.enableDataStore(aAppId, aName, aOwner);
   142           } else {
   143             // If the revision doesn't exist, let's create the first one.
   144             db.addRevision(aRevisionStore, 0, REVISION_VOID, function() {
   145               debug("First revision created.");
   146               self.enableDataStore(aAppId, aName, aOwner);
   147             });
   148           }
   149         };
   150       }
   151     );
   152   },
   154   enableDataStore: function(aAppId, aName, aOwner) {
   155     if (aName in this.stores && aAppId in this.stores[aName]) {
   156       this.stores[aName][aAppId].enabled = true;
   157       ppmm.broadcastAsyncMessage('datastore-first-revision-created',
   158                                  { name: aName, owner: aOwner });
   159     }
   160   },
   162   addPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
   163     // When a new DataStore is installed, the permissions must be set for the
   164     // owner app.
   165     let permission = "indexedDB-chrome-" + aName + '|' + aOwner;
   166     this.resetPermissions(aAppId, aOrigin, aOwner, permission, aReadOnly);
   168     // For any app that wants to have access to this DataStore we add the
   169     // permissions.
   170     if (aName in this.accessStores) {
   171       for (let appId in this.accessStores[aName]) {
   172         // ReadOnly is decided by the owner first.
   173         let readOnly = aReadOnly || this.accessStores[aName][appId].readOnly;
   174         this.resetPermissions(appId, this.accessStores[aName][appId].origin,
   175                               this.accessStores[aName][appId].owner,
   176                               permission, readOnly);
   177       }
   178     }
   179   },
   181   addAccessPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
   182     // When an app wants to have access to a DataStore, the permissions must be
   183     // set.
   184     if (!(aName in this.stores)) {
   185       return;
   186     }
   188     for (let appId in this.stores[aName]) {
   189       let permission = "indexedDB-chrome-" + aName + '|' + this.stores[aName][appId].owner;
   190       // The ReadOnly is decied by the owenr first.
   191       let readOnly = this.stores[aName][appId].readOnly || aReadOnly;
   192       this.resetPermissions(aAppId, aOrigin, aOwner, permission, readOnly);
   193     }
   194   },
   196   resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) {
   197     debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin +
   198           " - owner: " + aOwner + " - permissions: " + aPermission +
   199           " - readOnly: " + aReadOnly);
   201     let uri = Services.io.newURI(aOrigin, null, null);
   202     let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false);
   204     let result = permissionManager.testExactPermissionFromPrincipal(principal,
   205                                                                     aPermission + '-write');
   207     if (aReadOnly && result == Ci.nsIPermissionManager.ALLOW_ACTION) {
   208       debug("Write permission removed");
   209       permissionManager.removeFromPrincipal(principal, aPermission + '-write');
   210     } else if (!aReadOnly && result != Ci.nsIPermissionManager.ALLOW_ACTION) {
   211       debug("Write permission added");
   212       permissionManager.addFromPrincipal(principal, aPermission + '-write',
   213                                          Ci.nsIPermissionManager.ALLOW_ACTION);
   214     }
   216     result = permissionManager.testExactPermissionFromPrincipal(principal,
   217                                                                 aPermission + '-read');
   218     if (result != Ci.nsIPermissionManager.ALLOW_ACTION) {
   219       debug("Read permission added");
   220       permissionManager.addFromPrincipal(principal, aPermission + '-read',
   221                                          Ci.nsIPermissionManager.ALLOW_ACTION);
   222     }
   224     result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission);
   225     if (result != Ci.nsIPermissionManager.ALLOW_ACTION) {
   226       debug("Generic permission added");
   227       permissionManager.addFromPrincipal(principal, aPermission,
   228                                          Ci.nsIPermissionManager.ALLOW_ACTION);
   229     }
   230   },
   232   getDataStores: function(aWindow, aName) {
   233     debug('getDataStores - aName: ' + aName);
   235     let self = this;
   236     return new aWindow.Promise(function(resolve, reject) {
   237       // If this request comes from the main process, we have access to the
   238       // window, so we can skip the ipc communication.
   239       if (self.inParent) {
   240         let stores = self.getDataStoresInfo(aName, aWindow.document.nodePrincipal.appId);
   241         if (stores === null) {
   242           reject(new aWindow.DOMError("SecurityError", "Access denied"));
   243           return;
   244         }
   245         self.getDataStoreCreate(aWindow, resolve, stores);
   246       } else {
   247         // This method can be called in the child so we need to send a request
   248         // to the parent and create DataStore object here.
   249         new DataStoreServiceChild(aWindow, aName, function(aStores) {
   250           debug("DataStoreServiceChild success callback!");
   251           self.getDataStoreCreate(aWindow, resolve, aStores);
   252         }, function() {
   253           debug("DataStoreServiceChild error callback!");
   254           reject(new aWindow.DOMError("SecurityError", "Access denied"));
   255         });
   256       }
   257     });
   258   },
   260   getDataStoresInfo: function(aName, aAppId) {
   261     debug('GetDataStoresInfo');
   263     let appsService = Cc["@mozilla.org/AppsService;1"]
   264                         .getService(Ci.nsIAppsService);
   265     let app = appsService.getAppByLocalId(aAppId);
   266     if (!app) {
   267       return null;
   268     }
   270     let prefName = "dom.testing.datastore_enabled_for_hosted_apps";
   271     if (app.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
   272         (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID ||
   273           !Services.prefs.getBoolPref(prefName))) {
   274       return null;
   275     }
   277     let results = [];
   279     if (aName in this.stores) {
   280       if (aAppId in this.stores[aName]) {
   281         results.push({ name: aName,
   282                        owner: this.stores[aName][aAppId].owner,
   283                        readOnly: false,
   284                        enabled: this.stores[aName][aAppId].enabled });
   285       }
   287       for (var i in this.stores[aName]) {
   288         if (i == aAppId) {
   289           continue;
   290         }
   292         let access = this.getDataStoreAccess(aName, aAppId);
   293         if (!access) {
   294           continue;
   295         }
   297         let readOnly = this.stores[aName][i].readOnly || access.readOnly;
   298         results.push({ name: aName,
   299                        owner: this.stores[aName][i].owner,
   300                        readOnly: readOnly,
   301                        enabled: this.stores[aName][i].enabled });
   302       }
   303     }
   305     return results;
   306   },
   308   getDataStoreCreate: function(aWindow, aResolve, aStores) {
   309     debug("GetDataStoreCreate");
   311     let results = [];
   313     if (!aStores.length) {
   314       aResolve(results);
   315       return;
   316     }
   318     let pendingDataStores = [];
   320     for (let i = 0; i < aStores.length; ++i) {
   321       if (!aStores[i].enabled) {
   322         pendingDataStores.push(aStores[i].owner);
   323       }
   324     }
   326     if (!pendingDataStores.length) {
   327       this.getDataStoreResolve(aWindow, aResolve, aStores);
   328       return;
   329     }
   331     if (!(aStores[0].name in this.pendingRequests)) {
   332       this.pendingRequests[aStores[0].name] = [];
   333     }
   335     this.pendingRequests[aStores[0].name].push({ window: aWindow,
   336                                                  resolve: aResolve,
   337                                                  stores: aStores,
   338                                                  pendingDataStores: pendingDataStores });
   339   },
   341   getDataStoreResolve: function(aWindow, aResolve, aStores) {
   342     debug("GetDataStoreResolve");
   344     let callbackPending = aStores.length;
   345     let results = [];
   347     if (!callbackPending) {
   348       aResolve(results);
   349       return;
   350     }
   352     for (let i = 0; i < aStores.length; ++i) {
   353       let obj = new DataStore(aWindow, aStores[i].name,
   354                               aStores[i].owner, aStores[i].readOnly);
   356       let storeImpl = aWindow.DataStoreImpl._create(aWindow, obj);
   358       let exposedStore = new aWindow.DataStore();
   359       exposedStore.setDataStoreImpl(storeImpl);
   361       obj.exposedObject = exposedStore;
   363       results.push(exposedStore);
   365       obj.retrieveRevisionId(
   366         function() {
   367           --callbackPending;
   368           if (!callbackPending) {
   369             aResolve(results);
   370           }
   371         }
   372       );
   373     }
   374   },
   376   getDataStoreAccess: function(aName, aAppId) {
   377     if (!(aName in this.accessStores) ||
   378         !(aAppId in this.accessStores[aName])) {
   379       return null;
   380     }
   382     return this.accessStores[aName][aAppId];
   383   },
   385   observe: function observe(aSubject, aTopic, aData) {
   386     debug('observe - aTopic: ' + aTopic);
   387     if (aTopic != 'webapps-clear-data') {
   388       return;
   389     }
   391     let params =
   392       aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
   394     // DataStore is explosed to apps, not browser content.
   395     if (params.browserOnly) {
   396       return;
   397     }
   399     function isEmpty(aMap) {
   400       for (var key in aMap) {
   401         if (aMap.hasOwnProperty(key)) {
   402           return false;
   403         }
   404       }
   405       return true;
   406     }
   408     for (let key in this.stores) {
   409       if (params.appId in this.stores[key]) {
   410         this.deleteDatabase(key, this.stores[key][params.appId].owner);
   411         delete this.stores[key][params.appId];
   412       }
   414       if (isEmpty(this.stores[key])) {
   415         delete this.stores[key];
   416       }
   417     }
   419     for (let key in this.accessStores) {
   420       if (params.appId in this.accessStores[key]) {
   421         delete this.accessStores[key][params.appId];
   422       }
   424       if (isEmpty(this.accessStores[key])) {
   425         delete this.accessStores[key];
   426       }
   427     }
   428   },
   430   deleteDatabase: function(aName, aOwner) {
   431     debug("delete database: " + aName);
   433     let db = new DataStoreDB();
   434     db.init(aOwner, aName);
   435     db.delete();
   436   },
   438   receiveMessage: function(aMsg) {
   439     debug("receiveMessage");
   440     let data = aMsg.json;
   442     if (!(data.name in this.pendingRequests)) {
   443       return;
   444     }
   446     for (let i = 0; i < this.pendingRequests[data.name].length;) {
   447       let pos = this.pendingRequests[data.name][i].pendingDataStores.indexOf(data.owner);
   448       if (pos != -1) {
   449         this.pendingRequests[data.name][i].pendingDataStores.splice(pos, 1);
   450         if (!this.pendingRequests[data.name][i].pendingDataStores.length) {
   451           this.getDataStoreResolve(this.pendingRequests[data.name][i].window,
   452                                    this.pendingRequests[data.name][i].resolve,
   453                                    this.pendingRequests[data.name][i].stores);
   454           this.pendingRequests[data.name].splice(i, 1);
   455           continue;
   456         }
   457       }
   459       ++i;
   460     }
   462     if (!this.pendingRequests[data.name].length) {
   463       delete this.pendingRequests[data.name];
   464     }
   465   },
   467   classID : DATASTORESERVICE_CID,
   468   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService,
   469                                          Ci.nsIObserver]),
   470   classInfo: XPCOMUtils.generateCI({
   471     classID: DATASTORESERVICE_CID,
   472     contractID: '@mozilla.org/datastore-service;1',
   473     interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver],
   474     flags: Ci.nsIClassInfo.SINGLETON
   475   })
   476 };
   478 /* DataStoreServiceChild */
   480 function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) {
   481   debug("DataStoreServiceChild created");
   482   this.init(aWindow, aName, aSuccessCb, aErrorCb);
   483 }
   485 DataStoreServiceChild.prototype = {
   486   __proto__: DOMRequestIpcHelper.prototype,
   488   init: function(aWindow, aName, aSuccessCb, aErrorCb) {
   489     debug("DataStoreServiceChild init");
   490     this._successCb = aSuccessCb;
   491     this._errorCb = aErrorCb;
   493     this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK",
   494                                          "DataStore:Get:Return:KO" ]);
   496     cpmm.sendAsyncMessage("DataStore:Get",
   497                           { name: aName }, null, aWindow.document.nodePrincipal );
   498   },
   500   receiveMessage: function(aMessage) {
   501     debug("DataStoreServiceChild receiveMessage");
   503     switch (aMessage.name) {
   504       case 'DataStore:Get:Return:OK':
   505         this.destroyDOMRequestHelper();
   506         this._successCb(aMessage.data.stores);
   507         break;
   509       case 'DataStore:Get:Return:KO':
   510         this.destroyDOMRequestHelper();
   511         this._errorCb();
   512         break;
   513     }
   514   }
   515 }
   517 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);

mercurial