dom/datastore/DataStoreService.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial