dom/apps/src/Webapps.jsm

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rwxr-xr-x

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const Cu = Components.utils;
michael@0 8 const Cc = Components.classes;
michael@0 9 const Ci = Components.interfaces;
michael@0 10 const Cr = Components.results;
michael@0 11
michael@0 12 // Possible errors thrown by the signature verifier.
michael@0 13 const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
michael@0 14 const SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11);
michael@0 15
michael@0 16 // We need this to decide if we should accept or not files signed with expired
michael@0 17 // certificates.
michael@0 18 function buildIDToTime() {
michael@0 19 let platformBuildID =
michael@0 20 Cc["@mozilla.org/xre/app-info;1"]
michael@0 21 .getService(Ci.nsIXULAppInfo).platformBuildID;
michael@0 22 let platformBuildIDDate = new Date();
michael@0 23 platformBuildIDDate.setUTCFullYear(platformBuildID.substr(0,4),
michael@0 24 platformBuildID.substr(4,2) - 1,
michael@0 25 platformBuildID.substr(6,2));
michael@0 26 platformBuildIDDate.setUTCHours(platformBuildID.substr(8,2),
michael@0 27 platformBuildID.substr(10,2),
michael@0 28 platformBuildID.substr(12,2));
michael@0 29 return platformBuildIDDate.getTime();
michael@0 30 }
michael@0 31
michael@0 32 const PLATFORM_BUILD_ID_TIME = buildIDToTime();
michael@0 33
michael@0 34 this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
michael@0 35
michael@0 36 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 37 Cu.import("resource://gre/modules/Services.jsm");
michael@0 38 Cu.import("resource://gre/modules/FileUtils.jsm");
michael@0 39 Cu.import('resource://gre/modules/ActivitiesService.jsm');
michael@0 40 Cu.import("resource://gre/modules/AppsUtils.jsm");
michael@0 41 Cu.import("resource://gre/modules/AppDownloadManager.jsm");
michael@0 42 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 43 Cu.import("resource://gre/modules/Task.jsm");
michael@0 44 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 45
michael@0 46 XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate",
michael@0 47 "resource://gre/modules/StoreTrustAnchor.jsm");
michael@0 48
michael@0 49 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
michael@0 50 "resource://gre/modules/PermissionsInstaller.jsm");
michael@0 51
michael@0 52 XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller",
michael@0 53 "resource://gre/modules/OfflineCacheInstaller.jsm");
michael@0 54
michael@0 55 XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker",
michael@0 56 "resource://gre/modules/SystemMessagePermissionsChecker.jsm");
michael@0 57
michael@0 58 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
michael@0 59 "resource://gre/modules/WebappOSUtils.jsm");
michael@0 60
michael@0 61 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 62 "resource://gre/modules/NetUtil.jsm");
michael@0 63
michael@0 64 XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
michael@0 65 "resource://gre/modules/ScriptPreloader.jsm");
michael@0 66
michael@0 67 #ifdef MOZ_WIDGET_GONK
michael@0 68 XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
michael@0 69 Cu.import("resource://gre/modules/systemlibs.js");
michael@0 70 return libcutils;
michael@0 71 });
michael@0 72 #endif
michael@0 73
michael@0 74 function debug(aMsg) {
michael@0 75 #ifdef DEBUG
michael@0 76 dump("-*- Webapps.jsm : " + aMsg + "\n");
michael@0 77 #endif
michael@0 78 }
michael@0 79
michael@0 80 function getNSPRErrorCode(err) {
michael@0 81 return -1 * ((err) & 0xffff);
michael@0 82 }
michael@0 83
michael@0 84 function supportUseCurrentProfile() {
michael@0 85 return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile");
michael@0 86 }
michael@0 87
michael@0 88 function supportSystemMessages() {
michael@0 89 return Services.prefs.getBoolPref("dom.sysmsg.enabled");
michael@0 90 }
michael@0 91
michael@0 92 // Minimum delay between two progress events while downloading, in ms.
michael@0 93 const MIN_PROGRESS_EVENT_DELAY = 1500;
michael@0 94
michael@0 95 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
michael@0 96
michael@0 97 const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser";
michael@0 98
michael@0 99 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
michael@0 100 "@mozilla.org/parentprocessmessagemanager;1",
michael@0 101 "nsIMessageBroadcaster");
michael@0 102
michael@0 103 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
michael@0 104 "@mozilla.org/childprocessmessagemanager;1",
michael@0 105 "nsIMessageSender");
michael@0 106
michael@0 107 XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() {
michael@0 108 return Cc["@mozilla.org/inter-app-communication-service;1"]
michael@0 109 .getService(Ci.nsIInterAppCommService);
michael@0 110 });
michael@0 111
michael@0 112 XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService",
michael@0 113 "@mozilla.org/datastore-service;1",
michael@0 114 "nsIDataStoreService");
michael@0 115
michael@0 116 XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
michael@0 117 return Cc["@mozilla.org/system-message-internal;1"]
michael@0 118 .getService(Ci.nsISystemMessagesInternal);
michael@0 119 });
michael@0 120
michael@0 121 XPCOMUtils.defineLazyGetter(this, "updateSvc", function() {
michael@0 122 return Cc["@mozilla.org/offlinecacheupdate-service;1"]
michael@0 123 .getService(Ci.nsIOfflineCacheUpdateService);
michael@0 124 });
michael@0 125
michael@0 126 #ifdef MOZ_WIDGET_GONK
michael@0 127 const DIRECTORY_NAME = "webappsDir";
michael@0 128 #elifdef ANDROID
michael@0 129 const DIRECTORY_NAME = "webappsDir";
michael@0 130 #else
michael@0 131 // If we're executing in the context of the webapp runtime, the data files
michael@0 132 // are in a different directory (currently the Firefox profile that installed
michael@0 133 // the webapp); otherwise, they're in the current profile.
michael@0 134 const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD";
michael@0 135 #endif
michael@0 136
michael@0 137 // We'll use this to identify privileged apps that have been preinstalled
michael@0 138 // For those apps we'll set
michael@0 139 // STORE_ID_PENDING_PREFIX + installOrigin
michael@0 140 // as the storeID. This ensures it's unique and can't be set from a legit
michael@0 141 // store even by error.
michael@0 142 const STORE_ID_PENDING_PREFIX = "#unknownID#";
michael@0 143
michael@0 144 this.DOMApplicationRegistry = {
michael@0 145 // Path to the webapps.json file where we store the registry data.
michael@0 146 appsFile: null,
michael@0 147 webapps: { },
michael@0 148 children: [ ],
michael@0 149 allAppsLaunchable: false,
michael@0 150 _updateHandlers: [ ],
michael@0 151
michael@0 152 init: function() {
michael@0 153 this.messages = ["Webapps:Install", "Webapps:Uninstall",
michael@0 154 "Webapps:GetSelf", "Webapps:CheckInstalled",
michael@0 155 "Webapps:GetInstalled", "Webapps:GetNotInstalled",
michael@0 156 "Webapps:Launch", "Webapps:GetAll",
michael@0 157 "Webapps:InstallPackage",
michael@0 158 "Webapps:GetList", "Webapps:RegisterForMessages",
michael@0 159 "Webapps:UnregisterForMessages",
michael@0 160 "Webapps:CancelDownload", "Webapps:CheckForUpdate",
michael@0 161 "Webapps:Download", "Webapps:ApplyDownload",
michael@0 162 "Webapps:Install:Return:Ack", "Webapps:AddReceipt",
michael@0 163 "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt",
michael@0 164 "child-process-shutdown"];
michael@0 165
michael@0 166 this.frameMessages = ["Webapps:ClearBrowserData"];
michael@0 167
michael@0 168 this.messages.forEach((function(msgName) {
michael@0 169 ppmm.addMessageListener(msgName, this);
michael@0 170 }).bind(this));
michael@0 171
michael@0 172 cpmm.addMessageListener("Activities:Register:OK", this);
michael@0 173
michael@0 174 Services.obs.addObserver(this, "xpcom-shutdown", false);
michael@0 175 Services.obs.addObserver(this, "memory-pressure", false);
michael@0 176
michael@0 177 AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
michael@0 178
michael@0 179 this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
michael@0 180 ["webapps", "webapps.json"], true).path;
michael@0 181
michael@0 182 this.loadAndUpdateApps();
michael@0 183 },
michael@0 184
michael@0 185 // loads the current registry, that could be empty on first run.
michael@0 186 loadCurrentRegistry: function() {
michael@0 187 return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => {
michael@0 188 if (!aData) {
michael@0 189 return;
michael@0 190 }
michael@0 191
michael@0 192 this.webapps = aData;
michael@0 193 let appDir = OS.Path.dirname(this.appsFile);
michael@0 194 for (let id in this.webapps) {
michael@0 195 let app = this.webapps[id];
michael@0 196 if (!app) {
michael@0 197 delete this.webapps[id];
michael@0 198 continue;
michael@0 199 }
michael@0 200
michael@0 201 app.id = id;
michael@0 202
michael@0 203 // Make sure we have a localId
michael@0 204 if (app.localId === undefined) {
michael@0 205 app.localId = this._nextLocalId();
michael@0 206 }
michael@0 207
michael@0 208 if (app.basePath === undefined) {
michael@0 209 app.basePath = appDir;
michael@0 210 }
michael@0 211
michael@0 212 // Default to removable apps.
michael@0 213 if (app.removable === undefined) {
michael@0 214 app.removable = true;
michael@0 215 }
michael@0 216
michael@0 217 // Default to a non privileged status.
michael@0 218 if (app.appStatus === undefined) {
michael@0 219 app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
michael@0 220 }
michael@0 221
michael@0 222 // Default to NO_APP_ID and not in browser.
michael@0 223 if (app.installerAppId === undefined) {
michael@0 224 app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
michael@0 225 }
michael@0 226 if (app.installerIsBrowser === undefined) {
michael@0 227 app.installerIsBrowser = false;
michael@0 228 }
michael@0 229
michael@0 230 // Default installState to "installed", and reset if we shutdown
michael@0 231 // during an update.
michael@0 232 if (app.installState === undefined ||
michael@0 233 app.installState === "updating") {
michael@0 234 app.installState = "installed";
michael@0 235 }
michael@0 236
michael@0 237 // Default storeId to "" and storeVersion to 0
michael@0 238 if (this.webapps[id].storeId === undefined) {
michael@0 239 this.webapps[id].storeId = "";
michael@0 240 }
michael@0 241 if (this.webapps[id].storeVersion === undefined) {
michael@0 242 this.webapps[id].storeVersion = 0;
michael@0 243 }
michael@0 244
michael@0 245 // Default role to "".
michael@0 246 if (this.webapps[id].role === undefined) {
michael@0 247 this.webapps[id].role = "";
michael@0 248 }
michael@0 249
michael@0 250 // At startup we can't be downloading, and the $TMP directory
michael@0 251 // will be empty so we can't just apply a staged update.
michael@0 252 app.downloading = false;
michael@0 253 app.readyToApplyDownload = false;
michael@0 254 }
michael@0 255 });
michael@0 256 },
michael@0 257
michael@0 258 // Notify we are starting with registering apps.
michael@0 259 _registryStarted: Promise.defer(),
michael@0 260 notifyAppsRegistryStart: function notifyAppsRegistryStart() {
michael@0 261 Services.obs.notifyObservers(this, "webapps-registry-start", null);
michael@0 262 this._registryStarted.resolve();
michael@0 263 },
michael@0 264
michael@0 265 get registryStarted() {
michael@0 266 return this._registryStarted.promise;
michael@0 267 },
michael@0 268
michael@0 269 // Notify we are done with registering apps and save a copy of the registry.
michael@0 270 _registryReady: Promise.defer(),
michael@0 271 notifyAppsRegistryReady: function notifyAppsRegistryReady() {
michael@0 272 this._registryReady.resolve();
michael@0 273 Services.obs.notifyObservers(this, "webapps-registry-ready", null);
michael@0 274 this._saveApps();
michael@0 275 },
michael@0 276
michael@0 277 get registryReady() {
michael@0 278 return this._registryReady.promise;
michael@0 279 },
michael@0 280
michael@0 281 // Ensure that the .to property in redirects is a relative URL.
michael@0 282 sanitizeRedirects: function sanitizeRedirects(aSource) {
michael@0 283 if (!aSource) {
michael@0 284 return null;
michael@0 285 }
michael@0 286
michael@0 287 let res = [];
michael@0 288 for (let i = 0; i < aSource.length; i++) {
michael@0 289 let redirect = aSource[i];
michael@0 290 if (redirect.from && redirect.to &&
michael@0 291 isAbsoluteURI(redirect.from) &&
michael@0 292 !isAbsoluteURI(redirect.to)) {
michael@0 293 res.push(redirect);
michael@0 294 }
michael@0 295 }
michael@0 296 return res.length > 0 ? res : null;
michael@0 297 },
michael@0 298
michael@0 299 // Registers all the activities and system messages.
michael@0 300 registerAppsHandlers: function(aRunUpdate) {
michael@0 301 this.notifyAppsRegistryStart();
michael@0 302 let ids = [];
michael@0 303 for (let id in this.webapps) {
michael@0 304 ids.push({ id: id });
michael@0 305 }
michael@0 306 if (supportSystemMessages()) {
michael@0 307 this._processManifestForIds(ids, aRunUpdate);
michael@0 308 } else {
michael@0 309 // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on
michael@0 310 // _processManifestForIds so as to not reading the manifests
michael@0 311 // twice
michael@0 312 this._readManifests(ids).then((aResults) => {
michael@0 313 aResults.forEach((aResult) => {
michael@0 314 if (!aResult.manifest) {
michael@0 315 // If we can't load the manifest, we probably have a corrupted
michael@0 316 // registry. We delete the app since we can't do anything with it.
michael@0 317 delete this.webapps[aResult.id];
michael@0 318 return;
michael@0 319 }
michael@0 320 let app = this.webapps[aResult.id];
michael@0 321 app.csp = aResult.manifest.csp || "";
michael@0 322 app.role = aResult.manifest.role || "";
michael@0 323 if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
michael@0 324 app.redirects = this.sanitizeRedirects(aResult.redirects);
michael@0 325 }
michael@0 326 });
michael@0 327 });
michael@0 328
michael@0 329 // Nothing else to do but notifying we're ready.
michael@0 330 this.notifyAppsRegistryReady();
michael@0 331 }
michael@0 332 },
michael@0 333
michael@0 334 updateDataStoreForApp: function(aId) {
michael@0 335 if (!this.webapps[aId]) {
michael@0 336 return;
michael@0 337 }
michael@0 338
michael@0 339 // Create or Update the DataStore for this app
michael@0 340 this._readManifests([{ id: aId }]).then((aResult) => {
michael@0 341 let app = this.webapps[aId];
michael@0 342 this.updateDataStore(app.localId, app.origin, app.manifestURL,
michael@0 343 aResult[0].manifest, app.appStatus);
michael@0 344 });
michael@0 345 },
michael@0 346
michael@0 347 updatePermissionsForApp: function(aId) {
michael@0 348 if (!this.webapps[aId]) {
michael@0 349 return;
michael@0 350 }
michael@0 351
michael@0 352 // Install the permissions for this app, as if we were updating
michael@0 353 // to cleanup the old ones if needed.
michael@0 354 // TODO It's not clear what this should do when there are multiple profiles.
michael@0 355 if (supportUseCurrentProfile()) {
michael@0 356 this._readManifests([{ id: aId }]).then((aResult) => {
michael@0 357 let data = aResult[0];
michael@0 358 PermissionsInstaller.installPermissions({
michael@0 359 manifest: data.manifest,
michael@0 360 manifestURL: this.webapps[aId].manifestURL,
michael@0 361 origin: this.webapps[aId].origin
michael@0 362 }, true, function() {
michael@0 363 debug("Error installing permissions for " + aId);
michael@0 364 });
michael@0 365 });
michael@0 366 }
michael@0 367 },
michael@0 368
michael@0 369 updateOfflineCacheForApp: function(aId) {
michael@0 370 let app = this.webapps[aId];
michael@0 371 this._readManifests([{ id: aId }]).then((aResult) => {
michael@0 372 let manifest = new ManifestHelper(aResult[0].manifest, app.origin);
michael@0 373 OfflineCacheInstaller.installCache({
michael@0 374 cachePath: app.cachePath,
michael@0 375 appId: aId,
michael@0 376 origin: Services.io.newURI(app.origin, null, null),
michael@0 377 localId: app.localId,
michael@0 378 appcache_path: manifest.fullAppcachePath()
michael@0 379 });
michael@0 380 });
michael@0 381 },
michael@0 382
michael@0 383 // Installs a 3rd party app.
michael@0 384 installPreinstalledApp: function installPreinstalledApp(aId) {
michael@0 385 #ifdef MOZ_WIDGET_GONK
michael@0 386 let app = this.webapps[aId];
michael@0 387 let baseDir;
michael@0 388 try {
michael@0 389 baseDir = FileUtils.getDir("coreAppsDir", ["webapps", aId], false);
michael@0 390 if (!baseDir.exists()) {
michael@0 391 return;
michael@0 392 } else if (!baseDir.directoryEntries.hasMoreElements()) {
michael@0 393 debug("Error: Core app in " + baseDir.path + " is empty");
michael@0 394 return;
michael@0 395 }
michael@0 396 } catch(e) {
michael@0 397 // In ENG builds, we don't have apps in coreAppsDir.
michael@0 398 return;
michael@0 399 }
michael@0 400
michael@0 401 let filesToMove;
michael@0 402 let isPackage;
michael@0 403
michael@0 404 let updateFile = baseDir.clone();
michael@0 405 updateFile.append("update.webapp");
michael@0 406 if (!updateFile.exists()) {
michael@0 407 // The update manifest is missing, this is a hosted app only if there is
michael@0 408 // no application.zip
michael@0 409 let appFile = baseDir.clone();
michael@0 410 appFile.append("application.zip");
michael@0 411 if (appFile.exists()) {
michael@0 412 return;
michael@0 413 }
michael@0 414
michael@0 415 isPackage = false;
michael@0 416 filesToMove = ["manifest.webapp"];
michael@0 417 } else {
michael@0 418 isPackage = true;
michael@0 419 filesToMove = ["application.zip", "update.webapp"];
michael@0 420 }
michael@0 421
michael@0 422 debug("Installing 3rd party app : " + aId +
michael@0 423 " from " + baseDir.path);
michael@0 424
michael@0 425 // We copy this app to DIRECTORY_NAME/$aId, and set the base path as needed.
michael@0 426 let destDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
michael@0 427
michael@0 428 filesToMove.forEach(function(aFile) {
michael@0 429 let file = baseDir.clone();
michael@0 430 file.append(aFile);
michael@0 431 try {
michael@0 432 file.copyTo(destDir, aFile);
michael@0 433 } catch(e) {
michael@0 434 debug("Error: Failed to copy " + file.path + " to " + destDir.path);
michael@0 435 }
michael@0 436 });
michael@0 437
michael@0 438 app.installState = "installed";
michael@0 439 app.cachePath = app.basePath;
michael@0 440 app.basePath = OS.Path.dirname(this.appsFile);
michael@0 441
michael@0 442 if (!isPackage) {
michael@0 443 return;
michael@0 444 }
michael@0 445
michael@0 446 app.origin = "app://" + aId;
michael@0 447
michael@0 448 // Do this for all preinstalled apps... we can't know at this
michael@0 449 // point if the updates will be signed or not and it doesn't
michael@0 450 // hurt to have it always.
michael@0 451 app.storeId = STORE_ID_PENDING_PREFIX + app.installOrigin;
michael@0 452
michael@0 453 // Extract the manifest.webapp file from application.zip.
michael@0 454 let zipFile = baseDir.clone();
michael@0 455 zipFile.append("application.zip");
michael@0 456 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
michael@0 457 .createInstance(Ci.nsIZipReader);
michael@0 458 try {
michael@0 459 debug("Opening " + zipFile.path);
michael@0 460 zipReader.open(zipFile);
michael@0 461 if (!zipReader.hasEntry("manifest.webapp")) {
michael@0 462 throw "MISSING_MANIFEST";
michael@0 463 }
michael@0 464 let manifestFile = destDir.clone();
michael@0 465 manifestFile.append("manifest.webapp");
michael@0 466 zipReader.extract("manifest.webapp", manifestFile);
michael@0 467 } catch(e) {
michael@0 468 // If we are unable to extract the manifest, cleanup and remove this app.
michael@0 469 debug("Cleaning up: " + e);
michael@0 470 destDir.remove(true);
michael@0 471 delete this.webapps[aId];
michael@0 472 } finally {
michael@0 473 zipReader.close();
michael@0 474 }
michael@0 475 #endif
michael@0 476 },
michael@0 477
michael@0 478 // For hosted apps, uninstall an app served from http:// if we have
michael@0 479 // one installed from the same url with an https:// scheme.
michael@0 480 removeIfHttpsDuplicate: function(aId) {
michael@0 481 #ifdef MOZ_WIDGET_GONK
michael@0 482 let app = this.webapps[aId];
michael@0 483 if (!app || !app.origin.startsWith("http://")) {
michael@0 484 return;
michael@0 485 }
michael@0 486
michael@0 487 let httpsManifestURL =
michael@0 488 "https://" + app.manifestURL.substring("http://".length);
michael@0 489
michael@0 490 // This will uninstall the http apps and remove any data hold by this
michael@0 491 // app. Bug 948105 tracks data migration from http to https apps.
michael@0 492 for (let id in this.webapps) {
michael@0 493 if (this.webapps[id].manifestURL === httpsManifestURL) {
michael@0 494 debug("Found a http/https match: " + app.manifestURL + " / " +
michael@0 495 this.webapps[id].manifestURL);
michael@0 496 this.uninstall(app.manifestURL, function() {}, function() {});
michael@0 497 return;
michael@0 498 }
michael@0 499 }
michael@0 500 #endif
michael@0 501 },
michael@0 502
michael@0 503 // Implements the core of bug 787439
michael@0 504 // if at first run, go through these steps:
michael@0 505 // a. load the core apps registry.
michael@0 506 // b. uninstall any core app from the current registry but not in the
michael@0 507 // new core apps registry.
michael@0 508 // c. for all apps in the new core registry, install them if they are not
michael@0 509 // yet in the current registry, and run installPermissions()
michael@0 510 installSystemApps: function() {
michael@0 511 return Task.spawn(function() {
michael@0 512 let file;
michael@0 513 try {
michael@0 514 file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false);
michael@0 515 } catch(e) { }
michael@0 516
michael@0 517 if (!file || !file.exists()) {
michael@0 518 return;
michael@0 519 }
michael@0 520
michael@0 521 // a
michael@0 522 let data = yield AppsUtils.loadJSONAsync(file.path);
michael@0 523 if (!data) {
michael@0 524 return;
michael@0 525 }
michael@0 526
michael@0 527 // b : core apps are not removable.
michael@0 528 for (let id in this.webapps) {
michael@0 529 if (id in data || this.webapps[id].removable)
michael@0 530 continue;
michael@0 531 // Remove the permissions, cookies and private data for this app.
michael@0 532 let localId = this.webapps[id].localId;
michael@0 533 let permMgr = Cc["@mozilla.org/permissionmanager;1"]
michael@0 534 .getService(Ci.nsIPermissionManager);
michael@0 535 permMgr.removePermissionsForApp(localId, false);
michael@0 536 Services.cookies.removeCookiesForApp(localId, false);
michael@0 537 this._clearPrivateData(localId, false);
michael@0 538 delete this.webapps[id];
michael@0 539 }
michael@0 540
michael@0 541 let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
michael@0 542 // c
michael@0 543 for (let id in data) {
michael@0 544 // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org)
michael@0 545 // Use that property to check if they are new or not.
michael@0 546 if (!(id in this.webapps)) {
michael@0 547 this.webapps[id] = data[id];
michael@0 548 this.webapps[id].basePath = appDir.path;
michael@0 549
michael@0 550 this.webapps[id].id = id;
michael@0 551
michael@0 552 // Create a new localId.
michael@0 553 this.webapps[id].localId = this._nextLocalId();
michael@0 554
michael@0 555 // Core apps are not removable.
michael@0 556 if (this.webapps[id].removable === undefined) {
michael@0 557 this.webapps[id].removable = false;
michael@0 558 }
michael@0 559 } else {
michael@0 560 // we fall into this case if the app is present in /system/b2g/webapps/webapps.json
michael@0 561 // and in /data/local/webapps/webapps.json: this happens when updating gaia apps
michael@0 562 // Confere bug 989876
michael@0 563 this.webapps[id].updateTime = data[id].updateTime;
michael@0 564 this.webapps[id].lastUpdateCheck = data[id].updateTime;
michael@0 565 }
michael@0 566 }
michael@0 567 }.bind(this)).then(null, Cu.reportError);
michael@0 568 },
michael@0 569
michael@0 570 loadAndUpdateApps: function() {
michael@0 571 return Task.spawn(function() {
michael@0 572 let runUpdate = AppsUtils.isFirstRun(Services.prefs);
michael@0 573
michael@0 574 yield this.loadCurrentRegistry();
michael@0 575
michael@0 576 if (runUpdate) {
michael@0 577 #ifdef MOZ_WIDGET_GONK
michael@0 578 yield this.installSystemApps();
michael@0 579 #endif
michael@0 580
michael@0 581 // At first run, install preloaded apps and set up their permissions.
michael@0 582 for (let id in this.webapps) {
michael@0 583 this.installPreinstalledApp(id);
michael@0 584 this.removeIfHttpsDuplicate(id);
michael@0 585 if (!this.webapps[id]) {
michael@0 586 continue;
michael@0 587 }
michael@0 588 this.updateOfflineCacheForApp(id);
michael@0 589 this.updatePermissionsForApp(id);
michael@0 590 }
michael@0 591 // Need to update the persisted list of apps since
michael@0 592 // installPreinstalledApp() removes the ones failing to install.
michael@0 593 this._saveApps();
michael@0 594 }
michael@0 595
michael@0 596 // DataStores must be initialized at startup.
michael@0 597 for (let id in this.webapps) {
michael@0 598 this.updateDataStoreForApp(id);
michael@0 599 }
michael@0 600
michael@0 601 this.registerAppsHandlers(runUpdate);
michael@0 602 }.bind(this)).then(null, Cu.reportError);
michael@0 603 },
michael@0 604
michael@0 605 updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) {
michael@0 606 // Just Certified Apps can use DataStores
michael@0 607 let prefName = "dom.testing.datastore_enabled_for_hosted_apps";
michael@0 608 if (aAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
michael@0 609 (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID ||
michael@0 610 !Services.prefs.getBoolPref(prefName))) {
michael@0 611 return;
michael@0 612 }
michael@0 613
michael@0 614 if ('datastores-owned' in aManifest) {
michael@0 615 for (let name in aManifest['datastores-owned']) {
michael@0 616 let readonly = "access" in aManifest['datastores-owned'][name]
michael@0 617 ? aManifest['datastores-owned'][name].access == 'readonly'
michael@0 618 : false;
michael@0 619
michael@0 620 dataStoreService.installDataStore(aId, name, aOrigin, aManifestURL,
michael@0 621 readonly);
michael@0 622 }
michael@0 623 }
michael@0 624
michael@0 625 if ('datastores-access' in aManifest) {
michael@0 626 for (let name in aManifest['datastores-access']) {
michael@0 627 let readonly = ("readonly" in aManifest['datastores-access'][name]) &&
michael@0 628 !aManifest['datastores-access'][name].readonly
michael@0 629 ? false : true;
michael@0 630
michael@0 631 dataStoreService.installAccessDataStore(aId, name, aOrigin,
michael@0 632 aManifestURL, readonly);
michael@0 633 }
michael@0 634 }
michael@0 635 },
michael@0 636
michael@0 637 // |aEntryPoint| is either the entry_point name or the null in which case we
michael@0 638 // use the root of the manifest.
michael@0 639 //
michael@0 640 // TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...).
michael@0 641 _registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) {
michael@0 642 let root = aManifest;
michael@0 643 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
michael@0 644 root = aManifest.entry_points[aEntryPoint];
michael@0 645 }
michael@0 646
michael@0 647 if (!root.messages || !Array.isArray(root.messages) ||
michael@0 648 root.messages.length == 0) {
michael@0 649 return;
michael@0 650 }
michael@0 651
michael@0 652 let manifest = new ManifestHelper(aManifest, aApp.origin);
michael@0 653 let launchPath = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), null, null);
michael@0 654 let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
michael@0 655 root.messages.forEach(function registerPages(aMessage) {
michael@0 656 let href = launchPath;
michael@0 657 let messageName;
michael@0 658 if (typeof(aMessage) === "object" && Object.keys(aMessage).length === 1) {
michael@0 659 messageName = Object.keys(aMessage)[0];
michael@0 660 let uri;
michael@0 661 try {
michael@0 662 uri = manifest.resolveFromOrigin(aMessage[messageName]);
michael@0 663 } catch(e) {
michael@0 664 debug("system message url (" + aMessage[messageName] + ") is invalid, skipping. " +
michael@0 665 "Error is: " + e);
michael@0 666 return;
michael@0 667 }
michael@0 668 href = Services.io.newURI(uri, null, null);
michael@0 669 } else {
michael@0 670 messageName = aMessage;
michael@0 671 }
michael@0 672
michael@0 673 if (SystemMessagePermissionsChecker
michael@0 674 .isSystemMessagePermittedToRegister(messageName,
michael@0 675 aApp.origin,
michael@0 676 aManifest)) {
michael@0 677 msgmgr.registerPage(messageName, href, manifestURL);
michael@0 678 }
michael@0 679 });
michael@0 680 },
michael@0 681
michael@0 682 // |aEntryPoint| is either the entry_point name or the null in which case we
michael@0 683 // use the root of the manifest.
michael@0 684 //
michael@0 685 // TODO Bug 908094 Refine _registerInterAppConnectionsForEntryPoint(...).
michael@0 686 _registerInterAppConnectionsForEntryPoint: function(aManifest, aApp,
michael@0 687 aEntryPoint) {
michael@0 688 let root = aManifest;
michael@0 689 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
michael@0 690 root = aManifest.entry_points[aEntryPoint];
michael@0 691 }
michael@0 692
michael@0 693 let connections = root.connections;
michael@0 694 if (!connections) {
michael@0 695 return;
michael@0 696 }
michael@0 697
michael@0 698 if ((typeof connections) !== "object") {
michael@0 699 debug("|connections| is not an object. Skipping: " + connections);
michael@0 700 return;
michael@0 701 }
michael@0 702
michael@0 703 let manifest = new ManifestHelper(aManifest, aApp.origin);
michael@0 704 let launchPathURI = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint),
michael@0 705 null, null);
michael@0 706 let manifestURI = Services.io.newURI(aApp.manifestURL, null, null);
michael@0 707
michael@0 708 for (let keyword in connections) {
michael@0 709 let connection = connections[keyword];
michael@0 710
michael@0 711 // Resolve the handler path from origin. If |handler_path| is absent,
michael@0 712 // use |launch_path| as default.
michael@0 713 let fullHandlerPath;
michael@0 714 let handlerPath = connection.handler_path;
michael@0 715 if (handlerPath) {
michael@0 716 try {
michael@0 717 fullHandlerPath = manifest.resolveFromOrigin(handlerPath);
michael@0 718 } catch(e) {
michael@0 719 debug("Connection's handler path is invalid. Skipping: keyword: " +
michael@0 720 keyword + " handler_path: " + handlerPath);
michael@0 721 continue;
michael@0 722 }
michael@0 723 }
michael@0 724 let handlerPageURI = fullHandlerPath
michael@0 725 ? Services.io.newURI(fullHandlerPath, null, null)
michael@0 726 : launchPathURI;
michael@0 727
michael@0 728 if (SystemMessagePermissionsChecker
michael@0 729 .isSystemMessagePermittedToRegister("connection",
michael@0 730 aApp.origin,
michael@0 731 aManifest)) {
michael@0 732 msgmgr.registerPage("connection", handlerPageURI, manifestURI);
michael@0 733 }
michael@0 734
michael@0 735 interAppCommService.
michael@0 736 registerConnection(keyword,
michael@0 737 handlerPageURI,
michael@0 738 manifestURI,
michael@0 739 connection.description,
michael@0 740 connection.rules);
michael@0 741 }
michael@0 742 },
michael@0 743
michael@0 744 _registerSystemMessages: function(aManifest, aApp) {
michael@0 745 this._registerSystemMessagesForEntryPoint(aManifest, aApp, null);
michael@0 746
michael@0 747 if (!aManifest.entry_points) {
michael@0 748 return;
michael@0 749 }
michael@0 750
michael@0 751 for (let entryPoint in aManifest.entry_points) {
michael@0 752 this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint);
michael@0 753 }
michael@0 754 },
michael@0 755
michael@0 756 _registerInterAppConnections: function(aManifest, aApp) {
michael@0 757 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, null);
michael@0 758
michael@0 759 if (!aManifest.entry_points) {
michael@0 760 return;
michael@0 761 }
michael@0 762
michael@0 763 for (let entryPoint in aManifest.entry_points) {
michael@0 764 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp,
michael@0 765 entryPoint);
michael@0 766 }
michael@0 767 },
michael@0 768
michael@0 769 // |aEntryPoint| is either the entry_point name or the null in which case we
michael@0 770 // use the root of the manifest.
michael@0 771 _createActivitiesToRegister: function(aManifest, aApp, aEntryPoint, aRunUpdate) {
michael@0 772 let activitiesToRegister = [];
michael@0 773 let root = aManifest;
michael@0 774 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
michael@0 775 root = aManifest.entry_points[aEntryPoint];
michael@0 776 }
michael@0 777
michael@0 778 if (!root.activities) {
michael@0 779 return activitiesToRegister;
michael@0 780 }
michael@0 781
michael@0 782 let manifest = new ManifestHelper(aManifest, aApp.origin);
michael@0 783 for (let activity in root.activities) {
michael@0 784 let description = root.activities[activity];
michael@0 785 let href = description.href;
michael@0 786 if (!href) {
michael@0 787 href = manifest.launch_path;
michael@0 788 }
michael@0 789
michael@0 790 try {
michael@0 791 href = manifest.resolveFromOrigin(href);
michael@0 792 } catch (e) {
michael@0 793 debug("Activity href (" + href + ") is invalid, skipping. " +
michael@0 794 "Error is: " + e);
michael@0 795 continue;
michael@0 796 }
michael@0 797
michael@0 798 // Make a copy of the description object since we don't want to modify
michael@0 799 // the manifest itself, but need to register with a resolved URI.
michael@0 800 let newDesc = {};
michael@0 801 for (let prop in description) {
michael@0 802 newDesc[prop] = description[prop];
michael@0 803 }
michael@0 804 newDesc.href = href;
michael@0 805
michael@0 806 debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' +
michael@0 807 activity + ', description.href is ' + newDesc.href);
michael@0 808
michael@0 809 if (aRunUpdate) {
michael@0 810 activitiesToRegister.push({ "manifest": aApp.manifestURL,
michael@0 811 "name": activity,
michael@0 812 "icon": manifest.iconURLForSize(128),
michael@0 813 "description": newDesc });
michael@0 814 }
michael@0 815
michael@0 816 let launchPath = Services.io.newURI(href, null, null);
michael@0 817 let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
michael@0 818
michael@0 819 if (SystemMessagePermissionsChecker
michael@0 820 .isSystemMessagePermittedToRegister("activity",
michael@0 821 aApp.origin,
michael@0 822 aManifest)) {
michael@0 823 msgmgr.registerPage("activity", launchPath, manifestURL);
michael@0 824 }
michael@0 825 }
michael@0 826 return activitiesToRegister;
michael@0 827 },
michael@0 828
michael@0 829 // |aAppsToRegister| contains an array of apps to be registered, where
michael@0 830 // each element is an object in the format of {manifest: foo, app: bar}.
michael@0 831 _registerActivitiesForApps: function(aAppsToRegister, aRunUpdate) {
michael@0 832 // Collect the activities to be registered for root and entry_points.
michael@0 833 let activitiesToRegister = [];
michael@0 834 aAppsToRegister.forEach(function (aApp) {
michael@0 835 let manifest = aApp.manifest;
michael@0 836 let app = aApp.app;
michael@0 837 activitiesToRegister.push.apply(activitiesToRegister,
michael@0 838 this._createActivitiesToRegister(manifest, app, null, aRunUpdate));
michael@0 839
michael@0 840 if (!manifest.entry_points) {
michael@0 841 return;
michael@0 842 }
michael@0 843
michael@0 844 for (let entryPoint in manifest.entry_points) {
michael@0 845 activitiesToRegister.push.apply(activitiesToRegister,
michael@0 846 this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate));
michael@0 847 }
michael@0 848 }, this);
michael@0 849
michael@0 850 if (!aRunUpdate || activitiesToRegister.length == 0) {
michael@0 851 this.notifyAppsRegistryReady();
michael@0 852 return;
michael@0 853 }
michael@0 854
michael@0 855 // Send the array carrying all the activities to be registered.
michael@0 856 cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister);
michael@0 857 },
michael@0 858
michael@0 859 // Better to directly use |_registerActivitiesForApps()| if we have
michael@0 860 // multiple apps to be registered for activities.
michael@0 861 _registerActivities: function(aManifest, aApp, aRunUpdate) {
michael@0 862 this._registerActivitiesForApps([{ manifest: aManifest, app: aApp }], aRunUpdate);
michael@0 863 },
michael@0 864
michael@0 865 // |aEntryPoint| is either the entry_point name or the null in which case we
michael@0 866 // use the root of the manifest.
michael@0 867 _createActivitiesToUnregister: function(aManifest, aApp, aEntryPoint) {
michael@0 868 let activitiesToUnregister = [];
michael@0 869 let root = aManifest;
michael@0 870 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
michael@0 871 root = aManifest.entry_points[aEntryPoint];
michael@0 872 }
michael@0 873
michael@0 874 if (!root.activities) {
michael@0 875 return activitiesToUnregister;
michael@0 876 }
michael@0 877
michael@0 878 for (let activity in root.activities) {
michael@0 879 let description = root.activities[activity];
michael@0 880 activitiesToUnregister.push({ "manifest": aApp.manifestURL,
michael@0 881 "name": activity,
michael@0 882 "description": description });
michael@0 883 }
michael@0 884 return activitiesToUnregister;
michael@0 885 },
michael@0 886
michael@0 887 // |aAppsToUnregister| contains an array of apps to be unregistered, where
michael@0 888 // each element is an object in the format of {manifest: foo, app: bar}.
michael@0 889 _unregisterActivitiesForApps: function(aAppsToUnregister) {
michael@0 890 // Collect the activities to be unregistered for root and entry_points.
michael@0 891 let activitiesToUnregister = [];
michael@0 892 aAppsToUnregister.forEach(function (aApp) {
michael@0 893 let manifest = aApp.manifest;
michael@0 894 let app = aApp.app;
michael@0 895 activitiesToUnregister.push.apply(activitiesToUnregister,
michael@0 896 this._createActivitiesToUnregister(manifest, app, null));
michael@0 897
michael@0 898 if (!manifest.entry_points) {
michael@0 899 return;
michael@0 900 }
michael@0 901
michael@0 902 for (let entryPoint in manifest.entry_points) {
michael@0 903 activitiesToUnregister.push.apply(activitiesToUnregister,
michael@0 904 this._createActivitiesToUnregister(manifest, app, entryPoint));
michael@0 905 }
michael@0 906 }, this);
michael@0 907
michael@0 908 // Send the array carrying all the activities to be unregistered.
michael@0 909 cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister);
michael@0 910 },
michael@0 911
michael@0 912 // Better to directly use |_unregisterActivitiesForApps()| if we have
michael@0 913 // multiple apps to be unregistered for activities.
michael@0 914 _unregisterActivities: function(aManifest, aApp) {
michael@0 915 this._unregisterActivitiesForApps([{ manifest: aManifest, app: aApp }]);
michael@0 916 },
michael@0 917
michael@0 918 _processManifestForIds: function(aIds, aRunUpdate) {
michael@0 919 this._readManifests(aIds).then((aResults) => {
michael@0 920 let appsToRegister = [];
michael@0 921 aResults.forEach((aResult) => {
michael@0 922 let app = this.webapps[aResult.id];
michael@0 923 let manifest = aResult.manifest;
michael@0 924 if (!manifest) {
michael@0 925 // If we can't load the manifest, we probably have a corrupted
michael@0 926 // registry. We delete the app since we can't do anything with it.
michael@0 927 delete this.webapps[aResult.id];
michael@0 928 return;
michael@0 929 }
michael@0 930 app.name = manifest.name;
michael@0 931 app.csp = manifest.csp || "";
michael@0 932 app.role = manifest.role || "";
michael@0 933 if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
michael@0 934 app.redirects = this.sanitizeRedirects(manifest.redirects);
michael@0 935 }
michael@0 936 this._registerSystemMessages(manifest, app);
michael@0 937 this._registerInterAppConnections(manifest, app);
michael@0 938 appsToRegister.push({ manifest: manifest, app: app });
michael@0 939 });
michael@0 940 this._registerActivitiesForApps(appsToRegister, aRunUpdate);
michael@0 941 });
michael@0 942 },
michael@0 943
michael@0 944 observe: function(aSubject, aTopic, aData) {
michael@0 945 if (aTopic == "xpcom-shutdown") {
michael@0 946 this.messages.forEach((function(msgName) {
michael@0 947 ppmm.removeMessageListener(msgName, this);
michael@0 948 }).bind(this));
michael@0 949 Services.obs.removeObserver(this, "xpcom-shutdown");
michael@0 950 cpmm = null;
michael@0 951 ppmm = null;
michael@0 952 } else if (aTopic == "memory-pressure") {
michael@0 953 // Clear the manifest cache on memory pressure.
michael@0 954 this._manifestCache = {};
michael@0 955 }
michael@0 956 },
michael@0 957
michael@0 958 addMessageListener: function(aMsgNames, aApp, aMm) {
michael@0 959 aMsgNames.forEach(function (aMsgName) {
michael@0 960 let man = aApp && aApp.manifestURL;
michael@0 961 if (!(aMsgName in this.children)) {
michael@0 962 this.children[aMsgName] = [];
michael@0 963 }
michael@0 964
michael@0 965 let mmFound = this.children[aMsgName].some(function(mmRef) {
michael@0 966 if (mmRef.mm === aMm) {
michael@0 967 mmRef.refCount++;
michael@0 968 return true;
michael@0 969 }
michael@0 970 return false;
michael@0 971 });
michael@0 972
michael@0 973 if (!mmFound) {
michael@0 974 this.children[aMsgName].push({
michael@0 975 mm: aMm,
michael@0 976 refCount: 1
michael@0 977 });
michael@0 978 }
michael@0 979
michael@0 980 // If the state reported by the registration is outdated, update it now.
michael@0 981 if ((aMsgName === 'Webapps:FireEvent') ||
michael@0 982 (aMsgName === 'Webapps:UpdateState')) {
michael@0 983 if (man) {
michael@0 984 let app = this.getAppByManifestURL(aApp.manifestURL);
michael@0 985 if (app && ((aApp.installState !== app.installState) ||
michael@0 986 (aApp.downloading !== app.downloading))) {
michael@0 987 debug("Got a registration from an outdated app: " +
michael@0 988 aApp.manifestURL);
michael@0 989 let aEvent ={
michael@0 990 type: app.installState,
michael@0 991 app: app,
michael@0 992 manifestURL: app.manifestURL,
michael@0 993 manifest: app.manifest
michael@0 994 };
michael@0 995 aMm.sendAsyncMessage(aMsgName, aEvent);
michael@0 996 }
michael@0 997 }
michael@0 998 }
michael@0 999 }, this);
michael@0 1000 },
michael@0 1001
michael@0 1002 removeMessageListener: function(aMsgNames, aMm) {
michael@0 1003 if (aMsgNames.length === 1 &&
michael@0 1004 aMsgNames[0] === "Webapps:Internal:AllMessages") {
michael@0 1005 for (let msgName in this.children) {
michael@0 1006 let msg = this.children[msgName];
michael@0 1007
michael@0 1008 for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) {
michael@0 1009 let mmRef = msg[mmI];
michael@0 1010 if (mmRef.mm === aMm) {
michael@0 1011 msg.splice(mmI, 1);
michael@0 1012 }
michael@0 1013 }
michael@0 1014
michael@0 1015 if (msg.length === 0) {
michael@0 1016 delete this.children[msgName];
michael@0 1017 }
michael@0 1018 }
michael@0 1019 return;
michael@0 1020 }
michael@0 1021
michael@0 1022 aMsgNames.forEach(function(aMsgName) {
michael@0 1023 if (!(aMsgName in this.children)) {
michael@0 1024 return;
michael@0 1025 }
michael@0 1026
michael@0 1027 let removeIndex;
michael@0 1028 this.children[aMsgName].some(function(mmRef, index) {
michael@0 1029 if (mmRef.mm === aMm) {
michael@0 1030 mmRef.refCount--;
michael@0 1031 if (mmRef.refCount === 0) {
michael@0 1032 removeIndex = index;
michael@0 1033 }
michael@0 1034 return true;
michael@0 1035 }
michael@0 1036 return false;
michael@0 1037 });
michael@0 1038
michael@0 1039 if (removeIndex) {
michael@0 1040 this.children[aMsgName].splice(removeIndex, 1);
michael@0 1041 }
michael@0 1042 }, this);
michael@0 1043 },
michael@0 1044
michael@0 1045 receiveMessage: function(aMessage) {
michael@0 1046 // nsIPrefBranch throws if pref does not exist, faster to simply write
michael@0 1047 // the pref instead of first checking if it is false.
michael@0 1048 Services.prefs.setBoolPref("dom.mozApps.used", true);
michael@0 1049
michael@0 1050 // We need to check permissions for calls coming from mozApps.mgmt.
michael@0 1051 // These are: getAll(), getNotInstalled(), applyDownload() and uninstall().
michael@0 1052 if (["Webapps:GetAll",
michael@0 1053 "Webapps:GetNotInstalled",
michael@0 1054 "Webapps:ApplyDownload",
michael@0 1055 "Webapps:Uninstall"].indexOf(aMessage.name) != -1) {
michael@0 1056 if (!aMessage.target.assertPermission("webapps-manage")) {
michael@0 1057 debug("mozApps message " + aMessage.name +
michael@0 1058 " from a content process with no 'webapps-manage' privileges.");
michael@0 1059 return null;
michael@0 1060 }
michael@0 1061 }
michael@0 1062
michael@0 1063 let msg = aMessage.data || {};
michael@0 1064 let mm = aMessage.target;
michael@0 1065 msg.mm = mm;
michael@0 1066
michael@0 1067 switch (aMessage.name) {
michael@0 1068 case "Webapps:Install": {
michael@0 1069 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 1070 Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
michael@0 1071 #else
michael@0 1072 this.doInstall(msg, mm);
michael@0 1073 #endif
michael@0 1074 break;
michael@0 1075 }
michael@0 1076 case "Webapps:GetSelf":
michael@0 1077 this.getSelf(msg, mm);
michael@0 1078 break;
michael@0 1079 case "Webapps:Uninstall":
michael@0 1080 this.doUninstall(msg, mm);
michael@0 1081 break;
michael@0 1082 case "Webapps:Launch":
michael@0 1083 this.doLaunch(msg, mm);
michael@0 1084 break;
michael@0 1085 case "Webapps:CheckInstalled":
michael@0 1086 this.checkInstalled(msg, mm);
michael@0 1087 break;
michael@0 1088 case "Webapps:GetInstalled":
michael@0 1089 this.getInstalled(msg, mm);
michael@0 1090 break;
michael@0 1091 case "Webapps:GetNotInstalled":
michael@0 1092 this.getNotInstalled(msg, mm);
michael@0 1093 break;
michael@0 1094 case "Webapps:GetAll":
michael@0 1095 this.doGetAll(msg, mm);
michael@0 1096 break;
michael@0 1097 case "Webapps:InstallPackage": {
michael@0 1098 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 1099 Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
michael@0 1100 #else
michael@0 1101 this.doInstallPackage(msg, mm);
michael@0 1102 #endif
michael@0 1103 break;
michael@0 1104 }
michael@0 1105 case "Webapps:RegisterForMessages":
michael@0 1106 this.addMessageListener(msg.messages, msg.app, mm);
michael@0 1107 break;
michael@0 1108 case "Webapps:UnregisterForMessages":
michael@0 1109 this.removeMessageListener(msg, mm);
michael@0 1110 break;
michael@0 1111 case "child-process-shutdown":
michael@0 1112 this.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
michael@0 1113 break;
michael@0 1114 case "Webapps:GetList":
michael@0 1115 this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], null, mm);
michael@0 1116 return this.webapps;
michael@0 1117 case "Webapps:Download":
michael@0 1118 this.startDownload(msg.manifestURL);
michael@0 1119 break;
michael@0 1120 case "Webapps:CancelDownload":
michael@0 1121 this.cancelDownload(msg.manifestURL);
michael@0 1122 break;
michael@0 1123 case "Webapps:CheckForUpdate":
michael@0 1124 this.checkForUpdate(msg, mm);
michael@0 1125 break;
michael@0 1126 case "Webapps:ApplyDownload":
michael@0 1127 this.applyDownload(msg.manifestURL);
michael@0 1128 break;
michael@0 1129 case "Activities:Register:OK":
michael@0 1130 this.notifyAppsRegistryReady();
michael@0 1131 break;
michael@0 1132 case "Webapps:Install:Return:Ack":
michael@0 1133 this.onInstallSuccessAck(msg.manifestURL);
michael@0 1134 break;
michael@0 1135 case "Webapps:AddReceipt":
michael@0 1136 this.addReceipt(msg, mm);
michael@0 1137 break;
michael@0 1138 case "Webapps:RemoveReceipt":
michael@0 1139 this.removeReceipt(msg, mm);
michael@0 1140 break;
michael@0 1141 case "Webapps:ReplaceReceipt":
michael@0 1142 this.replaceReceipt(msg, mm);
michael@0 1143 break;
michael@0 1144 }
michael@0 1145 },
michael@0 1146
michael@0 1147 getAppInfo: function getAppInfo(aAppId) {
michael@0 1148 return AppsUtils.getAppInfo(this.webapps, aAppId);
michael@0 1149 },
michael@0 1150
michael@0 1151 // Some messages can be listened by several content processes:
michael@0 1152 // Webapps:AddApp
michael@0 1153 // Webapps:RemoveApp
michael@0 1154 // Webapps:Install:Return:OK
michael@0 1155 // Webapps:Uninstall:Return:OK
michael@0 1156 // Webapps:Uninstall:Broadcast:Return:OK
michael@0 1157 // Webapps:FireEvent
michael@0 1158 // Webapps:checkForUpdate:Return:OK
michael@0 1159 // Webapps:UpdateState
michael@0 1160 broadcastMessage: function broadcastMessage(aMsgName, aContent) {
michael@0 1161 if (!(aMsgName in this.children)) {
michael@0 1162 return;
michael@0 1163 }
michael@0 1164 this.children[aMsgName].forEach(function(mmRef) {
michael@0 1165 mmRef.mm.sendAsyncMessage(aMsgName, aContent);
michael@0 1166 });
michael@0 1167 },
michael@0 1168
michael@0 1169 registerUpdateHandler: function(aHandler) {
michael@0 1170 this._updateHandlers.push(aHandler);
michael@0 1171 },
michael@0 1172
michael@0 1173 unregisterUpdateHandler: function(aHandler) {
michael@0 1174 let index = this._updateHandlers.indexOf(aHandler);
michael@0 1175 if (index != -1) {
michael@0 1176 this._updateHandlers.splice(index, 1);
michael@0 1177 }
michael@0 1178 },
michael@0 1179
michael@0 1180 notifyUpdateHandlers: function(aApp, aManifest, aZipPath) {
michael@0 1181 for (let updateHandler of this._updateHandlers) {
michael@0 1182 updateHandler(aApp, aManifest, aZipPath);
michael@0 1183 }
michael@0 1184 },
michael@0 1185
michael@0 1186 _getAppDir: function(aId) {
michael@0 1187 return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
michael@0 1188 },
michael@0 1189
michael@0 1190 _writeFile: function(aPath, aData) {
michael@0 1191 debug("Saving " + aPath);
michael@0 1192
michael@0 1193 let deferred = Promise.defer();
michael@0 1194
michael@0 1195 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
michael@0 1196 file.initWithPath(aPath);
michael@0 1197
michael@0 1198 // Initialize the file output stream
michael@0 1199 let ostream = FileUtils.openSafeFileOutputStream(file);
michael@0 1200
michael@0 1201 // Obtain a converter to convert our data to a UTF-8 encoded input stream.
michael@0 1202 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
michael@0 1203 .createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 1204 converter.charset = "UTF-8";
michael@0 1205
michael@0 1206 // Asynchronously copy the data to the file.
michael@0 1207 let istream = converter.convertToInputStream(aData);
michael@0 1208 NetUtil.asyncCopy(istream, ostream, function(aResult) {
michael@0 1209 if (!Components.isSuccessCode(aResult)) {
michael@0 1210 deferred.reject()
michael@0 1211 } else {
michael@0 1212 deferred.resolve();
michael@0 1213 }
michael@0 1214 });
michael@0 1215
michael@0 1216 return deferred.promise;
michael@0 1217 },
michael@0 1218
michael@0 1219 doLaunch: function (aData, aMm) {
michael@0 1220 this.launch(
michael@0 1221 aData.manifestURL,
michael@0 1222 aData.startPoint,
michael@0 1223 aData.timestamp,
michael@0 1224 function onsuccess() {
michael@0 1225 aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData);
michael@0 1226 },
michael@0 1227 function onfailure(reason) {
michael@0 1228 aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData);
michael@0 1229 }
michael@0 1230 );
michael@0 1231 },
michael@0 1232
michael@0 1233 launch: function launch(aManifestURL, aStartPoint, aTimeStamp, aOnSuccess, aOnFailure) {
michael@0 1234 let app = this.getAppByManifestURL(aManifestURL);
michael@0 1235 if (!app) {
michael@0 1236 aOnFailure("NO_SUCH_APP");
michael@0 1237 return;
michael@0 1238 }
michael@0 1239
michael@0 1240 // Fire an error when trying to launch an app that is not
michael@0 1241 // yet fully installed.
michael@0 1242 if (app.installState == "pending") {
michael@0 1243 aOnFailure("PENDING_APP_NOT_LAUNCHABLE");
michael@0 1244 return;
michael@0 1245 }
michael@0 1246
michael@0 1247 // We have to clone the app object as nsIDOMApplication objects are
michael@0 1248 // stringified as an empty object. (see bug 830376)
michael@0 1249 let appClone = AppsUtils.cloneAppObject(app);
michael@0 1250 appClone.startPoint = aStartPoint;
michael@0 1251 appClone.timestamp = aTimeStamp;
michael@0 1252 Services.obs.notifyObservers(null, "webapps-launch", JSON.stringify(appClone));
michael@0 1253 aOnSuccess();
michael@0 1254 },
michael@0 1255
michael@0 1256 close: function close(aApp) {
michael@0 1257 debug("close");
michael@0 1258
michael@0 1259 // We have to clone the app object as nsIDOMApplication objects are
michael@0 1260 // stringified as an empty object. (see bug 830376)
michael@0 1261 let appClone = AppsUtils.cloneAppObject(aApp);
michael@0 1262 Services.obs.notifyObservers(null, "webapps-close", JSON.stringify(appClone));
michael@0 1263 },
michael@0 1264
michael@0 1265 cancelDownload: function cancelDownload(aManifestURL, aError) {
michael@0 1266 debug("cancelDownload " + aManifestURL);
michael@0 1267 let error = aError || "DOWNLOAD_CANCELED";
michael@0 1268 let download = AppDownloadManager.get(aManifestURL);
michael@0 1269 if (!download) {
michael@0 1270 debug("Could not find a download for " + aManifestURL);
michael@0 1271 return;
michael@0 1272 }
michael@0 1273
michael@0 1274 let app = this.webapps[download.appId];
michael@0 1275
michael@0 1276 if (download.cacheUpdate) {
michael@0 1277 try {
michael@0 1278 download.cacheUpdate.cancel();
michael@0 1279 } catch (e) {
michael@0 1280 debug (e);
michael@0 1281 }
michael@0 1282 } else if (download.channel) {
michael@0 1283 try {
michael@0 1284 download.channel.cancel(Cr.NS_BINDING_ABORTED);
michael@0 1285 } catch(e) { }
michael@0 1286 } else {
michael@0 1287 return;
michael@0 1288 }
michael@0 1289
michael@0 1290 this._saveApps().then(() => {
michael@0 1291 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1292 app: {
michael@0 1293 progress: 0,
michael@0 1294 installState: download.previousState,
michael@0 1295 downloading: false
michael@0 1296 },
michael@0 1297 error: error,
michael@0 1298 manifestURL: app.manifestURL,
michael@0 1299 })
michael@0 1300 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1301 eventType: "downloaderror",
michael@0 1302 manifestURL: app.manifestURL
michael@0 1303 });
michael@0 1304 });
michael@0 1305 AppDownloadManager.remove(aManifestURL);
michael@0 1306 },
michael@0 1307
michael@0 1308 startDownload: Task.async(function*(aManifestURL) {
michael@0 1309 debug("startDownload for " + aManifestURL);
michael@0 1310
michael@0 1311 let id = this._appIdForManifestURL(aManifestURL);
michael@0 1312 let app = this.webapps[id];
michael@0 1313 if (!app) {
michael@0 1314 debug("startDownload: No app found for " + aManifestURL);
michael@0 1315 return;
michael@0 1316 }
michael@0 1317
michael@0 1318 if (app.downloading) {
michael@0 1319 debug("app is already downloading. Ignoring.");
michael@0 1320 return;
michael@0 1321 }
michael@0 1322
michael@0 1323 // If the caller is trying to start a download but we have nothing to
michael@0 1324 // download, send an error.
michael@0 1325 if (!app.downloadAvailable) {
michael@0 1326 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1327 error: "NO_DOWNLOAD_AVAILABLE",
michael@0 1328 manifestURL: app.manifestURL
michael@0 1329 });
michael@0 1330 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1331 eventType: "downloaderror",
michael@0 1332 manifestURL: app.manifestURL
michael@0 1333 });
michael@0 1334 return;
michael@0 1335 }
michael@0 1336
michael@0 1337 // First of all, we check if the download is supposed to update an
michael@0 1338 // already installed application.
michael@0 1339 let isUpdate = (app.installState == "installed");
michael@0 1340
michael@0 1341 // An app download would only be triggered for two reasons: an app
michael@0 1342 // update or while retrying to download a previously failed or canceled
michael@0 1343 // instalation.
michael@0 1344 app.retryingDownload = !isUpdate;
michael@0 1345
michael@0 1346 // We need to get the update manifest here, not the webapp manifest.
michael@0 1347 // If this is an update, the update manifest is staged.
michael@0 1348 let file = FileUtils.getFile(DIRECTORY_NAME,
michael@0 1349 ["webapps", id,
michael@0 1350 isUpdate ? "staged-update.webapp"
michael@0 1351 : "update.webapp"],
michael@0 1352 true);
michael@0 1353
michael@0 1354 if (!file.exists()) {
michael@0 1355 // This is a hosted app, let's check if it has an appcache
michael@0 1356 // and download it.
michael@0 1357 let results = yield this._readManifests([{ id: id }]);
michael@0 1358
michael@0 1359 let jsonManifest = results[0].manifest;
michael@0 1360 let manifest = new ManifestHelper(jsonManifest, app.origin);
michael@0 1361
michael@0 1362 if (manifest.appcache_path) {
michael@0 1363 debug("appcache found");
michael@0 1364 this.startOfflineCacheDownload(manifest, app, null, isUpdate);
michael@0 1365 } else {
michael@0 1366 // Hosted app with no appcache, nothing to do, but we fire a
michael@0 1367 // downloaded event.
michael@0 1368 debug("No appcache found, sending 'downloaded' for " + aManifestURL);
michael@0 1369 app.downloadAvailable = false;
michael@0 1370
michael@0 1371 yield this._saveApps();
michael@0 1372
michael@0 1373 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1374 app: app,
michael@0 1375 manifest: jsonManifest,
michael@0 1376 manifestURL: aManifestURL
michael@0 1377 });
michael@0 1378 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1379 eventType: "downloadsuccess",
michael@0 1380 manifestURL: aManifestURL
michael@0 1381 });
michael@0 1382 }
michael@0 1383
michael@0 1384 return;
michael@0 1385 }
michael@0 1386
michael@0 1387 let json = yield AppsUtils.loadJSONAsync(file.path);
michael@0 1388 if (!json) {
michael@0 1389 debug("startDownload: No update manifest found at " + file.path + " " +
michael@0 1390 aManifestURL);
michael@0 1391 return;
michael@0 1392 }
michael@0 1393
michael@0 1394 let manifest = new ManifestHelper(json, app.manifestURL);
michael@0 1395 let [aId, aManifest] = yield this.downloadPackage(manifest, {
michael@0 1396 manifestURL: aManifestURL,
michael@0 1397 origin: app.origin,
michael@0 1398 installOrigin: app.installOrigin,
michael@0 1399 downloadSize: app.downloadSize
michael@0 1400 }, isUpdate);
michael@0 1401
michael@0 1402 // Success! Keep the zip in of TmpD, we'll move it out when
michael@0 1403 // applyDownload() will be called.
michael@0 1404 // Save the manifest in TmpD also
michael@0 1405 let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", aId,
michael@0 1406 "manifest.webapp");
michael@0 1407 yield this._writeFile(manFile, JSON.stringify(aManifest));
michael@0 1408
michael@0 1409 app = this.webapps[aId];
michael@0 1410 // Set state and fire events.
michael@0 1411 app.downloading = false;
michael@0 1412 app.downloadAvailable = false;
michael@0 1413 app.readyToApplyDownload = true;
michael@0 1414 app.updateTime = Date.now();
michael@0 1415
michael@0 1416 yield this._saveApps();
michael@0 1417
michael@0 1418 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1419 app: app,
michael@0 1420 manifestURL: aManifestURL
michael@0 1421 });
michael@0 1422 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1423 eventType: "downloadsuccess",
michael@0 1424 manifestURL: aManifestURL
michael@0 1425 });
michael@0 1426 if (app.installState == "pending") {
michael@0 1427 // We restarted a failed download, apply it automatically.
michael@0 1428 this.applyDownload(aManifestURL);
michael@0 1429 }
michael@0 1430 }),
michael@0 1431
michael@0 1432 applyDownload: function applyDownload(aManifestURL) {
michael@0 1433 debug("applyDownload for " + aManifestURL);
michael@0 1434 let id = this._appIdForManifestURL(aManifestURL);
michael@0 1435 let app = this.webapps[id];
michael@0 1436 if (!app || (app && !app.readyToApplyDownload)) {
michael@0 1437 return;
michael@0 1438 }
michael@0 1439
michael@0 1440 // We need to get the old manifest to unregister web activities.
michael@0 1441 this.getManifestFor(aManifestURL).then((aOldManifest) => {
michael@0 1442 // Move the application.zip and manifest.webapp files out of TmpD
michael@0 1443 let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
michael@0 1444 let manFile = tmpDir.clone();
michael@0 1445 manFile.append("manifest.webapp");
michael@0 1446 let appFile = tmpDir.clone();
michael@0 1447 appFile.append("application.zip");
michael@0 1448
michael@0 1449 let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
michael@0 1450 appFile.moveTo(dir, "application.zip");
michael@0 1451 manFile.moveTo(dir, "manifest.webapp");
michael@0 1452
michael@0 1453 // Move the staged update manifest to a non staged one.
michael@0 1454 let staged = dir.clone();
michael@0 1455 staged.append("staged-update.webapp");
michael@0 1456
michael@0 1457 // If we are applying after a restarted download, we have no
michael@0 1458 // staged update manifest.
michael@0 1459 if (staged.exists()) {
michael@0 1460 staged.moveTo(dir, "update.webapp");
michael@0 1461 }
michael@0 1462
michael@0 1463 try {
michael@0 1464 tmpDir.remove(true);
michael@0 1465 } catch(e) { }
michael@0 1466
michael@0 1467 // Clean up the deprecated manifest cache if needed.
michael@0 1468 if (id in this._manifestCache) {
michael@0 1469 delete this._manifestCache[id];
michael@0 1470 }
michael@0 1471
michael@0 1472 // Flush the zip reader cache to make sure we use the new application.zip
michael@0 1473 // when re-launching the application.
michael@0 1474 let zipFile = dir.clone();
michael@0 1475 zipFile.append("application.zip");
michael@0 1476 Services.obs.notifyObservers(zipFile, "flush-cache-entry", null);
michael@0 1477
michael@0 1478 // Get the manifest, and set properties.
michael@0 1479 this.getManifestFor(aManifestURL).then((aData) => {
michael@0 1480 app.downloading = false;
michael@0 1481 app.downloadAvailable = false;
michael@0 1482 app.downloadSize = 0;
michael@0 1483 app.installState = "installed";
michael@0 1484 app.readyToApplyDownload = false;
michael@0 1485
michael@0 1486 // Update the staged properties.
michael@0 1487 if (app.staged) {
michael@0 1488 for (let prop in app.staged) {
michael@0 1489 app[prop] = app.staged[prop];
michael@0 1490 }
michael@0 1491 delete app.staged;
michael@0 1492 }
michael@0 1493
michael@0 1494 delete app.retryingDownload;
michael@0 1495
michael@0 1496 // Update the asm.js scripts we need to compile.
michael@0 1497 ScriptPreloader.preload(app, aData)
michael@0 1498 .then(() => this._saveApps()).then(() => {
michael@0 1499 // Update the handlers and permissions for this app.
michael@0 1500 this.updateAppHandlers(aOldManifest, aData, app);
michael@0 1501
michael@0 1502 AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => {
michael@0 1503 let appObject = AppsUtils.cloneAppObject(app);
michael@0 1504 appObject.updateManifest = aUpdateManifest;
michael@0 1505 this.notifyUpdateHandlers(appObject, aData, appFile.path);
michael@0 1506 });
michael@0 1507
michael@0 1508 if (supportUseCurrentProfile()) {
michael@0 1509 PermissionsInstaller.installPermissions(
michael@0 1510 { manifest: aData,
michael@0 1511 origin: app.origin,
michael@0 1512 manifestURL: app.manifestURL },
michael@0 1513 true);
michael@0 1514 }
michael@0 1515 this.updateDataStore(this.webapps[id].localId, app.origin,
michael@0 1516 app.manifestURL, aData, app.appStatus);
michael@0 1517 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1518 app: app,
michael@0 1519 manifest: aData,
michael@0 1520 manifestURL: app.manifestURL
michael@0 1521 });
michael@0 1522 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1523 eventType: "downloadapplied",
michael@0 1524 manifestURL: app.manifestURL
michael@0 1525 });
michael@0 1526 });
michael@0 1527 });
michael@0 1528 });
michael@0 1529 },
michael@0 1530
michael@0 1531 startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
michael@0 1532 if (!aManifest.appcache_path) {
michael@0 1533 return;
michael@0 1534 }
michael@0 1535
michael@0 1536 // If the manifest has an appcache_path property, use it to populate the
michael@0 1537 // appcache.
michael@0 1538 let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(),
michael@0 1539 null, null);
michael@0 1540 let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null);
michael@0 1541
michael@0 1542 // We determine the app's 'installState' according to its previous
michael@0 1543 // state. Cancelled downloads should remain as 'pending'. Successfully
michael@0 1544 // installed apps should morph to 'updating'.
michael@0 1545 if (aIsUpdate) {
michael@0 1546 aApp.installState = "updating";
michael@0 1547 }
michael@0 1548
michael@0 1549 // We set the 'downloading' flag and update the apps registry right before
michael@0 1550 // starting the app download/update.
michael@0 1551 aApp.downloading = true;
michael@0 1552 aApp.progress = 0;
michael@0 1553 DOMApplicationRegistry._saveApps().then(() => {
michael@0 1554 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
michael@0 1555 app: {
michael@0 1556 downloading: true,
michael@0 1557 installState: aApp.installState,
michael@0 1558 progress: 0
michael@0 1559 },
michael@0 1560 manifestURL: aApp.manifestURL
michael@0 1561 });
michael@0 1562 let cacheUpdate = updateSvc.scheduleAppUpdate(
michael@0 1563 appcacheURI, docURI, aApp.localId, false, aProfileDir);
michael@0 1564
michael@0 1565 // We save the download details for potential further usage like
michael@0 1566 // cancelling it.
michael@0 1567 let download = {
michael@0 1568 cacheUpdate: cacheUpdate,
michael@0 1569 appId: this._appIdForManifestURL(aApp.manifestURL),
michael@0 1570 previousState: aIsUpdate ? "installed" : "pending"
michael@0 1571 };
michael@0 1572 AppDownloadManager.add(aApp.manifestURL, download);
michael@0 1573
michael@0 1574 cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
michael@0 1575
michael@0 1576 });
michael@0 1577 },
michael@0 1578
michael@0 1579 // Returns the MD5 hash of the manifest.
michael@0 1580 computeManifestHash: function(aManifest) {
michael@0 1581 return AppsUtils.computeHash(JSON.stringify(aManifest));
michael@0 1582 },
michael@0 1583
michael@0 1584 // Updates the redirect mapping, activities and system message handlers.
michael@0 1585 // aOldManifest can be null if we don't have any handler to unregister.
michael@0 1586 updateAppHandlers: function(aOldManifest, aNewManifest, aApp) {
michael@0 1587 debug("updateAppHandlers: old=" + aOldManifest + " new=" + aNewManifest);
michael@0 1588 this.notifyAppsRegistryStart();
michael@0 1589 if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
michael@0 1590 aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects);
michael@0 1591 }
michael@0 1592
michael@0 1593 if (supportSystemMessages()) {
michael@0 1594 if (aOldManifest) {
michael@0 1595 this._unregisterActivities(aOldManifest, aApp);
michael@0 1596 }
michael@0 1597 this._registerSystemMessages(aNewManifest, aApp);
michael@0 1598 this._registerActivities(aNewManifest, aApp, true);
michael@0 1599 this._registerInterAppConnections(aNewManifest, aApp);
michael@0 1600 } else {
michael@0 1601 // Nothing else to do but notifying we're ready.
michael@0 1602 this.notifyAppsRegistryReady();
michael@0 1603 }
michael@0 1604 },
michael@0 1605
michael@0 1606 checkForUpdate: function(aData, aMm) {
michael@0 1607 debug("checkForUpdate for " + aData.manifestURL);
michael@0 1608
michael@0 1609 function sendError(aError) {
michael@0 1610 aData.error = aError;
michael@0 1611 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
michael@0 1612 }
michael@0 1613
michael@0 1614 let id = this._appIdForManifestURL(aData.manifestURL);
michael@0 1615 let app = this.webapps[id];
michael@0 1616
michael@0 1617 // We cannot update an app that does not exists.
michael@0 1618 if (!app) {
michael@0 1619 sendError("NO_SUCH_APP");
michael@0 1620 return;
michael@0 1621 }
michael@0 1622
michael@0 1623 // We cannot update an app that is not fully installed.
michael@0 1624 if (app.installState !== "installed") {
michael@0 1625 sendError("PENDING_APP_NOT_UPDATABLE");
michael@0 1626 return;
michael@0 1627 }
michael@0 1628
michael@0 1629 // We may be able to remove this when Bug 839071 is fixed.
michael@0 1630 if (app.downloading) {
michael@0 1631 sendError("APP_IS_DOWNLOADING");
michael@0 1632 return;
michael@0 1633 }
michael@0 1634
michael@0 1635 // If the app is packaged and its manifestURL has an app:// scheme,
michael@0 1636 // then we can't have an update.
michael@0 1637 if (app.origin.startsWith("app://") &&
michael@0 1638 app.manifestURL.startsWith("app://")) {
michael@0 1639 aData.error = "NOT_UPDATABLE";
michael@0 1640 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
michael@0 1641 return;
michael@0 1642 }
michael@0 1643
michael@0 1644 // For non-removable hosted apps that lives in the core apps dir we
michael@0 1645 // only check the appcache because we can't modify the manifest even
michael@0 1646 // if it has changed.
michael@0 1647 let onlyCheckAppCache = false;
michael@0 1648
michael@0 1649 #ifdef MOZ_WIDGET_GONK
michael@0 1650 let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
michael@0 1651 onlyCheckAppCache = (app.basePath == appDir.path);
michael@0 1652 #endif
michael@0 1653
michael@0 1654 if (onlyCheckAppCache) {
michael@0 1655 // Bail out for packaged apps.
michael@0 1656 if (app.origin.startsWith("app://")) {
michael@0 1657 aData.error = "NOT_UPDATABLE";
michael@0 1658 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
michael@0 1659 return;
michael@0 1660 }
michael@0 1661
michael@0 1662 // We need the manifest to check if we have an appcache.
michael@0 1663 this._readManifests([{ id: id }]).then((aResult) => {
michael@0 1664 let manifest = aResult[0].manifest;
michael@0 1665 if (!manifest.appcache_path) {
michael@0 1666 aData.error = "NOT_UPDATABLE";
michael@0 1667 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
michael@0 1668 return;
michael@0 1669 }
michael@0 1670
michael@0 1671 debug("Checking only appcache for " + aData.manifestURL);
michael@0 1672 // Check if the appcache is updatable, and send "downloadavailable" or
michael@0 1673 // "downloadapplied".
michael@0 1674 let updateObserver = {
michael@0 1675 observe: function(aSubject, aTopic, aObsData) {
michael@0 1676 debug("onlyCheckAppCache updateSvc.checkForUpdate return for " +
michael@0 1677 app.manifestURL + " - event is " + aTopic);
michael@0 1678 if (aTopic == "offline-cache-update-available") {
michael@0 1679 app.downloadAvailable = true;
michael@0 1680 this._saveApps().then(() => {
michael@0 1681 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1682 app: app,
michael@0 1683 manifestURL: app.manifestURL
michael@0 1684 });
michael@0 1685 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1686 eventType: "downloadavailable",
michael@0 1687 manifestURL: app.manifestURL,
michael@0 1688 requestID: aData.requestID
michael@0 1689 });
michael@0 1690 });
michael@0 1691 } else {
michael@0 1692 aData.error = "NOT_UPDATABLE";
michael@0 1693 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
michael@0 1694 }
michael@0 1695 }
michael@0 1696 };
michael@0 1697 let helper = new ManifestHelper(manifest, aData.manifestURL);
michael@0 1698 debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " +
michael@0 1699 helper.fullAppcachePath());
michael@0 1700 updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null),
michael@0 1701 app.localId, false, updateObserver);
michael@0 1702 });
michael@0 1703 return;
michael@0 1704 }
michael@0 1705
michael@0 1706 // On xhr load request event
michael@0 1707 function onload(xhr, oldManifest) {
michael@0 1708 debug("Got http status=" + xhr.status + " for " + aData.manifestURL);
michael@0 1709 let oldHash = app.manifestHash;
michael@0 1710 let isPackage = app.origin.startsWith("app://");
michael@0 1711
michael@0 1712 if (xhr.status == 200) {
michael@0 1713 let manifest = xhr.response;
michael@0 1714 if (manifest == null) {
michael@0 1715 sendError("MANIFEST_PARSE_ERROR");
michael@0 1716 return;
michael@0 1717 }
michael@0 1718
michael@0 1719 if (!AppsUtils.checkManifest(manifest, app)) {
michael@0 1720 sendError("INVALID_MANIFEST");
michael@0 1721 return;
michael@0 1722 } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
michael@0 1723 sendError("INSTALL_FROM_DENIED");
michael@0 1724 return;
michael@0 1725 } else {
michael@0 1726 AppsUtils.ensureSameAppName(oldManifest, manifest, app);
michael@0 1727
michael@0 1728 let hash = this.computeManifestHash(manifest);
michael@0 1729 debug("Manifest hash = " + hash);
michael@0 1730 if (isPackage) {
michael@0 1731 if (!app.staged) {
michael@0 1732 app.staged = { };
michael@0 1733 }
michael@0 1734 app.staged.manifestHash = hash;
michael@0 1735 app.staged.etag = xhr.getResponseHeader("Etag");
michael@0 1736 } else {
michael@0 1737 app.manifestHash = hash;
michael@0 1738 app.etag = xhr.getResponseHeader("Etag");
michael@0 1739 }
michael@0 1740
michael@0 1741 app.lastCheckedUpdate = Date.now();
michael@0 1742 if (isPackage) {
michael@0 1743 if (oldHash != hash) {
michael@0 1744 this.updatePackagedApp(aData, id, app, manifest);
michael@0 1745 } else {
michael@0 1746 this._saveApps().then(() => {
michael@0 1747 // Like if we got a 304, just send a 'downloadapplied'
michael@0 1748 // or downloadavailable event.
michael@0 1749 let eventType = app.downloadAvailable ? "downloadavailable"
michael@0 1750 : "downloadapplied";
michael@0 1751 aMm.sendAsyncMessage("Webapps:UpdateState", {
michael@0 1752 app: app,
michael@0 1753 manifestURL: app.manifestURL
michael@0 1754 });
michael@0 1755 aMm.sendAsyncMessage("Webapps:FireEvent", {
michael@0 1756 eventType: eventType,
michael@0 1757 manifestURL: app.manifestURL,
michael@0 1758 requestID: aData.requestID
michael@0 1759 });
michael@0 1760 });
michael@0 1761 }
michael@0 1762 } else {
michael@0 1763 // Update only the appcache if the manifest has not changed
michael@0 1764 // based on the hash value.
michael@0 1765 this.updateHostedApp(aData, id, app, oldManifest,
michael@0 1766 oldHash == hash ? null : manifest);
michael@0 1767 }
michael@0 1768 }
michael@0 1769 } else if (xhr.status == 304) {
michael@0 1770 // The manifest has not changed.
michael@0 1771 if (isPackage) {
michael@0 1772 app.lastCheckedUpdate = Date.now();
michael@0 1773 this._saveApps().then(() => {
michael@0 1774 // If the app is a packaged app, we just send a 'downloadapplied'
michael@0 1775 // or downloadavailable event.
michael@0 1776 let eventType = app.downloadAvailable ? "downloadavailable"
michael@0 1777 : "downloadapplied";
michael@0 1778 aMm.sendAsyncMessage("Webapps:UpdateState", {
michael@0 1779 app: app,
michael@0 1780 manifestURL: app.manifestURL
michael@0 1781 });
michael@0 1782 aMm.sendAsyncMessage("Webapps:FireEvent", {
michael@0 1783 eventType: eventType,
michael@0 1784 manifestURL: app.manifestURL,
michael@0 1785 requestID: aData.requestID
michael@0 1786 });
michael@0 1787 });
michael@0 1788 } else {
michael@0 1789 // For hosted apps, even if the manifest has not changed, we check
michael@0 1790 // for offline cache updates.
michael@0 1791 this.updateHostedApp(aData, id, app, oldManifest, null);
michael@0 1792 }
michael@0 1793 } else {
michael@0 1794 sendError("MANIFEST_URL_ERROR");
michael@0 1795 }
michael@0 1796 }
michael@0 1797
michael@0 1798 // Try to download a new manifest.
michael@0 1799 function doRequest(oldManifest, headers) {
michael@0 1800 headers = headers || [];
michael@0 1801 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 1802 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 1803 xhr.open("GET", aData.manifestURL, true);
michael@0 1804 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 1805 headers.forEach(function(aHeader) {
michael@0 1806 debug("Adding header: " + aHeader.name + ": " + aHeader.value);
michael@0 1807 xhr.setRequestHeader(aHeader.name, aHeader.value);
michael@0 1808 });
michael@0 1809 xhr.responseType = "json";
michael@0 1810 if (app.etag) {
michael@0 1811 debug("adding manifest etag:" + app.etag);
michael@0 1812 xhr.setRequestHeader("If-None-Match", app.etag);
michael@0 1813 }
michael@0 1814 xhr.channel.notificationCallbacks =
michael@0 1815 this.createLoadContext(app.installerAppId, app.installerIsBrowser);
michael@0 1816
michael@0 1817 xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
michael@0 1818 xhr.addEventListener("error", (function() {
michael@0 1819 sendError("NETWORK_ERROR");
michael@0 1820 }).bind(this), false);
michael@0 1821
michael@0 1822 debug("Checking manifest at " + aData.manifestURL);
michael@0 1823 xhr.send(null);
michael@0 1824 }
michael@0 1825
michael@0 1826 // Read the current app manifest file
michael@0 1827 this._readManifests([{ id: id }]).then((aResult) => {
michael@0 1828 let extraHeaders = [];
michael@0 1829 #ifdef MOZ_WIDGET_GONK
michael@0 1830 let pingManifestURL;
michael@0 1831 try {
michael@0 1832 pingManifestURL = Services.prefs.getCharPref("ping.manifestURL");
michael@0 1833 } catch(e) { }
michael@0 1834
michael@0 1835 if (pingManifestURL && pingManifestURL == aData.manifestURL) {
michael@0 1836 // Get the device info.
michael@0 1837 let device = libcutils.property_get("ro.product.model");
michael@0 1838 extraHeaders.push({ name: "X-MOZ-B2G-DEVICE",
michael@0 1839 value: device || "unknown" });
michael@0 1840 }
michael@0 1841 #endif
michael@0 1842 doRequest.call(this, aResult[0].manifest, extraHeaders);
michael@0 1843 });
michael@0 1844 },
michael@0 1845
michael@0 1846 // Creates a nsILoadContext object with a given appId and isBrowser flag.
michael@0 1847 createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
michael@0 1848 return {
michael@0 1849 associatedWindow: null,
michael@0 1850 topWindow : null,
michael@0 1851 appId: aAppId,
michael@0 1852 isInBrowserElement: aIsBrowser,
michael@0 1853 usePrivateBrowsing: false,
michael@0 1854 isContent: false,
michael@0 1855
michael@0 1856 isAppOfType: function(appType) {
michael@0 1857 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1858 },
michael@0 1859
michael@0 1860 QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
michael@0 1861 Ci.nsIInterfaceRequestor,
michael@0 1862 Ci.nsISupports]),
michael@0 1863 getInterface: function(iid) {
michael@0 1864 if (iid.equals(Ci.nsILoadContext))
michael@0 1865 return this;
michael@0 1866 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 1867 }
michael@0 1868 }
michael@0 1869 },
michael@0 1870
michael@0 1871 updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
michael@0 1872 debug("updatePackagedApp");
michael@0 1873
michael@0 1874 // Store the new update manifest.
michael@0 1875 let dir = this._getAppDir(aId).path;
michael@0 1876 let manFile = OS.Path.join(dir, "staged-update.webapp");
michael@0 1877 yield this._writeFile(manFile, JSON.stringify(aNewManifest));
michael@0 1878
michael@0 1879 let manifest = new ManifestHelper(aNewManifest, aApp.manifestURL);
michael@0 1880 // A package is available: set downloadAvailable to fire the matching
michael@0 1881 // event.
michael@0 1882 aApp.downloadAvailable = true;
michael@0 1883 aApp.downloadSize = manifest.size;
michael@0 1884 aApp.updateManifest = aNewManifest;
michael@0 1885 yield this._saveApps();
michael@0 1886
michael@0 1887 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1888 app: aApp,
michael@0 1889 manifestURL: aApp.manifestURL
michael@0 1890 });
michael@0 1891 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1892 eventType: "downloadavailable",
michael@0 1893 manifestURL: aApp.manifestURL,
michael@0 1894 requestID: aData.requestID
michael@0 1895 });
michael@0 1896 }),
michael@0 1897
michael@0 1898 // A hosted app is updated if the app manifest or the appcache needs
michael@0 1899 // updating. Even if the app manifest has not changed, we still check
michael@0 1900 // for changes in the app cache.
michael@0 1901 // 'aNewManifest' would contain the updated app manifest if
michael@0 1902 // it has actually been updated, while 'aOldManifest' contains the
michael@0 1903 // stored app manifest.
michael@0 1904 updateHostedApp: Task.async(function*(aData, aId, aApp, aOldManifest, aNewManifest) {
michael@0 1905 debug("updateHostedApp " + aData.manifestURL);
michael@0 1906
michael@0 1907 // Clean up the deprecated manifest cache if needed.
michael@0 1908 if (aId in this._manifestCache) {
michael@0 1909 delete this._manifestCache[aId];
michael@0 1910 }
michael@0 1911
michael@0 1912 aApp.manifest = aNewManifest || aOldManifest;
michael@0 1913
michael@0 1914 let manifest;
michael@0 1915 if (aNewManifest) {
michael@0 1916 this.updateAppHandlers(aOldManifest, aNewManifest, aApp);
michael@0 1917
michael@0 1918 this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest);
michael@0 1919
michael@0 1920 // Store the new manifest.
michael@0 1921 let dir = this._getAppDir(aId).path;
michael@0 1922 let manFile = OS.Path.join(dir, "manifest.webapp");
michael@0 1923 yield this._writeFile(manFile, JSON.stringify(aNewManifest));
michael@0 1924
michael@0 1925 manifest = new ManifestHelper(aNewManifest, aApp.origin);
michael@0 1926
michael@0 1927 if (supportUseCurrentProfile()) {
michael@0 1928 // Update the permissions for this app.
michael@0 1929 PermissionsInstaller.installPermissions({
michael@0 1930 manifest: aApp.manifest,
michael@0 1931 origin: aApp.origin,
michael@0 1932 manifestURL: aData.manifestURL
michael@0 1933 }, true);
michael@0 1934 }
michael@0 1935
michael@0 1936 this.updateDataStore(this.webapps[aId].localId, aApp.origin,
michael@0 1937 aApp.manifestURL, aApp.manifest, aApp.appStatus);
michael@0 1938
michael@0 1939 aApp.name = manifest.name;
michael@0 1940 aApp.csp = manifest.csp || "";
michael@0 1941 aApp.role = manifest.role || "";
michael@0 1942 aApp.updateTime = Date.now();
michael@0 1943 } else {
michael@0 1944 manifest = new ManifestHelper(aOldManifest, aApp.origin);
michael@0 1945 }
michael@0 1946
michael@0 1947 // Update the registry.
michael@0 1948 this.webapps[aId] = aApp;
michael@0 1949 yield this._saveApps();
michael@0 1950
michael@0 1951 if (!manifest.appcache_path) {
michael@0 1952 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1953 app: aApp,
michael@0 1954 manifest: aApp.manifest,
michael@0 1955 manifestURL: aApp.manifestURL
michael@0 1956 });
michael@0 1957 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1958 eventType: "downloadapplied",
michael@0 1959 manifestURL: aApp.manifestURL,
michael@0 1960 requestID: aData.requestID
michael@0 1961 });
michael@0 1962 } else {
michael@0 1963 // Check if the appcache is updatable, and send "downloadavailable" or
michael@0 1964 // "downloadapplied".
michael@0 1965 debug("updateHostedApp: updateSvc.checkForUpdate for " +
michael@0 1966 manifest.fullAppcachePath());
michael@0 1967
michael@0 1968 let updateDeferred = Promise.defer();
michael@0 1969
michael@0 1970 updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null),
michael@0 1971 aApp.localId, false,
michael@0 1972 (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic));
michael@0 1973
michael@0 1974 let topic = yield updateDeferred.promise;
michael@0 1975
michael@0 1976 debug("updateHostedApp: updateSvc.checkForUpdate return for " +
michael@0 1977 aApp.manifestURL + " - event is " + topic);
michael@0 1978
michael@0 1979 let eventType =
michael@0 1980 topic == "offline-cache-update-available" ? "downloadavailable"
michael@0 1981 : "downloadapplied";
michael@0 1982
michael@0 1983 aApp.downloadAvailable = (eventType == "downloadavailable");
michael@0 1984 yield this._saveApps();
michael@0 1985
michael@0 1986 this.broadcastMessage("Webapps:UpdateState", {
michael@0 1987 app: aApp,
michael@0 1988 manifest: aApp.manifest,
michael@0 1989 manifestURL: aApp.manifestURL
michael@0 1990 });
michael@0 1991 this.broadcastMessage("Webapps:FireEvent", {
michael@0 1992 eventType: eventType,
michael@0 1993 manifestURL: aApp.manifestURL,
michael@0 1994 requestID: aData.requestID
michael@0 1995 });
michael@0 1996 }
michael@0 1997
michael@0 1998 delete aApp.manifest;
michael@0 1999 }),
michael@0 2000
michael@0 2001 // Downloads the manifest and run checks, then eventually triggers the
michael@0 2002 // installation UI.
michael@0 2003 doInstall: function doInstall(aData, aMm) {
michael@0 2004 let app = aData.app;
michael@0 2005
michael@0 2006 let sendError = function sendError(aError) {
michael@0 2007 aData.error = aError;
michael@0 2008 aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
michael@0 2009 Cu.reportError("Error installing app from: " + app.installOrigin +
michael@0 2010 ": " + aError);
michael@0 2011 }.bind(this);
michael@0 2012
michael@0 2013 if (app.receipts.length > 0) {
michael@0 2014 for (let receipt of app.receipts) {
michael@0 2015 let error = this.isReceipt(receipt);
michael@0 2016 if (error) {
michael@0 2017 sendError(error);
michael@0 2018 return;
michael@0 2019 }
michael@0 2020 }
michael@0 2021 }
michael@0 2022
michael@0 2023 // Hosted apps can't be trusted or certified, so just check that the
michael@0 2024 // manifest doesn't ask for those.
michael@0 2025 function checkAppStatus(aManifest) {
michael@0 2026 let manifestStatus = aManifest.type || "web";
michael@0 2027 return manifestStatus === "web";
michael@0 2028 }
michael@0 2029
michael@0 2030 let checkManifest = (function() {
michael@0 2031 if (!app.manifest) {
michael@0 2032 sendError("MANIFEST_PARSE_ERROR");
michael@0 2033 return false;
michael@0 2034 }
michael@0 2035
michael@0 2036 // Disallow multiple hosted apps installations from the same origin for now.
michael@0 2037 // We will remove this code after multiple apps per origin are supported (bug 778277).
michael@0 2038 // This will also disallow reinstalls from the same origin for now.
michael@0 2039 for (let id in this.webapps) {
michael@0 2040 if (this.webapps[id].origin == app.origin &&
michael@0 2041 !this.webapps[id].packageHash &&
michael@0 2042 this._isLaunchable(this.webapps[id])) {
michael@0 2043 sendError("MULTIPLE_APPS_PER_ORIGIN_FORBIDDEN");
michael@0 2044 return false;
michael@0 2045 }
michael@0 2046 }
michael@0 2047
michael@0 2048 if (!AppsUtils.checkManifest(app.manifest, app)) {
michael@0 2049 sendError("INVALID_MANIFEST");
michael@0 2050 return false;
michael@0 2051 }
michael@0 2052
michael@0 2053 if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) {
michael@0 2054 sendError("INSTALL_FROM_DENIED");
michael@0 2055 return false;
michael@0 2056 }
michael@0 2057
michael@0 2058 if (!checkAppStatus(app.manifest)) {
michael@0 2059 sendError("INVALID_SECURITY_LEVEL");
michael@0 2060 return false;
michael@0 2061 }
michael@0 2062
michael@0 2063 return true;
michael@0 2064 }).bind(this);
michael@0 2065
michael@0 2066 let installApp = (function() {
michael@0 2067 app.manifestHash = this.computeManifestHash(app.manifest);
michael@0 2068 // We allow bypassing the install confirmation process to facilitate
michael@0 2069 // automation.
michael@0 2070 let prefName = "dom.mozApps.auto_confirm_install";
michael@0 2071 if (Services.prefs.prefHasUserValue(prefName) &&
michael@0 2072 Services.prefs.getBoolPref(prefName)) {
michael@0 2073 this.confirmInstall(aData);
michael@0 2074 } else {
michael@0 2075 Services.obs.notifyObservers(aMm, "webapps-ask-install",
michael@0 2076 JSON.stringify(aData));
michael@0 2077 }
michael@0 2078 }).bind(this);
michael@0 2079
michael@0 2080 // We may already have the manifest (e.g. AutoInstall),
michael@0 2081 // in which case we don't need to load it.
michael@0 2082 if (app.manifest) {
michael@0 2083 if (checkManifest()) {
michael@0 2084 installApp();
michael@0 2085 }
michael@0 2086 return;
michael@0 2087 }
michael@0 2088
michael@0 2089 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 2090 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 2091 xhr.open("GET", app.manifestURL, true);
michael@0 2092 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 2093 xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
michael@0 2094 aData.isBrowser);
michael@0 2095 xhr.responseType = "json";
michael@0 2096
michael@0 2097 xhr.addEventListener("load", (function() {
michael@0 2098 if (xhr.status == 200) {
michael@0 2099 if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
michael@0 2100 xhr.getResponseHeader("content-type"))) {
michael@0 2101 sendError("INVALID_MANIFEST");
michael@0 2102 return;
michael@0 2103 }
michael@0 2104
michael@0 2105 app.manifest = xhr.response;
michael@0 2106 if (checkManifest()) {
michael@0 2107 app.etag = xhr.getResponseHeader("Etag");
michael@0 2108 installApp();
michael@0 2109 }
michael@0 2110 } else {
michael@0 2111 sendError("MANIFEST_URL_ERROR");
michael@0 2112 }
michael@0 2113 }).bind(this), false);
michael@0 2114
michael@0 2115 xhr.addEventListener("error", (function() {
michael@0 2116 sendError("NETWORK_ERROR");
michael@0 2117 }).bind(this), false);
michael@0 2118
michael@0 2119 xhr.send(null);
michael@0 2120 },
michael@0 2121
michael@0 2122 doInstallPackage: function doInstallPackage(aData, aMm) {
michael@0 2123 let app = aData.app;
michael@0 2124
michael@0 2125 let sendError = function sendError(aError) {
michael@0 2126 aData.error = aError;
michael@0 2127 aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
michael@0 2128 Cu.reportError("Error installing packaged app from: " +
michael@0 2129 app.installOrigin + ": " + aError);
michael@0 2130 }.bind(this);
michael@0 2131
michael@0 2132 if (app.receipts.length > 0) {
michael@0 2133 for (let receipt of app.receipts) {
michael@0 2134 let error = this.isReceipt(receipt);
michael@0 2135 if (error) {
michael@0 2136 sendError(error);
michael@0 2137 return;
michael@0 2138 }
michael@0 2139 }
michael@0 2140 }
michael@0 2141
michael@0 2142 let checkUpdateManifest = (function() {
michael@0 2143 let manifest = app.updateManifest;
michael@0 2144
michael@0 2145 // Disallow reinstalls from the same manifest URL for now.
michael@0 2146 let id = this._appIdForManifestURL(app.manifestURL);
michael@0 2147 if (id !== null && this._isLaunchable(this.webapps[id])) {
michael@0 2148 sendError("REINSTALL_FORBIDDEN");
michael@0 2149 return false;
michael@0 2150 }
michael@0 2151
michael@0 2152 if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) {
michael@0 2153 sendError("INVALID_MANIFEST");
michael@0 2154 return false;
michael@0 2155 }
michael@0 2156
michael@0 2157 if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
michael@0 2158 sendError("INSTALL_FROM_DENIED");
michael@0 2159 return false;
michael@0 2160 }
michael@0 2161
michael@0 2162 return true;
michael@0 2163 }).bind(this);
michael@0 2164
michael@0 2165 let installApp = (function() {
michael@0 2166 app.manifestHash = this.computeManifestHash(app.updateManifest);
michael@0 2167
michael@0 2168 // We allow bypassing the install confirmation process to facilitate
michael@0 2169 // automation.
michael@0 2170 let prefName = "dom.mozApps.auto_confirm_install";
michael@0 2171 if (Services.prefs.prefHasUserValue(prefName) &&
michael@0 2172 Services.prefs.getBoolPref(prefName)) {
michael@0 2173 this.confirmInstall(aData);
michael@0 2174 } else {
michael@0 2175 Services.obs.notifyObservers(aMm, "webapps-ask-install",
michael@0 2176 JSON.stringify(aData));
michael@0 2177 }
michael@0 2178 }).bind(this);
michael@0 2179
michael@0 2180 // We may already have the manifest (e.g. AutoInstall),
michael@0 2181 // in which case we don't need to load it.
michael@0 2182 if (app.updateManifest) {
michael@0 2183 if (checkUpdateManifest()) {
michael@0 2184 installApp();
michael@0 2185 }
michael@0 2186 return;
michael@0 2187 }
michael@0 2188
michael@0 2189 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 2190 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 2191 xhr.open("GET", app.manifestURL, true);
michael@0 2192 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 2193 xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
michael@0 2194 aData.isBrowser);
michael@0 2195 xhr.responseType = "json";
michael@0 2196
michael@0 2197 xhr.addEventListener("load", (function() {
michael@0 2198 if (xhr.status == 200) {
michael@0 2199 if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
michael@0 2200 xhr.getResponseHeader("content-type"))) {
michael@0 2201 sendError("INVALID_MANIFEST");
michael@0 2202 return;
michael@0 2203 }
michael@0 2204
michael@0 2205 app.updateManifest = xhr.response;
michael@0 2206 if (!app.updateManifest) {
michael@0 2207 sendError("MANIFEST_PARSE_ERROR");
michael@0 2208 return;
michael@0 2209 }
michael@0 2210 if (checkUpdateManifest()) {
michael@0 2211 app.etag = xhr.getResponseHeader("Etag");
michael@0 2212 debug("at install package got app etag=" + app.etag);
michael@0 2213 installApp();
michael@0 2214 }
michael@0 2215 }
michael@0 2216 else {
michael@0 2217 sendError("MANIFEST_URL_ERROR");
michael@0 2218 }
michael@0 2219 }).bind(this), false);
michael@0 2220
michael@0 2221 xhr.addEventListener("error", (function() {
michael@0 2222 sendError("NETWORK_ERROR");
michael@0 2223 }).bind(this), false);
michael@0 2224
michael@0 2225 xhr.send(null);
michael@0 2226 },
michael@0 2227
michael@0 2228 denyInstall: function(aData) {
michael@0 2229 let packageId = aData.app.packageId;
michael@0 2230 if (packageId) {
michael@0 2231 let dir = FileUtils.getDir("TmpD", ["webapps", packageId],
michael@0 2232 true, true);
michael@0 2233 try {
michael@0 2234 dir.remove(true);
michael@0 2235 } catch(e) {
michael@0 2236 }
michael@0 2237 }
michael@0 2238 aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
michael@0 2239 },
michael@0 2240
michael@0 2241 // This function is called after we called the onsuccess callback on the
michael@0 2242 // content side. This let the webpage the opportunity to set event handlers
michael@0 2243 // on the app before we start firing progress events.
michael@0 2244 queuedDownload: {},
michael@0 2245 queuedPackageDownload: {},
michael@0 2246
michael@0 2247 onInstallSuccessAck: function onInstallSuccessAck(aManifestURL,
michael@0 2248 aDontNeedNetwork) {
michael@0 2249 // If we are offline, register to run when we'll be online.
michael@0 2250 if ((Services.io.offline) && !aDontNeedNetwork) {
michael@0 2251 let onlineWrapper = {
michael@0 2252 observe: function(aSubject, aTopic, aData) {
michael@0 2253 Services.obs.removeObserver(onlineWrapper,
michael@0 2254 "network:offline-status-changed");
michael@0 2255 DOMApplicationRegistry.onInstallSuccessAck(aManifestURL);
michael@0 2256 }
michael@0 2257 };
michael@0 2258 Services.obs.addObserver(onlineWrapper,
michael@0 2259 "network:offline-status-changed", false);
michael@0 2260 return;
michael@0 2261 }
michael@0 2262
michael@0 2263 let cacheDownload = this.queuedDownload[aManifestURL];
michael@0 2264 if (cacheDownload) {
michael@0 2265 this.startOfflineCacheDownload(cacheDownload.manifest,
michael@0 2266 cacheDownload.app,
michael@0 2267 cacheDownload.profileDir);
michael@0 2268 delete this.queuedDownload[aManifestURL];
michael@0 2269
michael@0 2270 return;
michael@0 2271 }
michael@0 2272
michael@0 2273 let packageDownload = this.queuedPackageDownload[aManifestURL];
michael@0 2274 if (packageDownload) {
michael@0 2275 let manifest = packageDownload.manifest;
michael@0 2276 let newApp = packageDownload.app;
michael@0 2277 let installSuccessCallback = packageDownload.callback;
michael@0 2278
michael@0 2279 delete this.queuedPackageDownload[aManifestURL];
michael@0 2280
michael@0 2281 this.downloadPackage(manifest, newApp, false).then(
michael@0 2282 this._onDownloadPackage.bind(this, newApp, installSuccessCallback)
michael@0 2283 );
michael@0 2284 }
michael@0 2285 },
michael@0 2286
michael@0 2287 _setupApp: function(aData, aId) {
michael@0 2288 let app = aData.app;
michael@0 2289
michael@0 2290 // app can be uninstalled
michael@0 2291 app.removable = true;
michael@0 2292
michael@0 2293 if (aData.isPackage) {
michael@0 2294 // Override the origin with the correct id.
michael@0 2295 app.origin = "app://" + aId;
michael@0 2296 }
michael@0 2297
michael@0 2298 app.id = aId;
michael@0 2299 app.installTime = Date.now();
michael@0 2300 app.lastUpdateCheck = Date.now();
michael@0 2301
michael@0 2302 return app;
michael@0 2303 },
michael@0 2304
michael@0 2305 _cloneApp: function(aData, aNewApp, aManifest, aId, aLocalId) {
michael@0 2306 let appObject = AppsUtils.cloneAppObject(aNewApp);
michael@0 2307 appObject.appStatus =
michael@0 2308 aNewApp.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
michael@0 2309
michael@0 2310 if (aManifest.appcache_path) {
michael@0 2311 appObject.installState = "pending";
michael@0 2312 appObject.downloadAvailable = true;
michael@0 2313 appObject.downloading = true;
michael@0 2314 appObject.downloadSize = 0;
michael@0 2315 appObject.readyToApplyDownload = false;
michael@0 2316 } else if (aManifest.package_path) {
michael@0 2317 appObject.installState = "pending";
michael@0 2318 appObject.downloadAvailable = true;
michael@0 2319 appObject.downloading = true;
michael@0 2320 appObject.downloadSize = aManifest.size;
michael@0 2321 appObject.readyToApplyDownload = false;
michael@0 2322 } else {
michael@0 2323 appObject.installState = "installed";
michael@0 2324 appObject.downloadAvailable = false;
michael@0 2325 appObject.downloading = false;
michael@0 2326 appObject.readyToApplyDownload = false;
michael@0 2327 }
michael@0 2328
michael@0 2329 appObject.localId = aLocalId;
michael@0 2330 appObject.basePath = OS.Path.dirname(this.appsFile);
michael@0 2331 appObject.name = aManifest.name;
michael@0 2332 appObject.csp = aManifest.csp || "";
michael@0 2333 appObject.role = aManifest.role || "";
michael@0 2334 appObject.installerAppId = aData.appId;
michael@0 2335 appObject.installerIsBrowser = aData.isBrowser;
michael@0 2336
michael@0 2337 return appObject;
michael@0 2338 },
michael@0 2339
michael@0 2340 _writeManifestFile: function(aId, aIsPackage, aJsonManifest) {
michael@0 2341 debug("_writeManifestFile");
michael@0 2342
michael@0 2343 // For packaged apps, keep the update manifest distinct from the app manifest.
michael@0 2344 let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp";
michael@0 2345
michael@0 2346 let dir = this._getAppDir(aId).path;
michael@0 2347 let manFile = OS.Path.join(dir, manifestName);
michael@0 2348 this._writeFile(manFile, JSON.stringify(aJsonManifest));
michael@0 2349 },
michael@0 2350
michael@0 2351 // Add an app that is already installed to the registry.
michael@0 2352 addInstalledApp: Task.async(function*(aApp, aManifest, aUpdateManifest) {
michael@0 2353 if (this.getAppLocalIdByManifestURL(aApp.manifestURL) !=
michael@0 2354 Ci.nsIScriptSecurityManager.NO_APP_ID) {
michael@0 2355 return;
michael@0 2356 }
michael@0 2357
michael@0 2358 let app = AppsUtils.cloneAppObject(aApp);
michael@0 2359
michael@0 2360 if (!AppsUtils.checkManifest(aManifest, app) ||
michael@0 2361 (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) {
michael@0 2362 return;
michael@0 2363 }
michael@0 2364
michael@0 2365 app.name = aManifest.name;
michael@0 2366
michael@0 2367 app.csp = aManifest.csp || "";
michael@0 2368
michael@0 2369 app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
michael@0 2370
michael@0 2371 app.removable = true;
michael@0 2372
michael@0 2373 // Reuse the app ID if the scheme is "app".
michael@0 2374 let uri = Services.io.newURI(app.origin, null, null);
michael@0 2375 if (uri.scheme == "app") {
michael@0 2376 app.id = uri.host;
michael@0 2377 } else {
michael@0 2378 app.id = this.makeAppId();
michael@0 2379 }
michael@0 2380
michael@0 2381 app.localId = this._nextLocalId();
michael@0 2382
michael@0 2383 app.basePath = OS.Path.dirname(this.appsFile);
michael@0 2384
michael@0 2385 app.progress = 0.0;
michael@0 2386 app.installState = "installed";
michael@0 2387 app.downloadAvailable = false;
michael@0 2388 app.downloading = false;
michael@0 2389 app.readyToApplyDownload = false;
michael@0 2390
michael@0 2391 if (aUpdateManifest && aUpdateManifest.size) {
michael@0 2392 app.downloadSize = aUpdateManifest.size;
michael@0 2393 }
michael@0 2394
michael@0 2395 app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest ||
michael@0 2396 aManifest));
michael@0 2397
michael@0 2398 let zipFile = WebappOSUtils.getPackagePath(app);
michael@0 2399 app.packageHash = yield this._computeFileHash(zipFile);
michael@0 2400
michael@0 2401 app.role = aManifest.role || "";
michael@0 2402
michael@0 2403 app.redirects = this.sanitizeRedirects(aManifest.redirects);
michael@0 2404
michael@0 2405 this.webapps[app.id] = app;
michael@0 2406
michael@0 2407 // Store the manifest in the manifest cache, so we don't need to re-read it
michael@0 2408 this._manifestCache[app.id] = app.manifest;
michael@0 2409
michael@0 2410 // Store the manifest and the updateManifest.
michael@0 2411 this._writeManifestFile(app.id, false, aManifest);
michael@0 2412 if (aUpdateManifest) {
michael@0 2413 this._writeManifestFile(app.id, true, aUpdateManifest);
michael@0 2414 }
michael@0 2415
michael@0 2416 this._saveApps().then(() => {
michael@0 2417 this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app });
michael@0 2418 });
michael@0 2419 }),
michael@0 2420
michael@0 2421 confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) {
michael@0 2422 debug("confirmInstall");
michael@0 2423
michael@0 2424 let origin = Services.io.newURI(aData.app.origin, null, null);
michael@0 2425 let id = this._appIdForManifestURL(aData.app.manifestURL);
michael@0 2426 let manifestURL = origin.resolve(aData.app.manifestURL);
michael@0 2427 let localId = this.getAppLocalIdByManifestURL(manifestURL);
michael@0 2428
michael@0 2429 let isReinstall = false;
michael@0 2430
michael@0 2431 // Installing an application again is considered as an update.
michael@0 2432 if (id) {
michael@0 2433 isReinstall = true;
michael@0 2434 let dir = this._getAppDir(id);
michael@0 2435 try {
michael@0 2436 dir.remove(true);
michael@0 2437 } catch(e) { }
michael@0 2438 } else {
michael@0 2439 id = this.makeAppId();
michael@0 2440 localId = this._nextLocalId();
michael@0 2441 }
michael@0 2442
michael@0 2443 let app = this._setupApp(aData, id);
michael@0 2444
michael@0 2445 let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
michael@0 2446 this._writeManifestFile(id, aData.isPackage, jsonManifest);
michael@0 2447
michael@0 2448 debug("app.origin: " + app.origin);
michael@0 2449 let manifest = new ManifestHelper(jsonManifest, app.origin);
michael@0 2450
michael@0 2451 let appObject = this._cloneApp(aData, app, manifest, id, localId);
michael@0 2452
michael@0 2453 this.webapps[id] = appObject;
michael@0 2454
michael@0 2455 // For package apps, the permissions are not in the mini-manifest, so
michael@0 2456 // don't update the permissions yet.
michael@0 2457 if (!aData.isPackage) {
michael@0 2458 if (supportUseCurrentProfile()) {
michael@0 2459 PermissionsInstaller.installPermissions(
michael@0 2460 {
michael@0 2461 origin: appObject.origin,
michael@0 2462 manifestURL: appObject.manifestURL,
michael@0 2463 manifest: jsonManifest
michael@0 2464 },
michael@0 2465 isReinstall,
michael@0 2466 this.uninstall.bind(this, aData, aData.mm)
michael@0 2467 );
michael@0 2468 }
michael@0 2469
michael@0 2470 this.updateDataStore(this.webapps[id].localId, this.webapps[id].origin,
michael@0 2471 this.webapps[id].manifestURL, jsonManifest,
michael@0 2472 this.webapps[id].appStatus);
michael@0 2473 }
michael@0 2474
michael@0 2475 for each (let prop in ["installState", "downloadAvailable", "downloading",
michael@0 2476 "downloadSize", "readyToApplyDownload"]) {
michael@0 2477 aData.app[prop] = appObject[prop];
michael@0 2478 }
michael@0 2479
michael@0 2480 if (manifest.appcache_path) {
michael@0 2481 this.queuedDownload[app.manifestURL] = {
michael@0 2482 manifest: manifest,
michael@0 2483 app: appObject,
michael@0 2484 profileDir: aProfileDir
michael@0 2485 }
michael@0 2486 }
michael@0 2487
michael@0 2488 // We notify about the successful installation via mgmt.oninstall and the
michael@0 2489 // corresponging DOMRequest.onsuccess event as soon as the app is properly
michael@0 2490 // saved in the registry.
michael@0 2491 this._saveApps().then(() => {
michael@0 2492 this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
michael@0 2493 if (aData.isPackage && aData.apkInstall && !aData.requestID) {
michael@0 2494 // Skip directly to onInstallSuccessAck, since there isn't
michael@0 2495 // a WebappsRegistry to receive Webapps:Install:Return:OK and respond
michael@0 2496 // Webapps:Install:Return:Ack when an app is being auto-installed.
michael@0 2497 this.onInstallSuccessAck(app.manifestURL);
michael@0 2498 } else {
michael@0 2499 // Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify
michael@0 2500 // the installing page about the successful install, after which it'll
michael@0 2501 // respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck.
michael@0 2502 this.broadcastMessage("Webapps:Install:Return:OK", aData);
michael@0 2503 }
michael@0 2504 if (!aData.isPackage) {
michael@0 2505 this.updateAppHandlers(null, app.manifest, app);
michael@0 2506 if (aInstallSuccessCallback) {
michael@0 2507 aInstallSuccessCallback(app.manifest);
michael@0 2508 }
michael@0 2509 }
michael@0 2510 Services.obs.notifyObservers(null, "webapps-installed",
michael@0 2511 JSON.stringify({ manifestURL: app.manifestURL }));
michael@0 2512 });
michael@0 2513
michael@0 2514 let dontNeedNetwork = false;
michael@0 2515 if (manifest.package_path) {
michael@0 2516 // If it is a local app then it must been installed from a local file
michael@0 2517 // instead of web.
michael@0 2518 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 2519 // In that case, we would already have the manifest, not just the update
michael@0 2520 // manifest.
michael@0 2521 dontNeedNetwork = !!aData.app.manifest;
michael@0 2522 #else
michael@0 2523 if (aData.app.localInstallPath) {
michael@0 2524 dontNeedNetwork = true;
michael@0 2525 jsonManifest.package_path = "file://" + aData.app.localInstallPath;
michael@0 2526 }
michael@0 2527 #endif
michael@0 2528
michael@0 2529 // origin for install apps is meaningless here, since it's app:// and this
michael@0 2530 // can't be used to resolve package paths.
michael@0 2531 manifest = new ManifestHelper(jsonManifest, app.manifestURL);
michael@0 2532
michael@0 2533 this.queuedPackageDownload[app.manifestURL] = {
michael@0 2534 manifest: manifest,
michael@0 2535 app: appObject,
michael@0 2536 callback: aInstallSuccessCallback
michael@0 2537 };
michael@0 2538 }
michael@0 2539
michael@0 2540 if (aData.forceSuccessAck) {
michael@0 2541 // If it's a local install, there's no content process so just
michael@0 2542 // ack the install.
michael@0 2543 this.onInstallSuccessAck(app.manifestURL, dontNeedNetwork);
michael@0 2544 }
michael@0 2545 },
michael@0 2546
michael@0 2547 /**
michael@0 2548 * Install the package after successfully downloading it
michael@0 2549 *
michael@0 2550 * Bound params:
michael@0 2551 *
michael@0 2552 * @param aNewApp {Object} the new app data
michael@0 2553 * @param aInstallSuccessCallback {Function}
michael@0 2554 * the callback to call on install success
michael@0 2555 *
michael@0 2556 * Passed params:
michael@0 2557 *
michael@0 2558 * @param aId {Integer} the unique ID of the application
michael@0 2559 * @param aManifest {Object} The manifest of the application
michael@0 2560 */
michael@0 2561 _onDownloadPackage: Task.async(function*(aNewApp, aInstallSuccessCallback,
michael@0 2562 [aId, aManifest]) {
michael@0 2563 debug("_onDownloadPackage");
michael@0 2564 // Success! Move the zip out of TmpD.
michael@0 2565 let app = this.webapps[aId];
michael@0 2566 let zipFile =
michael@0 2567 FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
michael@0 2568 let dir = this._getAppDir(aId);
michael@0 2569 zipFile.moveTo(dir, "application.zip");
michael@0 2570 let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
michael@0 2571 try {
michael@0 2572 tmpDir.remove(true);
michael@0 2573 } catch(e) { }
michael@0 2574
michael@0 2575 // Save the manifest
michael@0 2576 let manFile = OS.Path.join(dir.path, "manifest.webapp");
michael@0 2577 yield this._writeFile(manFile, JSON.stringify(aManifest));
michael@0 2578 // Set state and fire events.
michael@0 2579 app.installState = "installed";
michael@0 2580 app.downloading = false;
michael@0 2581 app.downloadAvailable = false;
michael@0 2582
michael@0 2583 yield this._saveApps();
michael@0 2584
michael@0 2585 this.updateAppHandlers(null, aManifest, aNewApp);
michael@0 2586 // Clear the manifest cache in case it holds the update manifest.
michael@0 2587 if (aId in this._manifestCache) {
michael@0 2588 delete this._manifestCache[aId];
michael@0 2589 }
michael@0 2590
michael@0 2591 this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp });
michael@0 2592 Services.obs.notifyObservers(null, "webapps-installed",
michael@0 2593 JSON.stringify({ manifestURL: aNewApp.manifestURL }));
michael@0 2594
michael@0 2595 if (supportUseCurrentProfile()) {
michael@0 2596 // Update the permissions for this app.
michael@0 2597 PermissionsInstaller.installPermissions({
michael@0 2598 manifest: aManifest,
michael@0 2599 origin: aNewApp.origin,
michael@0 2600 manifestURL: aNewApp.manifestURL
michael@0 2601 }, true);
michael@0 2602 }
michael@0 2603
michael@0 2604 this.updateDataStore(this.webapps[aId].localId, aNewApp.origin,
michael@0 2605 aNewApp.manifestURL, aManifest, aNewApp.appStatus);
michael@0 2606
michael@0 2607 this.broadcastMessage("Webapps:UpdateState", {
michael@0 2608 app: app,
michael@0 2609 manifest: aManifest,
michael@0 2610 manifestURL: aNewApp.manifestURL
michael@0 2611 });
michael@0 2612
michael@0 2613 // Check if we have asm.js code to preload for this application.
michael@0 2614 yield ScriptPreloader.preload(aNewApp, aManifest);
michael@0 2615
michael@0 2616 this.broadcastMessage("Webapps:FireEvent", {
michael@0 2617 eventType: ["downloadsuccess", "downloadapplied"],
michael@0 2618 manifestURL: aNewApp.manifestURL
michael@0 2619 });
michael@0 2620
michael@0 2621 if (aInstallSuccessCallback) {
michael@0 2622 aInstallSuccessCallback(aManifest, zipFile.path);
michael@0 2623 }
michael@0 2624 }),
michael@0 2625
michael@0 2626 _nextLocalId: function() {
michael@0 2627 let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
michael@0 2628
michael@0 2629 while (this.getManifestURLByLocalId(id)) {
michael@0 2630 id++;
michael@0 2631 }
michael@0 2632
michael@0 2633 Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
michael@0 2634 Services.prefs.savePrefFile(null);
michael@0 2635 return id;
michael@0 2636 },
michael@0 2637
michael@0 2638 _appIdForManifestURL: function(aURI) {
michael@0 2639 for (let id in this.webapps) {
michael@0 2640 if (this.webapps[id].manifestURL == aURI)
michael@0 2641 return id;
michael@0 2642 }
michael@0 2643 return null;
michael@0 2644 },
michael@0 2645
michael@0 2646 makeAppId: function() {
michael@0 2647 let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
michael@0 2648 return uuidGenerator.generateUUID().toString();
michael@0 2649 },
michael@0 2650
michael@0 2651 _saveApps: function() {
michael@0 2652 return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2));
michael@0 2653 },
michael@0 2654
michael@0 2655 /**
michael@0 2656 * Asynchronously reads a list of manifests
michael@0 2657 */
michael@0 2658
michael@0 2659 _manifestCache: {},
michael@0 2660
michael@0 2661 _readManifests: function(aData) {
michael@0 2662 return Task.spawn(function*() {
michael@0 2663 for (let elem of aData) {
michael@0 2664 let id = elem.id;
michael@0 2665
michael@0 2666 if (!this._manifestCache[id]) {
michael@0 2667 // the manifest file used to be named manifest.json, so fallback on this.
michael@0 2668 let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath()
michael@0 2669 ? "coreAppsDir" : DIRECTORY_NAME;
michael@0 2670
michael@0 2671 let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true);
michael@0 2672
michael@0 2673 let fileNames = ["manifest.webapp", "update.webapp", "manifest.json"];
michael@0 2674 for (let fileName of fileNames) {
michael@0 2675 this._manifestCache[id] = yield AppsUtils.loadJSONAsync(OS.Path.join(dir.path, fileName));
michael@0 2676 if (this._manifestCache[id]) {
michael@0 2677 break;
michael@0 2678 }
michael@0 2679 }
michael@0 2680 }
michael@0 2681
michael@0 2682 elem.manifest = this._manifestCache[id];
michael@0 2683 }
michael@0 2684
michael@0 2685 return aData;
michael@0 2686 }.bind(this)).then(null, Cu.reportError);
michael@0 2687 },
michael@0 2688
michael@0 2689 downloadPackage: function(aManifest, aNewApp, aIsUpdate, aOnSuccess) {
michael@0 2690 // Here are the steps when installing a package:
michael@0 2691 // - create a temp directory where to store the app.
michael@0 2692 // - download the zip in this directory.
michael@0 2693 // - check the signature on the zip.
michael@0 2694 // - extract the manifest from the zip and check it.
michael@0 2695 // - ask confirmation to the user.
michael@0 2696 // - add the new app to the registry.
michael@0 2697 // If we fail at any step, we revert the previous ones and return an error.
michael@0 2698
michael@0 2699 // We define these outside the task to use them in its reject handler.
michael@0 2700 let id = this._appIdForManifestURL(aNewApp.manifestURL);
michael@0 2701 let oldApp = this.webapps[id];
michael@0 2702
michael@0 2703 return Task.spawn((function*() {
michael@0 2704 yield this._ensureSufficientStorage(aNewApp);
michael@0 2705
michael@0 2706 let fullPackagePath = aManifest.fullPackagePath();
michael@0 2707
michael@0 2708 // Check if it's a local file install (we've downloaded/sideloaded the
michael@0 2709 // package already, it existed on the build, or it came with an APK).
michael@0 2710 // Note that this variable also controls whether files signed with expired
michael@0 2711 // certificates are accepted or not. If isLocalFileInstall is true and the
michael@0 2712 // device date is earlier than the build generation date, then the signature
michael@0 2713 // will be accepted even if the certificate is expired.
michael@0 2714 let isLocalFileInstall =
michael@0 2715 Services.io.extractScheme(fullPackagePath) === 'file';
michael@0 2716
michael@0 2717 debug("About to download " + fullPackagePath);
michael@0 2718
michael@0 2719 let requestChannel = this._getRequestChannel(fullPackagePath,
michael@0 2720 isLocalFileInstall,
michael@0 2721 oldApp,
michael@0 2722 aNewApp);
michael@0 2723
michael@0 2724 AppDownloadManager.add(
michael@0 2725 aNewApp.manifestURL,
michael@0 2726 {
michael@0 2727 channel: requestChannel,
michael@0 2728 appId: id,
michael@0 2729 previousState: aIsUpdate ? "installed" : "pending"
michael@0 2730 }
michael@0 2731 );
michael@0 2732
michael@0 2733 // We set the 'downloading' flag to true right before starting the fetch.
michael@0 2734 oldApp.downloading = true;
michael@0 2735
michael@0 2736 // We determine the app's 'installState' according to its previous
michael@0 2737 // state. Cancelled download should remain as 'pending'. Successfully
michael@0 2738 // installed apps should morph to 'updating'.
michael@0 2739 oldApp.installState = aIsUpdate ? "updating" : "pending";
michael@0 2740
michael@0 2741 // initialize the progress to 0 right now
michael@0 2742 oldApp.progress = 0;
michael@0 2743
michael@0 2744 let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp);
michael@0 2745 let hash = yield this._computeFileHash(zipFile.path);
michael@0 2746
michael@0 2747 let responseStatus = requestChannel.responseStatus;
michael@0 2748 let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash);
michael@0 2749
michael@0 2750 if (oldPackage) {
michael@0 2751 debug("package's etag or hash unchanged; sending 'applied' event");
michael@0 2752 // The package's Etag or hash has not changed.
michael@0 2753 // We send a "applied" event right away.
michael@0 2754 this._sendAppliedEvent(aNewApp, oldApp, id);
michael@0 2755 return;
michael@0 2756 }
michael@0 2757
michael@0 2758 let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp,
michael@0 2759 isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash);
michael@0 2760
michael@0 2761 AppDownloadManager.remove(aNewApp.manifestURL);
michael@0 2762
michael@0 2763 return [oldApp.id, newManifest];
michael@0 2764
michael@0 2765 }).bind(this)).then(
michael@0 2766 aOnSuccess,
michael@0 2767 this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate)
michael@0 2768 );
michael@0 2769 },
michael@0 2770
michael@0 2771 _ensureSufficientStorage: function(aNewApp) {
michael@0 2772 let deferred = Promise.defer();
michael@0 2773
michael@0 2774 let navigator = Services.wm.getMostRecentWindow(chromeWindowType)
michael@0 2775 .navigator;
michael@0 2776 let deviceStorage = null;
michael@0 2777
michael@0 2778 if (navigator.getDeviceStorage) {
michael@0 2779 deviceStorage = navigator.getDeviceStorage("apps");
michael@0 2780 }
michael@0 2781
michael@0 2782 if (deviceStorage) {
michael@0 2783 let req = deviceStorage.freeSpace();
michael@0 2784 req.onsuccess = req.onerror = e => {
michael@0 2785 let freeBytes = e.target.result;
michael@0 2786 let sufficientStorage = this._checkDownloadSize(freeBytes, aNewApp);
michael@0 2787 if (sufficientStorage) {
michael@0 2788 deferred.resolve();
michael@0 2789 } else {
michael@0 2790 deferred.reject("INSUFFICIENT_STORAGE");
michael@0 2791 }
michael@0 2792 }
michael@0 2793 } else {
michael@0 2794 debug("No deviceStorage");
michael@0 2795 // deviceStorage isn't available, so use FileUtils to find the size of
michael@0 2796 // available storage.
michael@0 2797 let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true);
michael@0 2798 try {
michael@0 2799 let sufficientStorage = this._checkDownloadSize(dir.diskSpaceAvailable,
michael@0 2800 aNewApp);
michael@0 2801 if (sufficientStorage) {
michael@0 2802 deferred.resolve();
michael@0 2803 } else {
michael@0 2804 deferred.reject("INSUFFICIENT_STORAGE");
michael@0 2805 }
michael@0 2806 } catch(ex) {
michael@0 2807 // If disk space information isn't available, we'll end up here.
michael@0 2808 // We should proceed anyway, otherwise devices that support neither
michael@0 2809 // deviceStorage nor diskSpaceAvailable will never be able to install
michael@0 2810 // packaged apps.
michael@0 2811 deferred.resolve();
michael@0 2812 }
michael@0 2813 }
michael@0 2814
michael@0 2815 return deferred.promise;
michael@0 2816 },
michael@0 2817
michael@0 2818 _checkDownloadSize: function(aFreeBytes, aNewApp) {
michael@0 2819 if (aFreeBytes) {
michael@0 2820 debug("Free storage: " + aFreeBytes + ". Download size: " +
michael@0 2821 aNewApp.downloadSize);
michael@0 2822 if (aFreeBytes <=
michael@0 2823 aNewApp.downloadSize + AppDownloadManager.MIN_REMAINING_FREESPACE) {
michael@0 2824 return false;
michael@0 2825 }
michael@0 2826 }
michael@0 2827 return true;
michael@0 2828 },
michael@0 2829
michael@0 2830 _getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp,
michael@0 2831 aNewApp) {
michael@0 2832 let requestChannel;
michael@0 2833
michael@0 2834 if (aIsLocalFileInstall) {
michael@0 2835 requestChannel = NetUtil.newChannel(aFullPackagePath)
michael@0 2836 .QueryInterface(Ci.nsIFileChannel);
michael@0 2837 } else {
michael@0 2838 requestChannel = NetUtil.newChannel(aFullPackagePath)
michael@0 2839 .QueryInterface(Ci.nsIHttpChannel);
michael@0 2840 requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 2841 }
michael@0 2842
michael@0 2843 if (aOldApp.packageEtag && !aIsLocalFileInstall) {
michael@0 2844 debug("Add If-None-Match header: " + aOldApp.packageEtag);
michael@0 2845 requestChannel.setRequestHeader("If-None-Match", aOldApp.packageEtag,
michael@0 2846 false);
michael@0 2847 }
michael@0 2848
michael@0 2849 let lastProgressTime = 0;
michael@0 2850
michael@0 2851 requestChannel.notificationCallbacks = {
michael@0 2852 QueryInterface: function(aIID) {
michael@0 2853 if (aIID.equals(Ci.nsISupports) ||
michael@0 2854 aIID.equals(Ci.nsIProgressEventSink) ||
michael@0 2855 aIID.equals(Ci.nsILoadContext))
michael@0 2856 return this;
michael@0 2857 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 2858 },
michael@0 2859 getInterface: function(aIID) {
michael@0 2860 return this.QueryInterface(aIID);
michael@0 2861 },
michael@0 2862 onProgress: (function(aRequest, aContext, aProgress, aProgressMax) {
michael@0 2863 aOldApp.progress = aProgress;
michael@0 2864 let now = Date.now();
michael@0 2865 if (now - lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
michael@0 2866 debug("onProgress: " + aProgress + "/" + aProgressMax);
michael@0 2867 this._sendDownloadProgressEvent(aNewApp, aProgress);
michael@0 2868 lastProgressTime = now;
michael@0 2869 this._saveApps();
michael@0 2870 }
michael@0 2871 }).bind(this),
michael@0 2872 onStatus: function(aRequest, aContext, aStatus, aStatusArg) { },
michael@0 2873
michael@0 2874 // nsILoadContext
michael@0 2875 appId: aOldApp.installerAppId,
michael@0 2876 isInBrowserElement: aOldApp.installerIsBrowser,
michael@0 2877 usePrivateBrowsing: false,
michael@0 2878 isContent: false,
michael@0 2879 associatedWindow: null,
michael@0 2880 topWindow : null,
michael@0 2881 isAppOfType: function(appType) {
michael@0 2882 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0 2883 }
michael@0 2884 };
michael@0 2885
michael@0 2886 return requestChannel;
michael@0 2887 },
michael@0 2888
michael@0 2889 _sendDownloadProgressEvent: function(aNewApp, aProgress) {
michael@0 2890 this.broadcastMessage("Webapps:UpdateState", {
michael@0 2891 app: {
michael@0 2892 progress: aProgress
michael@0 2893 },
michael@0 2894 manifestURL: aNewApp.manifestURL
michael@0 2895 });
michael@0 2896 this.broadcastMessage("Webapps:FireEvent", {
michael@0 2897 eventType: "progress",
michael@0 2898 manifestURL: aNewApp.manifestURL
michael@0 2899 });
michael@0 2900 },
michael@0 2901
michael@0 2902 _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
michael@0 2903 let deferred = Promise.defer();
michael@0 2904
michael@0 2905 // Staging the zip in TmpD until all the checks are done.
michael@0 2906 let zipFile =
michael@0 2907 FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
michael@0 2908
michael@0 2909 // We need an output stream to write the channel content to the zip file.
michael@0 2910 let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
michael@0 2911 .createInstance(Ci.nsIFileOutputStream);
michael@0 2912 // write, create, truncate
michael@0 2913 outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
michael@0 2914 let bufferedOutputStream =
michael@0 2915 Cc['@mozilla.org/network/buffered-output-stream;1']
michael@0 2916 .createInstance(Ci.nsIBufferedOutputStream);
michael@0 2917 bufferedOutputStream.init(outputStream, 1024);
michael@0 2918
michael@0 2919 // Create a listener that will give data to the file output stream.
michael@0 2920 let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
michael@0 2921 .createInstance(Ci.nsISimpleStreamListener);
michael@0 2922
michael@0 2923 listener.init(bufferedOutputStream, {
michael@0 2924 onStartRequest: function(aRequest, aContext) {
michael@0 2925 // Nothing to do there anymore.
michael@0 2926 },
michael@0 2927
michael@0 2928 onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0 2929 bufferedOutputStream.close();
michael@0 2930 outputStream.close();
michael@0 2931
michael@0 2932 if (!Components.isSuccessCode(aStatusCode)) {
michael@0 2933 deferred.reject("NETWORK_ERROR");
michael@0 2934 return;
michael@0 2935 }
michael@0 2936
michael@0 2937 // If we get a 4XX or a 5XX http status, bail out like if we had a
michael@0 2938 // network error.
michael@0 2939 let responseStatus = aRequestChannel.responseStatus;
michael@0 2940 if (responseStatus >= 400 && responseStatus <= 599) {
michael@0 2941 // unrecoverable error, don't bug the user
michael@0 2942 aOldApp.downloadAvailable = false;
michael@0 2943 deferred.reject("NETWORK_ERROR");
michael@0 2944 return;
michael@0 2945 }
michael@0 2946
michael@0 2947 deferred.resolve(zipFile);
michael@0 2948 }
michael@0 2949 });
michael@0 2950 aRequestChannel.asyncOpen(listener, null);
michael@0 2951
michael@0 2952 // send a first progress event to correctly set the DOM object's properties
michael@0 2953 this._sendDownloadProgressEvent(aNewApp, 0);
michael@0 2954
michael@0 2955 return deferred.promise;
michael@0 2956 },
michael@0 2957
michael@0 2958 /**
michael@0 2959 * Compute the MD5 hash of a file, doing async IO off the main thread.
michael@0 2960 *
michael@0 2961 * @param {String} aFilePath
michael@0 2962 * the path of the file to hash
michael@0 2963 * @returns {String} the MD5 hash of the file
michael@0 2964 */
michael@0 2965 _computeFileHash: function(aFilePath) {
michael@0 2966 let deferred = Promise.defer();
michael@0 2967
michael@0 2968 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
michael@0 2969 file.initWithPath(aFilePath);
michael@0 2970
michael@0 2971 NetUtil.asyncFetch(file, function(inputStream, status) {
michael@0 2972 if (!Components.isSuccessCode(status)) {
michael@0 2973 debug("Error reading " + aFilePath + ": " + e);
michael@0 2974 deferred.reject();
michael@0 2975 return;
michael@0 2976 }
michael@0 2977
michael@0 2978 let hasher = Cc["@mozilla.org/security/hash;1"]
michael@0 2979 .createInstance(Ci.nsICryptoHash);
michael@0 2980 // We want to use the MD5 algorithm.
michael@0 2981 hasher.init(hasher.MD5);
michael@0 2982
michael@0 2983 const PR_UINT32_MAX = 0xffffffff;
michael@0 2984 hasher.updateFromStream(inputStream, PR_UINT32_MAX);
michael@0 2985
michael@0 2986 // Return the two-digit hexadecimal code for a byte.
michael@0 2987 function toHexString(charCode) {
michael@0 2988 return ("0" + charCode.toString(16)).slice(-2);
michael@0 2989 }
michael@0 2990
michael@0 2991 // We're passing false to get the binary hash and not base64.
michael@0 2992 let data = hasher.finish(false);
michael@0 2993 // Convert the binary hash data to a hex string.
michael@0 2994 let hash = [toHexString(data.charCodeAt(i)) for (i in data)].join("");
michael@0 2995 debug("File hash computed: " + hash);
michael@0 2996
michael@0 2997 deferred.resolve(hash);
michael@0 2998 });
michael@0 2999
michael@0 3000 return deferred.promise;
michael@0 3001 },
michael@0 3002
michael@0 3003 /**
michael@0 3004 * Send an "applied" event right away for the package being installed.
michael@0 3005 *
michael@0 3006 * XXX We use this to exit the app update process early when the downloaded
michael@0 3007 * package is identical to the last one we installed. Presumably we do
michael@0 3008 * something similar after updating the app, and we could refactor both cases
michael@0 3009 * to use the same code to send the "applied" event.
michael@0 3010 *
michael@0 3011 * @param aNewApp {Object} the new app data
michael@0 3012 * @param aOldApp {Object} the currently stored app data
michael@0 3013 * @param aId {String} the unique id of the app
michael@0 3014 */
michael@0 3015 _sendAppliedEvent: function(aNewApp, aOldApp, aId) {
michael@0 3016 aOldApp.downloading = false;
michael@0 3017 aOldApp.downloadAvailable = false;
michael@0 3018 aOldApp.downloadSize = 0;
michael@0 3019 aOldApp.installState = "installed";
michael@0 3020 aOldApp.readyToApplyDownload = false;
michael@0 3021 if (aOldApp.staged && aOldApp.staged.manifestHash) {
michael@0 3022 // If we're here then the manifest has changed but the package
michael@0 3023 // hasn't. Let's clear this, so we don't keep offering
michael@0 3024 // a bogus update to the user
michael@0 3025 aOldApp.manifestHash = aOldApp.staged.manifestHash;
michael@0 3026 aOldApp.etag = aOldApp.staged.etag || aOldApp.etag;
michael@0 3027 aOldApp.staged = {};
michael@0 3028
michael@0 3029 // Move the staged update manifest to a non staged one.
michael@0 3030 try {
michael@0 3031 let staged = this._getAppDir(aId);
michael@0 3032 staged.append("staged-update.webapp");
michael@0 3033 staged.moveTo(staged.parent, "update.webapp");
michael@0 3034 } catch (ex) {
michael@0 3035 // We don't really mind much if this fails.
michael@0 3036 }
michael@0 3037 }
michael@0 3038
michael@0 3039 // Save the updated registry, and cleanup the tmp directory.
michael@0 3040 this._saveApps().then(() => {
michael@0 3041 this.broadcastMessage("Webapps:UpdateState", {
michael@0 3042 app: aOldApp,
michael@0 3043 manifestURL: aNewApp.manifestURL
michael@0 3044 });
michael@0 3045 this.broadcastMessage("Webapps:FireEvent", {
michael@0 3046 manifestURL: aNewApp.manifestURL,
michael@0 3047 eventType: ["downloadsuccess", "downloadapplied"]
michael@0 3048 });
michael@0 3049 });
michael@0 3050 let file = FileUtils.getFile("TmpD", ["webapps", aId], false);
michael@0 3051 if (file && file.exists()) {
michael@0 3052 file.remove(true);
michael@0 3053 }
michael@0 3054 },
michael@0 3055
michael@0 3056 _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall,
michael@0 3057 aIsUpdate, aManifest, aRequestChannel, aHash) {
michael@0 3058 return Task.spawn((function*() {
michael@0 3059 let zipReader, isSigned, newManifest;
michael@0 3060
michael@0 3061 try {
michael@0 3062 [zipReader, isSigned] = yield this._openPackage(aZipFile, aOldApp,
michael@0 3063 aIsLocalFileInstall);
michael@0 3064 newManifest = yield this._readPackage(aOldApp, aNewApp,
michael@0 3065 aIsLocalFileInstall, aIsUpdate, aManifest, aRequestChannel,
michael@0 3066 aHash, zipReader, isSigned);
michael@0 3067 } catch (e) {
michael@0 3068 debug("package open/read error: " + e);
michael@0 3069 // Something bad happened when opening/reading the package.
michael@0 3070 // Unrecoverable error, don't bug the user.
michael@0 3071 // Apps with installState 'pending' does not produce any
michael@0 3072 // notification, so we are safe with its current
michael@0 3073 // downloadAvailable state.
michael@0 3074 if (aOldApp.installState !== "pending") {
michael@0 3075 aOldApp.downloadAvailable = false;
michael@0 3076 }
michael@0 3077 if (typeof e == 'object') {
michael@0 3078 Cu.reportError("Error while reading package:" + e);
michael@0 3079 throw "INVALID_PACKAGE";
michael@0 3080 } else {
michael@0 3081 throw e;
michael@0 3082 }
michael@0 3083 } finally {
michael@0 3084 if (zipReader) {
michael@0 3085 zipReader.close();
michael@0 3086 }
michael@0 3087 }
michael@0 3088
michael@0 3089 return newManifest;
michael@0 3090
michael@0 3091 }).bind(this));
michael@0 3092 },
michael@0 3093
michael@0 3094 _openPackage: function(aZipFile, aApp, aIsLocalFileInstall) {
michael@0 3095 return Task.spawn((function*() {
michael@0 3096 let certDb;
michael@0 3097 try {
michael@0 3098 certDb = Cc["@mozilla.org/security/x509certdb;1"]
michael@0 3099 .getService(Ci.nsIX509CertDB);
michael@0 3100 } catch (e) {
michael@0 3101 debug("nsIX509CertDB error: " + e);
michael@0 3102 // unrecoverable error, don't bug the user
michael@0 3103 aApp.downloadAvailable = false;
michael@0 3104 throw "CERTDB_ERROR";
michael@0 3105 }
michael@0 3106
michael@0 3107 let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin,
michael@0 3108 aApp.manifestURL,
michael@0 3109 aZipFile,
michael@0 3110 certDb);
michael@0 3111
michael@0 3112 // We cannot really know if the system date is correct or
michael@0 3113 // not. What we can know is if it's after the build date or not,
michael@0 3114 // and assume the build date is correct (which we cannot
michael@0 3115 // really know either).
michael@0 3116 let isLaterThanBuildTime = Date.now() > PLATFORM_BUILD_ID_TIME;
michael@0 3117
michael@0 3118 let isSigned;
michael@0 3119
michael@0 3120 if (Components.isSuccessCode(result)) {
michael@0 3121 isSigned = true;
michael@0 3122 } else if (result == Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY ||
michael@0 3123 result == Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY ||
michael@0 3124 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING) {
michael@0 3125 throw "APP_PACKAGE_CORRUPTED";
michael@0 3126 } else if (result == Cr.NS_ERROR_FILE_CORRUPTED ||
michael@0 3127 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE ||
michael@0 3128 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID ||
michael@0 3129 result == Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID) {
michael@0 3130 throw "APP_PACKAGE_INVALID";
michael@0 3131 } else if ((!aIsLocalFileInstall || isLaterThanBuildTime) &&
michael@0 3132 (result != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)) {
michael@0 3133 throw "INVALID_SIGNATURE";
michael@0 3134 } else {
michael@0 3135 // If it's a localFileInstall and the validation failed
michael@0 3136 // because of a expired certificate, just assume it was valid
michael@0 3137 // and that the error occurred because the system time has not
michael@0 3138 // been set yet.
michael@0 3139 isSigned = (aIsLocalFileInstall &&
michael@0 3140 (getNSPRErrorCode(result) ==
michael@0 3141 SEC_ERROR_EXPIRED_CERTIFICATE));
michael@0 3142
michael@0 3143 zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
michael@0 3144 .createInstance(Ci.nsIZipReader);
michael@0 3145 zipReader.open(aZipFile);
michael@0 3146 }
michael@0 3147
michael@0 3148 return [zipReader, isSigned];
michael@0 3149
michael@0 3150 }).bind(this));
michael@0 3151 },
michael@0 3152
michael@0 3153 _openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) {
michael@0 3154 let deferred = Promise.defer();
michael@0 3155
michael@0 3156 let root = TrustedRootCertificate.index;
michael@0 3157
michael@0 3158 let useReviewerCerts = false;
michael@0 3159 try {
michael@0 3160 useReviewerCerts = Services.prefs.
michael@0 3161 getBoolPref("dom.mozApps.use_reviewer_certs");
michael@0 3162 } catch (ex) { }
michael@0 3163
michael@0 3164 // We'll use the reviewer and dev certificates only if the pref is set to
michael@0 3165 // true.
michael@0 3166 if (useReviewerCerts) {
michael@0 3167 let manifestPath = Services.io.newURI(aManifestURL, null, null).path;
michael@0 3168
michael@0 3169 switch (aInstallOrigin) {
michael@0 3170 case "https://marketplace.firefox.com":
michael@0 3171 root = manifestPath.startsWith("/reviewers/")
michael@0 3172 ? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot
michael@0 3173 : Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot;
michael@0 3174 break;
michael@0 3175
michael@0 3176 case "https://marketplace-dev.allizom.org":
michael@0 3177 root = manifestPath.startsWith("/reviewers/")
michael@0 3178 ? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot
michael@0 3179 : Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot;
michael@0 3180 break;
michael@0 3181 }
michael@0 3182 }
michael@0 3183
michael@0 3184 aCertDb.openSignedAppFileAsync(
michael@0 3185 root, aZipFile,
michael@0 3186 function(aRv, aZipReader) {
michael@0 3187 deferred.resolve([aRv, aZipReader]);
michael@0 3188 }
michael@0 3189 );
michael@0 3190
michael@0 3191 return deferred.promise;
michael@0 3192 },
michael@0 3193
michael@0 3194 _readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate,
michael@0 3195 aManifest, aRequestChannel, aHash, aZipReader,
michael@0 3196 aIsSigned) {
michael@0 3197 this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall);
michael@0 3198
michael@0 3199 if (!aZipReader.hasEntry("manifest.webapp")) {
michael@0 3200 throw "MISSING_MANIFEST";
michael@0 3201 }
michael@0 3202
michael@0 3203 let istream = aZipReader.getInputStream("manifest.webapp");
michael@0 3204
michael@0 3205 // Obtain a converter to read from a UTF-8 encoded input stream.
michael@0 3206 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
michael@0 3207 .createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 3208 converter.charset = "UTF-8";
michael@0 3209
michael@0 3210 let newManifest = JSON.parse(converter.ConvertToUnicode(
michael@0 3211 NetUtil.readInputStreamToString(istream, istream.available()) || ""));
michael@0 3212
michael@0 3213 if (!AppsUtils.checkManifest(newManifest, aOldApp)) {
michael@0 3214 throw "INVALID_MANIFEST";
michael@0 3215 }
michael@0 3216
michael@0 3217 // For app updates we don't forbid apps to rename themselves but
michael@0 3218 // we still retain the old name of the app. In the future we
michael@0 3219 // will use UI to allow updates to rename an app after we check
michael@0 3220 // with the user that the rename is ok.
michael@0 3221 if (aIsUpdate) {
michael@0 3222 // Call ensureSameAppName before compareManifests as `manifest`
michael@0 3223 // has been normalized to avoid app rename.
michael@0 3224 AppsUtils.ensureSameAppName(aManifest._manifest, newManifest, aOldApp);
michael@0 3225 }
michael@0 3226
michael@0 3227 if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) {
michael@0 3228 throw "MANIFEST_MISMATCH";
michael@0 3229 }
michael@0 3230
michael@0 3231 if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) {
michael@0 3232 throw "INSTALL_FROM_DENIED";
michael@0 3233 }
michael@0 3234
michael@0 3235 // Local file installs can be privileged even without the signature.
michael@0 3236 let maxStatus = aIsSigned || aIsLocalFileInstall
michael@0 3237 ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
michael@0 3238 : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
michael@0 3239
michael@0 3240 if (AppsUtils.getAppManifestStatus(newManifest) > maxStatus) {
michael@0 3241 throw "INVALID_SECURITY_LEVEL";
michael@0 3242 }
michael@0 3243
michael@0 3244 aOldApp.appStatus = AppsUtils.getAppManifestStatus(newManifest);
michael@0 3245
michael@0 3246 this._saveEtag(aIsUpdate, aOldApp, aRequestChannel, aHash, newManifest);
michael@0 3247 this._checkOrigin(aIsSigned || aIsLocalFileInstall, aOldApp, newManifest,
michael@0 3248 aIsUpdate);
michael@0 3249 this._getIds(aIsSigned, aZipReader, converter, aNewApp, aOldApp, aIsUpdate);
michael@0 3250
michael@0 3251 return newManifest;
michael@0 3252 },
michael@0 3253
michael@0 3254 _checkSignature: function(aApp, aIsSigned, aIsLocalFileInstall) {
michael@0 3255 // XXX Security: You CANNOT safely add a new app store for
michael@0 3256 // installing privileged apps just by modifying this pref and
michael@0 3257 // adding the signing cert for that store to the cert trust
michael@0 3258 // database. *Any* origin listed can install apps signed with
michael@0 3259 // *any* certificate trusted; we don't try to maintain a strong
michael@0 3260 // association between certificate with installOrign. The
michael@0 3261 // expectation here is that in production builds the pref will
michael@0 3262 // contain exactly one origin. However, in custom development
michael@0 3263 // builds it may contain more than one origin so we can test
michael@0 3264 // different stages (dev, staging, prod) of the same app store.
michael@0 3265 //
michael@0 3266 // Only allow signed apps to be installed from a whitelist of
michael@0 3267 // domains, and require all packages installed from any of the
michael@0 3268 // domains on the whitelist to be signed. This is a stopgap until
michael@0 3269 // we have a real story for handling multiple app stores signing
michael@0 3270 // apps.
michael@0 3271 let signedAppOriginsStr =
michael@0 3272 Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from");
michael@0 3273 // If it's a local install and it's signed then we assume
michael@0 3274 // the app origin is a valid signer.
michael@0 3275 let isSignedAppOrigin = (aIsSigned && aIsLocalFileInstall) ||
michael@0 3276 signedAppOriginsStr.split(",").
michael@0 3277 indexOf(aApp.installOrigin) > -1;
michael@0 3278 if (!aIsSigned && isSignedAppOrigin) {
michael@0 3279 // Packaged apps installed from these origins must be signed;
michael@0 3280 // if not, assume somebody stripped the signature.
michael@0 3281 throw "INVALID_SIGNATURE";
michael@0 3282 } else if (aIsSigned && !isSignedAppOrigin) {
michael@0 3283 // Other origins are *prohibited* from installing signed apps.
michael@0 3284 // One reason is that our app revocation mechanism requires
michael@0 3285 // strong cooperation from the host of the mini-manifest, which
michael@0 3286 // we assume to be under the control of the install origin,
michael@0 3287 // even if it has a different origin.
michael@0 3288 throw "INSTALL_FROM_DENIED";
michael@0 3289 }
michael@0 3290 },
michael@0 3291
michael@0 3292 _saveEtag: function(aIsUpdate, aOldApp, aRequestChannel, aHash, aManifest) {
michael@0 3293 // Save the new Etag for the package.
michael@0 3294 if (aIsUpdate) {
michael@0 3295 if (!aOldApp.staged) {
michael@0 3296 aOldApp.staged = { };
michael@0 3297 }
michael@0 3298 try {
michael@0 3299 aOldApp.staged.packageEtag = aRequestChannel.getResponseHeader("Etag");
michael@0 3300 } catch(e) { }
michael@0 3301 aOldApp.staged.packageHash = aHash;
michael@0 3302 aOldApp.staged.appStatus = AppsUtils.getAppManifestStatus(aManifest);
michael@0 3303 } else {
michael@0 3304 try {
michael@0 3305 aOldApp.packageEtag = aRequestChannel.getResponseHeader("Etag");
michael@0 3306 } catch(e) { }
michael@0 3307 aOldApp.packageHash = aHash;
michael@0 3308 aOldApp.appStatus = AppsUtils.getAppManifestStatus(aManifest);
michael@0 3309 }
michael@0 3310 },
michael@0 3311
michael@0 3312 _checkOrigin: function(aIsSigned, aOldApp, aManifest, aIsUpdate) {
michael@0 3313 // Check if the app declares which origin it will use.
michael@0 3314 if (aIsSigned &&
michael@0 3315 aOldApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
michael@0 3316 aManifest.origin !== undefined) {
michael@0 3317 let uri;
michael@0 3318 try {
michael@0 3319 uri = Services.io.newURI(aManifest.origin, null, null);
michael@0 3320 } catch(e) {
michael@0 3321 throw "INVALID_ORIGIN";
michael@0 3322 }
michael@0 3323 if (uri.scheme != "app") {
michael@0 3324 throw "INVALID_ORIGIN";
michael@0 3325 }
michael@0 3326
michael@0 3327 if (aIsUpdate) {
michael@0 3328 // Changing the origin during an update is not allowed.
michael@0 3329 if (uri.prePath != aOldApp.origin) {
michael@0 3330 throw "INVALID_ORIGIN_CHANGE";
michael@0 3331 }
michael@0 3332 // Nothing else to do for an update... since the
michael@0 3333 // origin can't change we don't need to move the
michael@0 3334 // app nor can we have a duplicated origin
michael@0 3335 } else {
michael@0 3336 debug("Setting origin to " + uri.prePath +
michael@0 3337 " for " + aOldApp.manifestURL);
michael@0 3338 let newId = uri.prePath.substring(6); // "app://".length
michael@0 3339 if (newId in this.webapps) {
michael@0 3340 throw "DUPLICATE_ORIGIN";
michael@0 3341 }
michael@0 3342 aOldApp.origin = uri.prePath;
michael@0 3343 // Update the registry.
michael@0 3344 let oldId = aOldApp.id;
michael@0 3345 aOldApp.id = newId;
michael@0 3346 this.webapps[newId] = aOldApp;
michael@0 3347 delete this.webapps[oldId];
michael@0 3348 // Rename the directories where the files are installed.
michael@0 3349 [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) {
michael@0 3350 let parent = FileUtils.getDir(aDir, ["webapps"], true, true);
michael@0 3351 let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true);
michael@0 3352 dir.moveTo(parent, newId);
michael@0 3353 });
michael@0 3354 // Signals that we need to swap the old id with the new app.
michael@0 3355 this.broadcastMessage("Webapps:RemoveApp", { id: oldId });
michael@0 3356 this.broadcastMessage("Webapps:AddApp", { id: newId,
michael@0 3357 app: aOldApp });
michael@0 3358 }
michael@0 3359 }
michael@0 3360 },
michael@0 3361
michael@0 3362 _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp,
michael@0 3363 aIsUpdate) {
michael@0 3364 // Get ids.json if the file is signed
michael@0 3365 if (aIsSigned) {
michael@0 3366 let idsStream;
michael@0 3367 try {
michael@0 3368 idsStream = aZipReader.getInputStream("META-INF/ids.json");
michael@0 3369 } catch (e) {
michael@0 3370 throw aZipReader.hasEntry("META-INF/ids.json")
michael@0 3371 ? e
michael@0 3372 : "MISSING_IDS_JSON";
michael@0 3373 }
michael@0 3374
michael@0 3375 let ids = JSON.parse(aConverter.ConvertToUnicode(NetUtil.
michael@0 3376 readInputStreamToString( idsStream, idsStream.available()) || ""));
michael@0 3377 if ((!ids.id) || !Number.isInteger(ids.version) ||
michael@0 3378 (ids.version <= 0)) {
michael@0 3379 throw "INVALID_IDS_JSON";
michael@0 3380 }
michael@0 3381 let storeId = aNewApp.installOrigin + "#" + ids.id;
michael@0 3382 this._checkForStoreIdMatch(aIsUpdate, aOldApp, storeId, ids.version);
michael@0 3383 aOldApp.storeId = storeId;
michael@0 3384 aOldApp.storeVersion = ids.version;
michael@0 3385 }
michael@0 3386 },
michael@0 3387
michael@0 3388 // aStoreId must be a string of the form
michael@0 3389 // <installOrigin>#<storeId from ids.json>
michael@0 3390 // aStoreVersion must be a positive integer.
michael@0 3391 _checkForStoreIdMatch: function(aIsUpdate, aNewApp, aStoreId, aStoreVersion) {
michael@0 3392 // Things to check:
michael@0 3393 // 1. if it's a update:
michael@0 3394 // a. We should already have this storeId, or the original storeId must
michael@0 3395 // start with STORE_ID_PENDING_PREFIX
michael@0 3396 // b. The manifestURL for the stored app should be the same one we're
michael@0 3397 // updating
michael@0 3398 // c. And finally the version of the update should be higher than the one
michael@0 3399 // on the already installed package
michael@0 3400 // 2. else
michael@0 3401 // a. We should not have this storeId on the list
michael@0 3402 // We're currently launching WRONG_APP_STORE_ID for all the mismatch kind of
michael@0 3403 // errors, and APP_STORE_VERSION_ROLLBACK for the version error.
michael@0 3404
michael@0 3405 // Does an app with this storeID exist already?
michael@0 3406 let appId = this.getAppLocalIdByStoreId(aStoreId);
michael@0 3407 let isInstalled = appId != Ci.nsIScriptSecurityManager.NO_APP_ID;
michael@0 3408 if (aIsUpdate) {
michael@0 3409 let isDifferent = aNewApp.localId !== appId;
michael@0 3410 let isPending = aNewApp.storeId.indexOf(STORE_ID_PENDING_PREFIX) == 0;
michael@0 3411
michael@0 3412 if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) {
michael@0 3413 throw "WRONG_APP_STORE_ID";
michael@0 3414 }
michael@0 3415
michael@0 3416 if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) {
michael@0 3417 throw "APP_STORE_VERSION_ROLLBACK";
michael@0 3418 }
michael@0 3419
michael@0 3420 } else if (isInstalled) {
michael@0 3421 throw "WRONG_APP_STORE_ID";
michael@0 3422 }
michael@0 3423 },
michael@0 3424
michael@0 3425 // Removes the directory we created, and sends an error to the DOM side.
michael@0 3426 _revertDownloadPackage: function(aId, aOldApp, aNewApp, aIsUpdate, aError) {
michael@0 3427 debug("Cleanup: " + aError + "\n" + aError.stack);
michael@0 3428 let dir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
michael@0 3429 try {
michael@0 3430 dir.remove(true);
michael@0 3431 } catch (e) { }
michael@0 3432
michael@0 3433 // We avoid notifying the error to the DOM side if the app download
michael@0 3434 // was cancelled via cancelDownload, which already sends its own
michael@0 3435 // notification.
michael@0 3436 if (aOldApp.isCanceling) {
michael@0 3437 delete aOldApp.isCanceling;
michael@0 3438 return;
michael@0 3439 }
michael@0 3440
michael@0 3441 let download = AppDownloadManager.get(aNewApp.manifestURL);
michael@0 3442 aOldApp.downloading = false;
michael@0 3443
michael@0 3444 // If there were not enough storage to download the package we
michael@0 3445 // won't have a record of the download details, so we just set the
michael@0 3446 // installState to 'pending' at first download and to 'installed' when
michael@0 3447 // updating.
michael@0 3448 aOldApp.installState = download ? download.previousState
michael@0 3449 : aIsUpdate ? "installed"
michael@0 3450 : "pending";
michael@0 3451
michael@0 3452 if (aOldApp.staged) {
michael@0 3453 delete aOldApp.staged;
michael@0 3454 }
michael@0 3455
michael@0 3456 this._saveApps().then(() => {
michael@0 3457 this.broadcastMessage("Webapps:UpdateState", {
michael@0 3458 app: aOldApp,
michael@0 3459 error: aError,
michael@0 3460 manifestURL: aNewApp.manifestURL
michael@0 3461 });
michael@0 3462 this.broadcastMessage("Webapps:FireEvent", {
michael@0 3463 eventType: "downloaderror",
michael@0 3464 manifestURL: aNewApp.manifestURL
michael@0 3465 });
michael@0 3466 });
michael@0 3467 AppDownloadManager.remove(aNewApp.manifestURL);
michael@0 3468 },
michael@0 3469
michael@0 3470 doUninstall: function(aData, aMm) {
michael@0 3471 this.uninstall(aData.manifestURL,
michael@0 3472 function onsuccess() {
michael@0 3473 aMm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData);
michael@0 3474 },
michael@0 3475 function onfailure() {
michael@0 3476 // Fall-through, fails to uninstall the desired app because:
michael@0 3477 // - we cannot find the app to be uninstalled.
michael@0 3478 // - the app to be uninstalled is not removable.
michael@0 3479 aMm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData);
michael@0 3480 }
michael@0 3481 );
michael@0 3482 },
michael@0 3483
michael@0 3484 uninstall: function(aManifestURL, aOnSuccess, aOnFailure) {
michael@0 3485 debug("uninstall " + aManifestURL);
michael@0 3486
michael@0 3487 let app = this.getAppByManifestURL(aManifestURL);
michael@0 3488 if (!app) {
michael@0 3489 aOnFailure("NO_SUCH_APP");
michael@0 3490 return;
michael@0 3491 }
michael@0 3492 let id = app.id;
michael@0 3493
michael@0 3494 if (!app.removable) {
michael@0 3495 debug("Error: cannot uninstall a non-removable app.");
michael@0 3496 aOnFailure("NON_REMOVABLE_APP");
michael@0 3497 return;
michael@0 3498 }
michael@0 3499
michael@0 3500 // Check if we are downloading something for this app, and cancel the
michael@0 3501 // download if needed.
michael@0 3502 this.cancelDownload(app.manifestURL);
michael@0 3503
michael@0 3504 // Clean up the deprecated manifest cache if needed.
michael@0 3505 if (id in this._manifestCache) {
michael@0 3506 delete this._manifestCache[id];
michael@0 3507 }
michael@0 3508
michael@0 3509 // Clear private data first.
michael@0 3510 this._clearPrivateData(app.localId, false);
michael@0 3511
michael@0 3512 // Then notify observers.
michael@0 3513 // We have to clone the app object as nsIDOMApplication objects are
michael@0 3514 // stringified as an empty object. (see bug 830376)
michael@0 3515 let appClone = AppsUtils.cloneAppObject(app);
michael@0 3516 Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(appClone));
michael@0 3517
michael@0 3518 if (supportSystemMessages()) {
michael@0 3519 this._readManifests([{ id: id }]).then((aResult) => {
michael@0 3520 this._unregisterActivities(aResult[0].manifest, app);
michael@0 3521 });
michael@0 3522 }
michael@0 3523
michael@0 3524 let dir = this._getAppDir(id);
michael@0 3525 try {
michael@0 3526 dir.remove(true);
michael@0 3527 } catch (e) {}
michael@0 3528
michael@0 3529 delete this.webapps[id];
michael@0 3530
michael@0 3531 this._saveApps().then(() => {
michael@0 3532 this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", appClone);
michael@0 3533 // Catch exception on callback call to ensure notifying observers after
michael@0 3534 try {
michael@0 3535 if (aOnSuccess) {
michael@0 3536 aOnSuccess();
michael@0 3537 }
michael@0 3538 } catch(ex) {
michael@0 3539 Cu.reportError("DOMApplicationRegistry: Exception on app uninstall: " +
michael@0 3540 ex + "\n" + ex.stack);
michael@0 3541 }
michael@0 3542 this.broadcastMessage("Webapps:RemoveApp", { id: id });
michael@0 3543 });
michael@0 3544 },
michael@0 3545
michael@0 3546 getSelf: function(aData, aMm) {
michael@0 3547 aData.apps = [];
michael@0 3548
michael@0 3549 if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
michael@0 3550 aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
michael@0 3551 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
michael@0 3552 return;
michael@0 3553 }
michael@0 3554
michael@0 3555 let tmp = [];
michael@0 3556
michael@0 3557 for (let id in this.webapps) {
michael@0 3558 if (this.webapps[id].origin == aData.origin &&
michael@0 3559 this.webapps[id].localId == aData.appId &&
michael@0 3560 this._isLaunchable(this.webapps[id])) {
michael@0 3561 let app = AppsUtils.cloneAppObject(this.webapps[id]);
michael@0 3562 aData.apps.push(app);
michael@0 3563 tmp.push({ id: id });
michael@0 3564 break;
michael@0 3565 }
michael@0 3566 }
michael@0 3567
michael@0 3568 if (!aData.apps.length) {
michael@0 3569 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
michael@0 3570 return;
michael@0 3571 }
michael@0 3572
michael@0 3573 this._readManifests(tmp).then((aResult) => {
michael@0 3574 for (let i = 0; i < aResult.length; i++)
michael@0 3575 aData.apps[i].manifest = aResult[i].manifest;
michael@0 3576 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
michael@0 3577 });
michael@0 3578 },
michael@0 3579
michael@0 3580 checkInstalled: function(aData, aMm) {
michael@0 3581 aData.app = null;
michael@0 3582 let tmp = [];
michael@0 3583
michael@0 3584 for (let appId in this.webapps) {
michael@0 3585 if (this.webapps[appId].manifestURL == aData.manifestURL &&
michael@0 3586 this._isLaunchable(this.webapps[appId])) {
michael@0 3587 aData.app = AppsUtils.cloneAppObject(this.webapps[appId]);
michael@0 3588 tmp.push({ id: appId });
michael@0 3589 break;
michael@0 3590 }
michael@0 3591 }
michael@0 3592
michael@0 3593 this._readManifests(tmp).then((aResult) => {
michael@0 3594 for (let i = 0; i < aResult.length; i++) {
michael@0 3595 aData.app.manifest = aResult[i].manifest;
michael@0 3596 break;
michael@0 3597 }
michael@0 3598 aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", aData);
michael@0 3599 });
michael@0 3600 },
michael@0 3601
michael@0 3602 getInstalled: function(aData, aMm) {
michael@0 3603 aData.apps = [];
michael@0 3604 let tmp = [];
michael@0 3605
michael@0 3606 for (let id in this.webapps) {
michael@0 3607 if (this.webapps[id].installOrigin == aData.origin &&
michael@0 3608 this._isLaunchable(this.webapps[id])) {
michael@0 3609 aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
michael@0 3610 tmp.push({ id: id });
michael@0 3611 }
michael@0 3612 }
michael@0 3613
michael@0 3614 this._readManifests(tmp).then((aResult) => {
michael@0 3615 for (let i = 0; i < aResult.length; i++)
michael@0 3616 aData.apps[i].manifest = aResult[i].manifest;
michael@0 3617 aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", aData);
michael@0 3618 });
michael@0 3619 },
michael@0 3620
michael@0 3621 getNotInstalled: function(aData, aMm) {
michael@0 3622 aData.apps = [];
michael@0 3623 let tmp = [];
michael@0 3624
michael@0 3625 for (let id in this.webapps) {
michael@0 3626 if (!this._isLaunchable(this.webapps[id])) {
michael@0 3627 aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
michael@0 3628 tmp.push({ id: id });
michael@0 3629 }
michael@0 3630 }
michael@0 3631
michael@0 3632 this._readManifests(tmp).then((aResult) => {
michael@0 3633 for (let i = 0; i < aResult.length; i++)
michael@0 3634 aData.apps[i].manifest = aResult[i].manifest;
michael@0 3635 aMm.sendAsyncMessage("Webapps:GetNotInstalled:Return:OK", aData);
michael@0 3636 });
michael@0 3637 },
michael@0 3638
michael@0 3639 doGetAll: function(aData, aMm) {
michael@0 3640 this.getAll(function (apps) {
michael@0 3641 aData.apps = apps;
michael@0 3642 aMm.sendAsyncMessage("Webapps:GetAll:Return:OK", aData);
michael@0 3643 });
michael@0 3644 },
michael@0 3645
michael@0 3646 getAll: function(aCallback) {
michael@0 3647 debug("getAll");
michael@0 3648 let apps = [];
michael@0 3649 let tmp = [];
michael@0 3650
michael@0 3651 for (let id in this.webapps) {
michael@0 3652 let app = AppsUtils.cloneAppObject(this.webapps[id]);
michael@0 3653 if (!this._isLaunchable(app))
michael@0 3654 continue;
michael@0 3655
michael@0 3656 apps.push(app);
michael@0 3657 tmp.push({ id: id });
michael@0 3658 }
michael@0 3659
michael@0 3660 this._readManifests(tmp).then((aResult) => {
michael@0 3661 for (let i = 0; i < aResult.length; i++)
michael@0 3662 apps[i].manifest = aResult[i].manifest;
michael@0 3663 aCallback(apps);
michael@0 3664 });
michael@0 3665 },
michael@0 3666
michael@0 3667 /* Check if |data| is actually a receipt */
michael@0 3668 isReceipt: function(data) {
michael@0 3669 try {
michael@0 3670 // The receipt data shouldn't be too big (allow up to 1 MiB of data)
michael@0 3671 const MAX_RECEIPT_SIZE = 1048576;
michael@0 3672
michael@0 3673 if (data.length > MAX_RECEIPT_SIZE) {
michael@0 3674 return "RECEIPT_TOO_BIG";
michael@0 3675 }
michael@0 3676
michael@0 3677 // Marketplace receipts are JWK + "~" + JWT
michael@0 3678 // Other receipts may contain only the JWT
michael@0 3679 let receiptParts = data.split('~');
michael@0 3680 let jwtData = null;
michael@0 3681 if (receiptParts.length == 2) {
michael@0 3682 jwtData = receiptParts[1];
michael@0 3683 } else {
michael@0 3684 jwtData = receiptParts[0];
michael@0 3685 }
michael@0 3686
michael@0 3687 let segments = jwtData.split('.');
michael@0 3688 if (segments.length != 3) {
michael@0 3689 return "INVALID_SEGMENTS_NUMBER";
michael@0 3690 }
michael@0 3691
michael@0 3692 // We need to translate the base64 alphabet used in JWT to our base64 alphabet
michael@0 3693 // before calling atob.
michael@0 3694 let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+')
michael@0 3695 .replace(/_/g, '/')));
michael@0 3696 if (!decodedReceipt) {
michael@0 3697 return "INVALID_RECEIPT_ENCODING";
michael@0 3698 }
michael@0 3699
michael@0 3700 // Required values for a receipt
michael@0 3701 if (!decodedReceipt.typ) {
michael@0 3702 return "RECEIPT_TYPE_REQUIRED";
michael@0 3703 }
michael@0 3704 if (!decodedReceipt.product) {
michael@0 3705 return "RECEIPT_PRODUCT_REQUIRED";
michael@0 3706 }
michael@0 3707 if (!decodedReceipt.user) {
michael@0 3708 return "RECEIPT_USER_REQUIRED";
michael@0 3709 }
michael@0 3710 if (!decodedReceipt.iss) {
michael@0 3711 return "RECEIPT_ISS_REQUIRED";
michael@0 3712 }
michael@0 3713 if (!decodedReceipt.nbf) {
michael@0 3714 return "RECEIPT_NBF_REQUIRED";
michael@0 3715 }
michael@0 3716 if (!decodedReceipt.iat) {
michael@0 3717 return "RECEIPT_IAT_REQUIRED";
michael@0 3718 }
michael@0 3719
michael@0 3720 let allowedTypes = [ "purchase-receipt", "developer-receipt",
michael@0 3721 "reviewer-receipt", "test-receipt" ];
michael@0 3722 if (allowedTypes.indexOf(decodedReceipt.typ) < 0) {
michael@0 3723 return "RECEIPT_TYPE_UNSUPPORTED";
michael@0 3724 }
michael@0 3725 } catch (e) {
michael@0 3726 return "RECEIPT_ERROR";
michael@0 3727 }
michael@0 3728
michael@0 3729 return null;
michael@0 3730 },
michael@0 3731
michael@0 3732 addReceipt: function(aData, aMm) {
michael@0 3733 debug("addReceipt " + aData.manifestURL);
michael@0 3734
michael@0 3735 let receipt = aData.receipt;
michael@0 3736
michael@0 3737 if (!receipt) {
michael@0 3738 aData.error = "INVALID_PARAMETERS";
michael@0 3739 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
michael@0 3740 return;
michael@0 3741 }
michael@0 3742
michael@0 3743 let error = this.isReceipt(receipt);
michael@0 3744 if (error) {
michael@0 3745 aData.error = error;
michael@0 3746 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
michael@0 3747 return;
michael@0 3748 }
michael@0 3749
michael@0 3750 let id = this._appIdForManifestURL(aData.manifestURL);
michael@0 3751 let app = this.webapps[id];
michael@0 3752
michael@0 3753 if (!app.receipts) {
michael@0 3754 app.receipts = [];
michael@0 3755 } else if (app.receipts.length > 500) {
michael@0 3756 aData.error = "TOO_MANY_RECEIPTS";
michael@0 3757 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
michael@0 3758 return;
michael@0 3759 }
michael@0 3760
michael@0 3761 let index = app.receipts.indexOf(receipt);
michael@0 3762 if (index >= 0) {
michael@0 3763 aData.error = "RECEIPT_ALREADY_EXISTS";
michael@0 3764 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
michael@0 3765 return;
michael@0 3766 }
michael@0 3767
michael@0 3768 app.receipts.push(receipt);
michael@0 3769
michael@0 3770 this._saveApps().then(() => {
michael@0 3771 aData.receipts = app.receipts;
michael@0 3772 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData);
michael@0 3773 });
michael@0 3774 },
michael@0 3775
michael@0 3776 removeReceipt: function(aData, aMm) {
michael@0 3777 debug("removeReceipt " + aData.manifestURL);
michael@0 3778
michael@0 3779 let receipt = aData.receipt;
michael@0 3780
michael@0 3781 if (!receipt) {
michael@0 3782 aData.error = "INVALID_PARAMETERS";
michael@0 3783 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
michael@0 3784 return;
michael@0 3785 }
michael@0 3786
michael@0 3787 let id = this._appIdForManifestURL(aData.manifestURL);
michael@0 3788 let app = this.webapps[id];
michael@0 3789
michael@0 3790 if (!app.receipts) {
michael@0 3791 aData.error = "NO_SUCH_RECEIPT";
michael@0 3792 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
michael@0 3793 return;
michael@0 3794 }
michael@0 3795
michael@0 3796 let index = app.receipts.indexOf(receipt);
michael@0 3797 if (index == -1) {
michael@0 3798 aData.error = "NO_SUCH_RECEIPT";
michael@0 3799 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
michael@0 3800 return;
michael@0 3801 }
michael@0 3802
michael@0 3803 app.receipts.splice(index, 1);
michael@0 3804
michael@0 3805 this._saveApps().then(() => {
michael@0 3806 aData.receipts = app.receipts;
michael@0 3807 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData);
michael@0 3808 });
michael@0 3809 },
michael@0 3810
michael@0 3811 replaceReceipt: function(aData, aMm) {
michael@0 3812 debug("replaceReceipt " + aData.manifestURL);
michael@0 3813
michael@0 3814 let oldReceipt = aData.oldReceipt;
michael@0 3815 let newReceipt = aData.newReceipt;
michael@0 3816
michael@0 3817 if (!oldReceipt || !newReceipt) {
michael@0 3818 aData.error = "INVALID_PARAMETERS";
michael@0 3819 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
michael@0 3820 return;
michael@0 3821 }
michael@0 3822
michael@0 3823 let error = this.isReceipt(newReceipt);
michael@0 3824 if (error) {
michael@0 3825 aData.error = error;
michael@0 3826 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
michael@0 3827 return;
michael@0 3828 }
michael@0 3829
michael@0 3830 let id = this._appIdForManifestURL(aData.manifestURL);
michael@0 3831 let app = this.webapps[id];
michael@0 3832
michael@0 3833 if (!app.receipts) {
michael@0 3834 aData.error = "NO_SUCH_RECEIPT";
michael@0 3835 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
michael@0 3836 return;
michael@0 3837 }
michael@0 3838
michael@0 3839 let oldIndex = app.receipts.indexOf(oldReceipt);
michael@0 3840 if (oldIndex == -1) {
michael@0 3841 aData.error = "NO_SUCH_RECEIPT";
michael@0 3842 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
michael@0 3843 return;
michael@0 3844 }
michael@0 3845
michael@0 3846 app.receipts[oldIndex] = newReceipt;
michael@0 3847
michael@0 3848 this._saveApps().then(() => {
michael@0 3849 aData.receipts = app.receipts;
michael@0 3850 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData);
michael@0 3851 });
michael@0 3852 },
michael@0 3853
michael@0 3854 getManifestFor: function(aManifestURL) {
michael@0 3855 let id = this._appIdForManifestURL(aManifestURL);
michael@0 3856 let app = this.webapps[id];
michael@0 3857 if (!id || (app.installState == "pending" && !app.retryingDownload)) {
michael@0 3858 return Promise.resolve(null);
michael@0 3859 }
michael@0 3860
michael@0 3861 return this._readManifests([{ id: id }]).then((aResult) => {
michael@0 3862 return aResult[0].manifest;
michael@0 3863 });
michael@0 3864 },
michael@0 3865
michael@0 3866 getAppByManifestURL: function(aManifestURL) {
michael@0 3867 return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
michael@0 3868 },
michael@0 3869
michael@0 3870 getCSPByLocalId: function(aLocalId) {
michael@0 3871 debug("getCSPByLocalId:" + aLocalId);
michael@0 3872 return AppsUtils.getCSPByLocalId(this.webapps, aLocalId);
michael@0 3873 },
michael@0 3874
michael@0 3875 getAppLocalIdByStoreId: function(aStoreId) {
michael@0 3876 debug("getAppLocalIdByStoreId:" + aStoreId);
michael@0 3877 return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
michael@0 3878 },
michael@0 3879
michael@0 3880 getAppByLocalId: function(aLocalId) {
michael@0 3881 return AppsUtils.getAppByLocalId(this.webapps, aLocalId);
michael@0 3882 },
michael@0 3883
michael@0 3884 getManifestURLByLocalId: function(aLocalId) {
michael@0 3885 return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
michael@0 3886 },
michael@0 3887
michael@0 3888 getAppLocalIdByManifestURL: function(aManifestURL) {
michael@0 3889 return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
michael@0 3890 },
michael@0 3891
michael@0 3892 getCoreAppsBasePath: function() {
michael@0 3893 return AppsUtils.getCoreAppsBasePath();
michael@0 3894 },
michael@0 3895
michael@0 3896 getWebAppsBasePath: function() {
michael@0 3897 return OS.Path.dirname(this.appsFile);
michael@0 3898 },
michael@0 3899
michael@0 3900 _isLaunchable: function(aApp) {
michael@0 3901 if (this.allAppsLaunchable)
michael@0 3902 return true;
michael@0 3903
michael@0 3904 return WebappOSUtils.isLaunchable(aApp);
michael@0 3905 },
michael@0 3906
michael@0 3907 _notifyCategoryAndObservers: function(subject, topic, data, msg) {
michael@0 3908 const serviceMarker = "service,";
michael@0 3909
michael@0 3910 // First create observers from the category manager.
michael@0 3911 let cm =
michael@0 3912 Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
michael@0 3913 let enumerator = cm.enumerateCategory(topic);
michael@0 3914
michael@0 3915 let observers = [];
michael@0 3916
michael@0 3917 while (enumerator.hasMoreElements()) {
michael@0 3918 let entry =
michael@0 3919 enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
michael@0 3920 let contractID = cm.getCategoryEntry(topic, entry);
michael@0 3921
michael@0 3922 let factoryFunction;
michael@0 3923 if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
michael@0 3924 contractID = contractID.substring(serviceMarker.length);
michael@0 3925 factoryFunction = "getService";
michael@0 3926 }
michael@0 3927 else {
michael@0 3928 factoryFunction = "createInstance";
michael@0 3929 }
michael@0 3930
michael@0 3931 try {
michael@0 3932 let handler = Cc[contractID][factoryFunction]();
michael@0 3933 if (handler) {
michael@0 3934 let observer = handler.QueryInterface(Ci.nsIObserver);
michael@0 3935 observers.push(observer);
michael@0 3936 }
michael@0 3937 } catch(e) { }
michael@0 3938 }
michael@0 3939
michael@0 3940 // Next enumerate the registered observers.
michael@0 3941 enumerator = Services.obs.enumerateObservers(topic);
michael@0 3942 while (enumerator.hasMoreElements()) {
michael@0 3943 try {
michael@0 3944 let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver);
michael@0 3945 if (observers.indexOf(observer) == -1) {
michael@0 3946 observers.push(observer);
michael@0 3947 }
michael@0 3948 } catch (e) { }
michael@0 3949 }
michael@0 3950
michael@0 3951 observers.forEach(function (observer) {
michael@0 3952 try {
michael@0 3953 observer.observe(subject, topic, data);
michael@0 3954 } catch(e) { }
michael@0 3955 });
michael@0 3956 // Send back an answer to the child.
michael@0 3957 if (msg) {
michael@0 3958 ppmm.broadcastAsyncMessage("Webapps:ClearBrowserData:Return", msg);
michael@0 3959 }
michael@0 3960 },
michael@0 3961
michael@0 3962 registerBrowserElementParentForApp: function(bep, appId) {
michael@0 3963 let mm = bep._mm;
michael@0 3964
michael@0 3965 // Make a listener function that holds on to this appId.
michael@0 3966 let listener = this.receiveAppMessage.bind(this, appId);
michael@0 3967
michael@0 3968 this.frameMessages.forEach(function(msgName) {
michael@0 3969 mm.addMessageListener(msgName, listener);
michael@0 3970 });
michael@0 3971 },
michael@0 3972
michael@0 3973 receiveAppMessage: function(appId, message) {
michael@0 3974 switch (message.name) {
michael@0 3975 case "Webapps:ClearBrowserData":
michael@0 3976 this._clearPrivateData(appId, true, message.data);
michael@0 3977 break;
michael@0 3978 }
michael@0 3979 },
michael@0 3980
michael@0 3981 _clearPrivateData: function(appId, browserOnly, msg) {
michael@0 3982 let subject = {
michael@0 3983 appId: appId,
michael@0 3984 browserOnly: browserOnly,
michael@0 3985 QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
michael@0 3986 };
michael@0 3987 this._notifyCategoryAndObservers(subject, "webapps-clear-data", null, msg);
michael@0 3988 }
michael@0 3989 };
michael@0 3990
michael@0 3991 /**
michael@0 3992 * Appcache download observer
michael@0 3993 */
michael@0 3994 let AppcacheObserver = function(aApp) {
michael@0 3995 debug("Creating AppcacheObserver for " + aApp.origin +
michael@0 3996 " - " + aApp.installState);
michael@0 3997 this.app = aApp;
michael@0 3998 this.startStatus = aApp.installState;
michael@0 3999 this.lastProgressTime = 0;
michael@0 4000 // Send a first progress event to correctly set the DOM object's properties.
michael@0 4001 this._sendProgressEvent();
michael@0 4002 };
michael@0 4003
michael@0 4004 AppcacheObserver.prototype = {
michael@0 4005 // nsIOfflineCacheUpdateObserver implementation
michael@0 4006 _sendProgressEvent: function() {
michael@0 4007 let app = this.app;
michael@0 4008 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
michael@0 4009 app: app,
michael@0 4010 manifestURL: app.manifestURL
michael@0 4011 });
michael@0 4012 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
michael@0 4013 eventType: "progress",
michael@0 4014 manifestURL: app.manifestURL
michael@0 4015 });
michael@0 4016 },
michael@0 4017
michael@0 4018 updateStateChanged: function appObs_Update(aUpdate, aState) {
michael@0 4019 let mustSave = false;
michael@0 4020 let app = this.app;
michael@0 4021
michael@0 4022 debug("Offline cache state change for " + app.origin + " : " + aState);
michael@0 4023
michael@0 4024 var self = this;
michael@0 4025 let setStatus = function appObs_setStatus(aStatus, aProgress) {
michael@0 4026 debug("Offlinecache setStatus to " + aStatus + " with progress " +
michael@0 4027 aProgress + " for " + app.origin);
michael@0 4028 mustSave = (app.installState != aStatus);
michael@0 4029
michael@0 4030 app.installState = aStatus;
michael@0 4031 app.progress = aProgress;
michael@0 4032 if (aStatus != "installed") {
michael@0 4033 self._sendProgressEvent();
michael@0 4034 return;
michael@0 4035 }
michael@0 4036
michael@0 4037 app.updateTime = Date.now();
michael@0 4038 app.downloading = false;
michael@0 4039 app.downloadAvailable = false;
michael@0 4040 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
michael@0 4041 app: app,
michael@0 4042 manifestURL: app.manifestURL
michael@0 4043 });
michael@0 4044 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
michael@0 4045 eventType: ["downloadsuccess", "downloadapplied"],
michael@0 4046 manifestURL: app.manifestURL
michael@0 4047 });
michael@0 4048 }
michael@0 4049
michael@0 4050 let setError = function appObs_setError(aError) {
michael@0 4051 debug("Offlinecache setError to " + aError);
michael@0 4052 app.downloading = false;
michael@0 4053 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
michael@0 4054 app: app,
michael@0 4055 manifestURL: app.manifestURL
michael@0 4056 });
michael@0 4057 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
michael@0 4058 error: aError,
michael@0 4059 eventType: "downloaderror",
michael@0 4060 manifestURL: app.manifestURL
michael@0 4061 });
michael@0 4062 mustSave = true;
michael@0 4063 }
michael@0 4064
michael@0 4065 switch (aState) {
michael@0 4066 case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR:
michael@0 4067 aUpdate.removeObserver(this);
michael@0 4068 AppDownloadManager.remove(app.manifestURL);
michael@0 4069 setError("APP_CACHE_DOWNLOAD_ERROR");
michael@0 4070 break;
michael@0 4071 case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE:
michael@0 4072 case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED:
michael@0 4073 aUpdate.removeObserver(this);
michael@0 4074 AppDownloadManager.remove(app.manifestURL);
michael@0 4075 setStatus("installed", aUpdate.byteProgress);
michael@0 4076 break;
michael@0 4077 case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING:
michael@0 4078 setStatus(this.startStatus, aUpdate.byteProgress);
michael@0 4079 break;
michael@0 4080 case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED:
michael@0 4081 case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS:
michael@0 4082 let now = Date.now();
michael@0 4083 if (now - this.lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
michael@0 4084 setStatus(this.startStatus, aUpdate.byteProgress);
michael@0 4085 this.lastProgressTime = now;
michael@0 4086 }
michael@0 4087 break;
michael@0 4088 }
michael@0 4089
michael@0 4090 // Status changed, update the stored version.
michael@0 4091 if (mustSave) {
michael@0 4092 DOMApplicationRegistry._saveApps();
michael@0 4093 }
michael@0 4094 },
michael@0 4095
michael@0 4096 applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) {
michael@0 4097 // Nothing to do.
michael@0 4098 }
michael@0 4099 };
michael@0 4100
michael@0 4101 DOMApplicationRegistry.init();

mercurial