dom/apps/src/Webapps.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/apps/src/Webapps.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,4101 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +const Cu = Components.utils;
    1.11 +const Cc = Components.classes;
    1.12 +const Ci = Components.interfaces;
    1.13 +const Cr = Components.results;
    1.14 +
    1.15 +// Possible errors thrown by the signature verifier.
    1.16 +const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
    1.17 +const SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11);
    1.18 +
    1.19 +// We need this to decide if we should accept or not files signed with expired
    1.20 +// certificates.
    1.21 +function buildIDToTime() {
    1.22 +  let platformBuildID =
    1.23 +    Cc["@mozilla.org/xre/app-info;1"]
    1.24 +      .getService(Ci.nsIXULAppInfo).platformBuildID;
    1.25 +  let platformBuildIDDate = new Date();
    1.26 +  platformBuildIDDate.setUTCFullYear(platformBuildID.substr(0,4),
    1.27 +                                      platformBuildID.substr(4,2) - 1,
    1.28 +                                      platformBuildID.substr(6,2));
    1.29 +  platformBuildIDDate.setUTCHours(platformBuildID.substr(8,2),
    1.30 +                                  platformBuildID.substr(10,2),
    1.31 +                                  platformBuildID.substr(12,2));
    1.32 +  return platformBuildIDDate.getTime();
    1.33 +}
    1.34 +
    1.35 +const PLATFORM_BUILD_ID_TIME = buildIDToTime();
    1.36 +
    1.37 +this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
    1.38 +
    1.39 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.40 +Cu.import("resource://gre/modules/Services.jsm");
    1.41 +Cu.import("resource://gre/modules/FileUtils.jsm");
    1.42 +Cu.import('resource://gre/modules/ActivitiesService.jsm');
    1.43 +Cu.import("resource://gre/modules/AppsUtils.jsm");
    1.44 +Cu.import("resource://gre/modules/AppDownloadManager.jsm");
    1.45 +Cu.import("resource://gre/modules/osfile.jsm");
    1.46 +Cu.import("resource://gre/modules/Task.jsm");
    1.47 +Cu.import("resource://gre/modules/Promise.jsm");
    1.48 +
    1.49 +XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate",
    1.50 +  "resource://gre/modules/StoreTrustAnchor.jsm");
    1.51 +
    1.52 +XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
    1.53 +  "resource://gre/modules/PermissionsInstaller.jsm");
    1.54 +
    1.55 +XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller",
    1.56 +  "resource://gre/modules/OfflineCacheInstaller.jsm");
    1.57 +
    1.58 +XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker",
    1.59 +  "resource://gre/modules/SystemMessagePermissionsChecker.jsm");
    1.60 +
    1.61 +XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
    1.62 +  "resource://gre/modules/WebappOSUtils.jsm");
    1.63 +
    1.64 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.65 +  "resource://gre/modules/NetUtil.jsm");
    1.66 +
    1.67 +XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
    1.68 +                                  "resource://gre/modules/ScriptPreloader.jsm");
    1.69 +
    1.70 +#ifdef MOZ_WIDGET_GONK
    1.71 +XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
    1.72 +  Cu.import("resource://gre/modules/systemlibs.js");
    1.73 +  return libcutils;
    1.74 +});
    1.75 +#endif
    1.76 +
    1.77 +function debug(aMsg) {
    1.78 +#ifdef DEBUG
    1.79 +  dump("-*- Webapps.jsm : " + aMsg + "\n");
    1.80 +#endif
    1.81 +}
    1.82 +
    1.83 +function getNSPRErrorCode(err) {
    1.84 +  return -1 * ((err) & 0xffff);
    1.85 +}
    1.86 +
    1.87 +function supportUseCurrentProfile() {
    1.88 +  return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile");
    1.89 +}
    1.90 +
    1.91 +function supportSystemMessages() {
    1.92 +  return Services.prefs.getBoolPref("dom.sysmsg.enabled");
    1.93 +}
    1.94 +
    1.95 +// Minimum delay between two progress events while downloading, in ms.
    1.96 +const MIN_PROGRESS_EVENT_DELAY = 1500;
    1.97 +
    1.98 +const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
    1.99 +
   1.100 +const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser";
   1.101 +
   1.102 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
   1.103 +                                   "@mozilla.org/parentprocessmessagemanager;1",
   1.104 +                                   "nsIMessageBroadcaster");
   1.105 +
   1.106 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
   1.107 +                                   "@mozilla.org/childprocessmessagemanager;1",
   1.108 +                                   "nsIMessageSender");
   1.109 +
   1.110 +XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() {
   1.111 +  return Cc["@mozilla.org/inter-app-communication-service;1"]
   1.112 +         .getService(Ci.nsIInterAppCommService);
   1.113 +});
   1.114 +
   1.115 +XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService",
   1.116 +                                   "@mozilla.org/datastore-service;1",
   1.117 +                                   "nsIDataStoreService");
   1.118 +
   1.119 +XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
   1.120 +  return Cc["@mozilla.org/system-message-internal;1"]
   1.121 +         .getService(Ci.nsISystemMessagesInternal);
   1.122 +});
   1.123 +
   1.124 +XPCOMUtils.defineLazyGetter(this, "updateSvc", function() {
   1.125 +  return Cc["@mozilla.org/offlinecacheupdate-service;1"]
   1.126 +           .getService(Ci.nsIOfflineCacheUpdateService);
   1.127 +});
   1.128 +
   1.129 +#ifdef MOZ_WIDGET_GONK
   1.130 +  const DIRECTORY_NAME = "webappsDir";
   1.131 +#elifdef ANDROID
   1.132 +  const DIRECTORY_NAME = "webappsDir";
   1.133 +#else
   1.134 +  // If we're executing in the context of the webapp runtime, the data files
   1.135 +  // are in a different directory (currently the Firefox profile that installed
   1.136 +  // the webapp); otherwise, they're in the current profile.
   1.137 +  const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD";
   1.138 +#endif
   1.139 +
   1.140 +// We'll use this to identify privileged apps that have been preinstalled
   1.141 +// For those apps we'll set
   1.142 +// STORE_ID_PENDING_PREFIX + installOrigin
   1.143 +// as the storeID. This ensures it's unique and can't be set from a legit
   1.144 +// store even by error.
   1.145 +const STORE_ID_PENDING_PREFIX = "#unknownID#";
   1.146 +
   1.147 +this.DOMApplicationRegistry = {
   1.148 +  // Path to the webapps.json file where we store the registry data.
   1.149 +  appsFile: null,
   1.150 +  webapps: { },
   1.151 +  children: [ ],
   1.152 +  allAppsLaunchable: false,
   1.153 +  _updateHandlers: [ ],
   1.154 +
   1.155 +  init: function() {
   1.156 +    this.messages = ["Webapps:Install", "Webapps:Uninstall",
   1.157 +                     "Webapps:GetSelf", "Webapps:CheckInstalled",
   1.158 +                     "Webapps:GetInstalled", "Webapps:GetNotInstalled",
   1.159 +                     "Webapps:Launch", "Webapps:GetAll",
   1.160 +                     "Webapps:InstallPackage",
   1.161 +                     "Webapps:GetList", "Webapps:RegisterForMessages",
   1.162 +                     "Webapps:UnregisterForMessages",
   1.163 +                     "Webapps:CancelDownload", "Webapps:CheckForUpdate",
   1.164 +                     "Webapps:Download", "Webapps:ApplyDownload",
   1.165 +                     "Webapps:Install:Return:Ack", "Webapps:AddReceipt",
   1.166 +                     "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt",
   1.167 +                     "child-process-shutdown"];
   1.168 +
   1.169 +    this.frameMessages = ["Webapps:ClearBrowserData"];
   1.170 +
   1.171 +    this.messages.forEach((function(msgName) {
   1.172 +      ppmm.addMessageListener(msgName, this);
   1.173 +    }).bind(this));
   1.174 +
   1.175 +    cpmm.addMessageListener("Activities:Register:OK", this);
   1.176 +
   1.177 +    Services.obs.addObserver(this, "xpcom-shutdown", false);
   1.178 +    Services.obs.addObserver(this, "memory-pressure", false);
   1.179 +
   1.180 +    AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
   1.181 +
   1.182 +    this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
   1.183 +                                      ["webapps", "webapps.json"], true).path;
   1.184 +
   1.185 +    this.loadAndUpdateApps();
   1.186 +  },
   1.187 +
   1.188 +  // loads the current registry, that could be empty on first run.
   1.189 +  loadCurrentRegistry: function() {
   1.190 +    return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => {
   1.191 +      if (!aData) {
   1.192 +        return;
   1.193 +      }
   1.194 +
   1.195 +      this.webapps = aData;
   1.196 +      let appDir = OS.Path.dirname(this.appsFile);
   1.197 +      for (let id in this.webapps) {
   1.198 +        let app = this.webapps[id];
   1.199 +        if (!app) {
   1.200 +          delete this.webapps[id];
   1.201 +          continue;
   1.202 +        }
   1.203 +
   1.204 +        app.id = id;
   1.205 +
   1.206 +        // Make sure we have a localId
   1.207 +        if (app.localId === undefined) {
   1.208 +          app.localId = this._nextLocalId();
   1.209 +        }
   1.210 +
   1.211 +        if (app.basePath === undefined) {
   1.212 +          app.basePath = appDir;
   1.213 +        }
   1.214 +
   1.215 +        // Default to removable apps.
   1.216 +        if (app.removable === undefined) {
   1.217 +          app.removable = true;
   1.218 +        }
   1.219 +
   1.220 +        // Default to a non privileged status.
   1.221 +        if (app.appStatus === undefined) {
   1.222 +          app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
   1.223 +        }
   1.224 +
   1.225 +        // Default to NO_APP_ID and not in browser.
   1.226 +        if (app.installerAppId === undefined) {
   1.227 +          app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
   1.228 +        }
   1.229 +        if (app.installerIsBrowser === undefined) {
   1.230 +          app.installerIsBrowser = false;
   1.231 +        }
   1.232 +
   1.233 +        // Default installState to "installed", and reset if we shutdown
   1.234 +        // during an update.
   1.235 +        if (app.installState === undefined ||
   1.236 +            app.installState === "updating") {
   1.237 +          app.installState = "installed";
   1.238 +        }
   1.239 +
   1.240 +        // Default storeId to "" and storeVersion to 0
   1.241 +        if (this.webapps[id].storeId === undefined) {
   1.242 +          this.webapps[id].storeId = "";
   1.243 +        }
   1.244 +        if (this.webapps[id].storeVersion === undefined) {
   1.245 +          this.webapps[id].storeVersion = 0;
   1.246 +        }
   1.247 +
   1.248 +        // Default role to "".
   1.249 +        if (this.webapps[id].role === undefined) {
   1.250 +          this.webapps[id].role = "";
   1.251 +        }
   1.252 +
   1.253 +        // At startup we can't be downloading, and the $TMP directory
   1.254 +        // will be empty so we can't just apply a staged update.
   1.255 +        app.downloading = false;
   1.256 +        app.readyToApplyDownload = false;
   1.257 +      }
   1.258 +    });
   1.259 +  },
   1.260 +
   1.261 +  // Notify we are starting with registering apps.
   1.262 +  _registryStarted: Promise.defer(),
   1.263 +  notifyAppsRegistryStart: function notifyAppsRegistryStart() {
   1.264 +    Services.obs.notifyObservers(this, "webapps-registry-start", null);
   1.265 +    this._registryStarted.resolve();
   1.266 +  },
   1.267 +
   1.268 +  get registryStarted() {
   1.269 +    return this._registryStarted.promise;
   1.270 +  },
   1.271 +
   1.272 +  // Notify we are done with registering apps and save a copy of the registry.
   1.273 +  _registryReady: Promise.defer(),
   1.274 +  notifyAppsRegistryReady: function notifyAppsRegistryReady() {
   1.275 +    this._registryReady.resolve();
   1.276 +    Services.obs.notifyObservers(this, "webapps-registry-ready", null);
   1.277 +    this._saveApps();
   1.278 +  },
   1.279 +
   1.280 +  get registryReady() {
   1.281 +    return this._registryReady.promise;
   1.282 +  },
   1.283 +
   1.284 +  // Ensure that the .to property in redirects is a relative URL.
   1.285 +  sanitizeRedirects: function sanitizeRedirects(aSource) {
   1.286 +    if (!aSource) {
   1.287 +      return null;
   1.288 +    }
   1.289 +
   1.290 +    let res = [];
   1.291 +    for (let i = 0; i < aSource.length; i++) {
   1.292 +      let redirect = aSource[i];
   1.293 +      if (redirect.from && redirect.to &&
   1.294 +          isAbsoluteURI(redirect.from) &&
   1.295 +          !isAbsoluteURI(redirect.to)) {
   1.296 +        res.push(redirect);
   1.297 +      }
   1.298 +    }
   1.299 +    return res.length > 0 ? res : null;
   1.300 +  },
   1.301 +
   1.302 +  // Registers all the activities and system messages.
   1.303 +  registerAppsHandlers: function(aRunUpdate) {
   1.304 +    this.notifyAppsRegistryStart();
   1.305 +    let ids = [];
   1.306 +    for (let id in this.webapps) {
   1.307 +      ids.push({ id: id });
   1.308 +    }
   1.309 +    if (supportSystemMessages()) {
   1.310 +      this._processManifestForIds(ids, aRunUpdate);
   1.311 +    } else {
   1.312 +      // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on
   1.313 +      // _processManifestForIds so as to not reading the manifests
   1.314 +      // twice
   1.315 +      this._readManifests(ids).then((aResults) => {
   1.316 +        aResults.forEach((aResult) => {
   1.317 +          if (!aResult.manifest) {
   1.318 +            // If we can't load the manifest, we probably have a corrupted
   1.319 +            // registry. We delete the app since we can't do anything with it.
   1.320 +            delete this.webapps[aResult.id];
   1.321 +            return;
   1.322 +          }
   1.323 +          let app = this.webapps[aResult.id];
   1.324 +          app.csp = aResult.manifest.csp || "";
   1.325 +          app.role = aResult.manifest.role || "";
   1.326 +          if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
   1.327 +            app.redirects = this.sanitizeRedirects(aResult.redirects);
   1.328 +          }
   1.329 +        });
   1.330 +      });
   1.331 +
   1.332 +      // Nothing else to do but notifying we're ready.
   1.333 +      this.notifyAppsRegistryReady();
   1.334 +    }
   1.335 +  },
   1.336 +
   1.337 +  updateDataStoreForApp: function(aId) {
   1.338 +    if (!this.webapps[aId]) {
   1.339 +      return;
   1.340 +    }
   1.341 +
   1.342 +    // Create or Update the DataStore for this app
   1.343 +    this._readManifests([{ id: aId }]).then((aResult) => {
   1.344 +      let app = this.webapps[aId];
   1.345 +      this.updateDataStore(app.localId, app.origin, app.manifestURL,
   1.346 +                           aResult[0].manifest, app.appStatus);
   1.347 +    });
   1.348 +  },
   1.349 +
   1.350 +  updatePermissionsForApp: function(aId) {
   1.351 +    if (!this.webapps[aId]) {
   1.352 +      return;
   1.353 +    }
   1.354 +
   1.355 +    // Install the permissions for this app, as if we were updating
   1.356 +    // to cleanup the old ones if needed.
   1.357 +    // TODO It's not clear what this should do when there are multiple profiles.
   1.358 +    if (supportUseCurrentProfile()) {
   1.359 +      this._readManifests([{ id: aId }]).then((aResult) => {
   1.360 +        let data = aResult[0];
   1.361 +        PermissionsInstaller.installPermissions({
   1.362 +          manifest: data.manifest,
   1.363 +          manifestURL: this.webapps[aId].manifestURL,
   1.364 +          origin: this.webapps[aId].origin
   1.365 +        }, true, function() {
   1.366 +          debug("Error installing permissions for " + aId);
   1.367 +        });
   1.368 +      });
   1.369 +    }
   1.370 +  },
   1.371 +
   1.372 +  updateOfflineCacheForApp: function(aId) {
   1.373 +    let app = this.webapps[aId];
   1.374 +    this._readManifests([{ id: aId }]).then((aResult) => {
   1.375 +      let manifest = new ManifestHelper(aResult[0].manifest, app.origin);
   1.376 +      OfflineCacheInstaller.installCache({
   1.377 +        cachePath: app.cachePath,
   1.378 +        appId: aId,
   1.379 +        origin: Services.io.newURI(app.origin, null, null),
   1.380 +        localId: app.localId,
   1.381 +        appcache_path: manifest.fullAppcachePath()
   1.382 +      });
   1.383 +    });
   1.384 +  },
   1.385 +
   1.386 +  // Installs a 3rd party app.
   1.387 +  installPreinstalledApp: function installPreinstalledApp(aId) {
   1.388 +#ifdef MOZ_WIDGET_GONK
   1.389 +    let app = this.webapps[aId];
   1.390 +    let baseDir;
   1.391 +    try {
   1.392 +      baseDir = FileUtils.getDir("coreAppsDir", ["webapps", aId], false);
   1.393 +      if (!baseDir.exists()) {
   1.394 +        return;
   1.395 +      } else if (!baseDir.directoryEntries.hasMoreElements()) {
   1.396 +        debug("Error: Core app in " + baseDir.path + " is empty");
   1.397 +        return;
   1.398 +      }
   1.399 +    } catch(e) {
   1.400 +      // In ENG builds, we don't have apps in coreAppsDir.
   1.401 +      return;
   1.402 +    }
   1.403 +
   1.404 +    let filesToMove;
   1.405 +    let isPackage;
   1.406 +
   1.407 +    let updateFile = baseDir.clone();
   1.408 +    updateFile.append("update.webapp");
   1.409 +    if (!updateFile.exists()) {
   1.410 +      // The update manifest is missing, this is a hosted app only if there is
   1.411 +      // no application.zip
   1.412 +      let appFile = baseDir.clone();
   1.413 +      appFile.append("application.zip");
   1.414 +      if (appFile.exists()) {
   1.415 +        return;
   1.416 +      }
   1.417 +
   1.418 +      isPackage = false;
   1.419 +      filesToMove = ["manifest.webapp"];
   1.420 +    } else {
   1.421 +      isPackage = true;
   1.422 +      filesToMove = ["application.zip", "update.webapp"];
   1.423 +    }
   1.424 +
   1.425 +    debug("Installing 3rd party app : " + aId +
   1.426 +          " from " + baseDir.path);
   1.427 +
   1.428 +    // We copy this app to DIRECTORY_NAME/$aId, and set the base path as needed.
   1.429 +    let destDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
   1.430 +
   1.431 +    filesToMove.forEach(function(aFile) {
   1.432 +        let file = baseDir.clone();
   1.433 +        file.append(aFile);
   1.434 +        try {
   1.435 +          file.copyTo(destDir, aFile);
   1.436 +        } catch(e) {
   1.437 +          debug("Error: Failed to copy " + file.path + " to " + destDir.path);
   1.438 +        }
   1.439 +      });
   1.440 +
   1.441 +    app.installState = "installed";
   1.442 +    app.cachePath = app.basePath;
   1.443 +    app.basePath = OS.Path.dirname(this.appsFile);
   1.444 +
   1.445 +    if (!isPackage) {
   1.446 +      return;
   1.447 +    }
   1.448 +
   1.449 +    app.origin = "app://" + aId;
   1.450 +
   1.451 +    // Do this for all preinstalled apps... we can't know at this
   1.452 +    // point if the updates will be signed or not and it doesn't
   1.453 +    // hurt to have it always.
   1.454 +    app.storeId = STORE_ID_PENDING_PREFIX + app.installOrigin;
   1.455 +
   1.456 +    // Extract the manifest.webapp file from application.zip.
   1.457 +    let zipFile = baseDir.clone();
   1.458 +    zipFile.append("application.zip");
   1.459 +    let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
   1.460 +                      .createInstance(Ci.nsIZipReader);
   1.461 +    try {
   1.462 +      debug("Opening " + zipFile.path);
   1.463 +      zipReader.open(zipFile);
   1.464 +      if (!zipReader.hasEntry("manifest.webapp")) {
   1.465 +        throw "MISSING_MANIFEST";
   1.466 +      }
   1.467 +      let manifestFile = destDir.clone();
   1.468 +      manifestFile.append("manifest.webapp");
   1.469 +      zipReader.extract("manifest.webapp", manifestFile);
   1.470 +    } catch(e) {
   1.471 +      // If we are unable to extract the manifest, cleanup and remove this app.
   1.472 +      debug("Cleaning up: " + e);
   1.473 +      destDir.remove(true);
   1.474 +      delete this.webapps[aId];
   1.475 +    } finally {
   1.476 +      zipReader.close();
   1.477 +    }
   1.478 +#endif
   1.479 +  },
   1.480 +
   1.481 +  // For hosted apps, uninstall an app served from http:// if we have
   1.482 +  // one installed from the same url with an https:// scheme.
   1.483 +  removeIfHttpsDuplicate: function(aId) {
   1.484 +#ifdef MOZ_WIDGET_GONK
   1.485 +    let app = this.webapps[aId];
   1.486 +    if (!app || !app.origin.startsWith("http://")) {
   1.487 +      return;
   1.488 +    }
   1.489 +
   1.490 +    let httpsManifestURL =
   1.491 +      "https://" + app.manifestURL.substring("http://".length);
   1.492 +
   1.493 +    // This will uninstall the http apps and remove any data hold by this
   1.494 +    // app. Bug 948105 tracks data migration from http to https apps.
   1.495 +    for (let id in this.webapps) {
   1.496 +       if (this.webapps[id].manifestURL === httpsManifestURL) {
   1.497 +         debug("Found a http/https match: " + app.manifestURL + " / " +
   1.498 +               this.webapps[id].manifestURL);
   1.499 +         this.uninstall(app.manifestURL, function() {}, function() {});
   1.500 +         return;
   1.501 +       }
   1.502 +    }
   1.503 +#endif
   1.504 +  },
   1.505 +
   1.506 +  // Implements the core of bug 787439
   1.507 +  // if at first run, go through these steps:
   1.508 +  //   a. load the core apps registry.
   1.509 +  //   b. uninstall any core app from the current registry but not in the
   1.510 +  //      new core apps registry.
   1.511 +  //   c. for all apps in the new core registry, install them if they are not
   1.512 +  //      yet in the current registry, and run installPermissions()
   1.513 +  installSystemApps: function() {
   1.514 +    return Task.spawn(function() {
   1.515 +      let file;
   1.516 +      try {
   1.517 +        file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false);
   1.518 +      } catch(e) { }
   1.519 +
   1.520 +      if (!file || !file.exists()) {
   1.521 +        return;
   1.522 +      }
   1.523 +
   1.524 +      // a
   1.525 +      let data = yield AppsUtils.loadJSONAsync(file.path);
   1.526 +      if (!data) {
   1.527 +        return;
   1.528 +      }
   1.529 +
   1.530 +      // b : core apps are not removable.
   1.531 +      for (let id in this.webapps) {
   1.532 +        if (id in data || this.webapps[id].removable)
   1.533 +          continue;
   1.534 +        // Remove the permissions, cookies and private data for this app.
   1.535 +        let localId = this.webapps[id].localId;
   1.536 +        let permMgr = Cc["@mozilla.org/permissionmanager;1"]
   1.537 +                        .getService(Ci.nsIPermissionManager);
   1.538 +        permMgr.removePermissionsForApp(localId, false);
   1.539 +        Services.cookies.removeCookiesForApp(localId, false);
   1.540 +        this._clearPrivateData(localId, false);
   1.541 +        delete this.webapps[id];
   1.542 +      }
   1.543 +
   1.544 +      let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
   1.545 +      // c
   1.546 +      for (let id in data) {
   1.547 +        // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org)
   1.548 +        // Use that property to check if they are new or not.
   1.549 +        if (!(id in this.webapps)) {
   1.550 +          this.webapps[id] = data[id];
   1.551 +          this.webapps[id].basePath = appDir.path;
   1.552 +
   1.553 +          this.webapps[id].id = id;
   1.554 +
   1.555 +          // Create a new localId.
   1.556 +          this.webapps[id].localId = this._nextLocalId();
   1.557 +
   1.558 +          // Core apps are not removable.
   1.559 +          if (this.webapps[id].removable === undefined) {
   1.560 +            this.webapps[id].removable = false;
   1.561 +          }
   1.562 +        } else {
   1.563 +          // we fall into this case if the app is present in /system/b2g/webapps/webapps.json
   1.564 +          // and in /data/local/webapps/webapps.json: this happens when updating gaia apps
   1.565 +          // Confere bug 989876
   1.566 +          this.webapps[id].updateTime = data[id].updateTime;
   1.567 +          this.webapps[id].lastUpdateCheck = data[id].updateTime;
   1.568 +        }
   1.569 +      }
   1.570 +    }.bind(this)).then(null, Cu.reportError);
   1.571 +  },
   1.572 +
   1.573 +  loadAndUpdateApps: function() {
   1.574 +    return Task.spawn(function() {
   1.575 +      let runUpdate = AppsUtils.isFirstRun(Services.prefs);
   1.576 +
   1.577 +      yield this.loadCurrentRegistry();
   1.578 +
   1.579 +      if (runUpdate) {
   1.580 +#ifdef MOZ_WIDGET_GONK
   1.581 +        yield this.installSystemApps();
   1.582 +#endif
   1.583 +
   1.584 +        // At first run, install preloaded apps and set up their permissions.
   1.585 +        for (let id in this.webapps) {
   1.586 +          this.installPreinstalledApp(id);
   1.587 +          this.removeIfHttpsDuplicate(id);
   1.588 +          if (!this.webapps[id]) {
   1.589 +            continue;
   1.590 +          }
   1.591 +          this.updateOfflineCacheForApp(id);
   1.592 +          this.updatePermissionsForApp(id);
   1.593 +        }
   1.594 +        // Need to update the persisted list of apps since
   1.595 +        // installPreinstalledApp() removes the ones failing to install.
   1.596 +        this._saveApps();
   1.597 +      }
   1.598 +
   1.599 +      // DataStores must be initialized at startup.
   1.600 +      for (let id in this.webapps) {
   1.601 +        this.updateDataStoreForApp(id);
   1.602 +      }
   1.603 +
   1.604 +      this.registerAppsHandlers(runUpdate);
   1.605 +    }.bind(this)).then(null, Cu.reportError);
   1.606 +  },
   1.607 +
   1.608 +  updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) {
   1.609 +    // Just Certified Apps can use DataStores
   1.610 +    let prefName = "dom.testing.datastore_enabled_for_hosted_apps";
   1.611 +    if (aAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
   1.612 +        (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID ||
   1.613 +         !Services.prefs.getBoolPref(prefName))) {
   1.614 +      return;
   1.615 +    }
   1.616 +
   1.617 +    if ('datastores-owned' in aManifest) {
   1.618 +      for (let name in aManifest['datastores-owned']) {
   1.619 +        let readonly = "access" in aManifest['datastores-owned'][name]
   1.620 +                         ? aManifest['datastores-owned'][name].access == 'readonly'
   1.621 +                         : false;
   1.622 +
   1.623 +        dataStoreService.installDataStore(aId, name, aOrigin, aManifestURL,
   1.624 +                                          readonly);
   1.625 +      }
   1.626 +    }
   1.627 +
   1.628 +    if ('datastores-access' in aManifest) {
   1.629 +      for (let name in aManifest['datastores-access']) {
   1.630 +        let readonly = ("readonly" in aManifest['datastores-access'][name]) &&
   1.631 +                       !aManifest['datastores-access'][name].readonly
   1.632 +                         ? false : true;
   1.633 +
   1.634 +        dataStoreService.installAccessDataStore(aId, name, aOrigin,
   1.635 +                                                aManifestURL, readonly);
   1.636 +      }
   1.637 +    }
   1.638 +  },
   1.639 +
   1.640 +  // |aEntryPoint| is either the entry_point name or the null in which case we
   1.641 +  // use the root of the manifest.
   1.642 +  //
   1.643 +  // TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...).
   1.644 +  _registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) {
   1.645 +    let root = aManifest;
   1.646 +    if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
   1.647 +      root = aManifest.entry_points[aEntryPoint];
   1.648 +    }
   1.649 +
   1.650 +    if (!root.messages || !Array.isArray(root.messages) ||
   1.651 +        root.messages.length == 0) {
   1.652 +      return;
   1.653 +    }
   1.654 +
   1.655 +    let manifest = new ManifestHelper(aManifest, aApp.origin);
   1.656 +    let launchPath = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), null, null);
   1.657 +    let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
   1.658 +    root.messages.forEach(function registerPages(aMessage) {
   1.659 +      let href = launchPath;
   1.660 +      let messageName;
   1.661 +      if (typeof(aMessage) === "object" && Object.keys(aMessage).length === 1) {
   1.662 +        messageName = Object.keys(aMessage)[0];
   1.663 +        let uri;
   1.664 +        try {
   1.665 +          uri = manifest.resolveFromOrigin(aMessage[messageName]);
   1.666 +        } catch(e) {
   1.667 +          debug("system message url (" + aMessage[messageName] + ") is invalid, skipping. " +
   1.668 +                "Error is: " + e);
   1.669 +          return;
   1.670 +        }
   1.671 +        href = Services.io.newURI(uri, null, null);
   1.672 +      } else {
   1.673 +        messageName = aMessage;
   1.674 +      }
   1.675 +
   1.676 +      if (SystemMessagePermissionsChecker
   1.677 +            .isSystemMessagePermittedToRegister(messageName,
   1.678 +                                                aApp.origin,
   1.679 +                                                aManifest)) {
   1.680 +        msgmgr.registerPage(messageName, href, manifestURL);
   1.681 +      }
   1.682 +    });
   1.683 +  },
   1.684 +
   1.685 +  // |aEntryPoint| is either the entry_point name or the null in which case we
   1.686 +  // use the root of the manifest.
   1.687 +  //
   1.688 +  // TODO Bug 908094 Refine _registerInterAppConnectionsForEntryPoint(...).
   1.689 +  _registerInterAppConnectionsForEntryPoint: function(aManifest, aApp,
   1.690 +                                                      aEntryPoint) {
   1.691 +    let root = aManifest;
   1.692 +    if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
   1.693 +      root = aManifest.entry_points[aEntryPoint];
   1.694 +    }
   1.695 +
   1.696 +    let connections = root.connections;
   1.697 +    if (!connections) {
   1.698 +      return;
   1.699 +    }
   1.700 +
   1.701 +    if ((typeof connections) !== "object") {
   1.702 +      debug("|connections| is not an object. Skipping: " + connections);
   1.703 +      return;
   1.704 +    }
   1.705 +
   1.706 +    let manifest = new ManifestHelper(aManifest, aApp.origin);
   1.707 +    let launchPathURI = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint),
   1.708 +                                           null, null);
   1.709 +    let manifestURI = Services.io.newURI(aApp.manifestURL, null, null);
   1.710 +
   1.711 +    for (let keyword in connections) {
   1.712 +      let connection = connections[keyword];
   1.713 +
   1.714 +      // Resolve the handler path from origin. If |handler_path| is absent,
   1.715 +      // use |launch_path| as default.
   1.716 +      let fullHandlerPath;
   1.717 +      let handlerPath = connection.handler_path;
   1.718 +      if (handlerPath) {
   1.719 +        try {
   1.720 +          fullHandlerPath = manifest.resolveFromOrigin(handlerPath);
   1.721 +        } catch(e) {
   1.722 +          debug("Connection's handler path is invalid. Skipping: keyword: " +
   1.723 +                keyword + " handler_path: " + handlerPath);
   1.724 +          continue;
   1.725 +        }
   1.726 +      }
   1.727 +      let handlerPageURI = fullHandlerPath
   1.728 +                           ? Services.io.newURI(fullHandlerPath, null, null)
   1.729 +                           : launchPathURI;
   1.730 +
   1.731 +      if (SystemMessagePermissionsChecker
   1.732 +            .isSystemMessagePermittedToRegister("connection",
   1.733 +                                                aApp.origin,
   1.734 +                                                aManifest)) {
   1.735 +        msgmgr.registerPage("connection", handlerPageURI, manifestURI);
   1.736 +      }
   1.737 +
   1.738 +      interAppCommService.
   1.739 +        registerConnection(keyword,
   1.740 +                           handlerPageURI,
   1.741 +                           manifestURI,
   1.742 +                           connection.description,
   1.743 +                           connection.rules);
   1.744 +    }
   1.745 +  },
   1.746 +
   1.747 +  _registerSystemMessages: function(aManifest, aApp) {
   1.748 +    this._registerSystemMessagesForEntryPoint(aManifest, aApp, null);
   1.749 +
   1.750 +    if (!aManifest.entry_points) {
   1.751 +      return;
   1.752 +    }
   1.753 +
   1.754 +    for (let entryPoint in aManifest.entry_points) {
   1.755 +      this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint);
   1.756 +    }
   1.757 +  },
   1.758 +
   1.759 +  _registerInterAppConnections: function(aManifest, aApp) {
   1.760 +    this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, null);
   1.761 +
   1.762 +    if (!aManifest.entry_points) {
   1.763 +      return;
   1.764 +    }
   1.765 +
   1.766 +    for (let entryPoint in aManifest.entry_points) {
   1.767 +      this._registerInterAppConnectionsForEntryPoint(aManifest, aApp,
   1.768 +                                                     entryPoint);
   1.769 +    }
   1.770 +  },
   1.771 +
   1.772 +  // |aEntryPoint| is either the entry_point name or the null in which case we
   1.773 +  // use the root of the manifest.
   1.774 +  _createActivitiesToRegister: function(aManifest, aApp, aEntryPoint, aRunUpdate) {
   1.775 +    let activitiesToRegister = [];
   1.776 +    let root = aManifest;
   1.777 +    if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
   1.778 +      root = aManifest.entry_points[aEntryPoint];
   1.779 +    }
   1.780 +
   1.781 +    if (!root.activities) {
   1.782 +      return activitiesToRegister;
   1.783 +    }
   1.784 +
   1.785 +    let manifest = new ManifestHelper(aManifest, aApp.origin);
   1.786 +    for (let activity in root.activities) {
   1.787 +      let description = root.activities[activity];
   1.788 +      let href = description.href;
   1.789 +      if (!href) {
   1.790 +        href = manifest.launch_path;
   1.791 +      }
   1.792 +
   1.793 +      try {
   1.794 +        href = manifest.resolveFromOrigin(href);
   1.795 +      } catch (e) {
   1.796 +        debug("Activity href (" + href + ") is invalid, skipping. " +
   1.797 +              "Error is: " + e);
   1.798 +        continue;
   1.799 +      }
   1.800 +
   1.801 +      // Make a copy of the description object since we don't want to modify
   1.802 +      // the manifest itself, but need to register with a resolved URI.
   1.803 +      let newDesc = {};
   1.804 +      for (let prop in description) {
   1.805 +        newDesc[prop] = description[prop];
   1.806 +      }
   1.807 +      newDesc.href = href;
   1.808 +
   1.809 +      debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' +
   1.810 +          activity + ', description.href is ' + newDesc.href);
   1.811 +
   1.812 +      if (aRunUpdate) {
   1.813 +        activitiesToRegister.push({ "manifest": aApp.manifestURL,
   1.814 +                                    "name": activity,
   1.815 +                                    "icon": manifest.iconURLForSize(128),
   1.816 +                                    "description": newDesc });
   1.817 +      }
   1.818 +
   1.819 +      let launchPath = Services.io.newURI(href, null, null);
   1.820 +      let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
   1.821 +
   1.822 +      if (SystemMessagePermissionsChecker
   1.823 +            .isSystemMessagePermittedToRegister("activity",
   1.824 +                                                aApp.origin,
   1.825 +                                                aManifest)) {
   1.826 +        msgmgr.registerPage("activity", launchPath, manifestURL);
   1.827 +      }
   1.828 +    }
   1.829 +    return activitiesToRegister;
   1.830 +  },
   1.831 +
   1.832 +  // |aAppsToRegister| contains an array of apps to be registered, where
   1.833 +  // each element is an object in the format of {manifest: foo, app: bar}.
   1.834 +  _registerActivitiesForApps: function(aAppsToRegister, aRunUpdate) {
   1.835 +    // Collect the activities to be registered for root and entry_points.
   1.836 +    let activitiesToRegister = [];
   1.837 +    aAppsToRegister.forEach(function (aApp) {
   1.838 +      let manifest = aApp.manifest;
   1.839 +      let app = aApp.app;
   1.840 +      activitiesToRegister.push.apply(activitiesToRegister,
   1.841 +        this._createActivitiesToRegister(manifest, app, null, aRunUpdate));
   1.842 +
   1.843 +      if (!manifest.entry_points) {
   1.844 +        return;
   1.845 +      }
   1.846 +
   1.847 +      for (let entryPoint in manifest.entry_points) {
   1.848 +        activitiesToRegister.push.apply(activitiesToRegister,
   1.849 +          this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate));
   1.850 +      }
   1.851 +    }, this);
   1.852 +
   1.853 +    if (!aRunUpdate || activitiesToRegister.length == 0) {
   1.854 +      this.notifyAppsRegistryReady();
   1.855 +      return;
   1.856 +    }
   1.857 +
   1.858 +    // Send the array carrying all the activities to be registered.
   1.859 +    cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister);
   1.860 +  },
   1.861 +
   1.862 +  // Better to directly use |_registerActivitiesForApps()| if we have
   1.863 +  // multiple apps to be registered for activities.
   1.864 +  _registerActivities: function(aManifest, aApp, aRunUpdate) {
   1.865 +    this._registerActivitiesForApps([{ manifest: aManifest, app: aApp }], aRunUpdate);
   1.866 +  },
   1.867 +
   1.868 +  // |aEntryPoint| is either the entry_point name or the null in which case we
   1.869 +  // use the root of the manifest.
   1.870 +  _createActivitiesToUnregister: function(aManifest, aApp, aEntryPoint) {
   1.871 +    let activitiesToUnregister = [];
   1.872 +    let root = aManifest;
   1.873 +    if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
   1.874 +      root = aManifest.entry_points[aEntryPoint];
   1.875 +    }
   1.876 +
   1.877 +    if (!root.activities) {
   1.878 +      return activitiesToUnregister;
   1.879 +    }
   1.880 +
   1.881 +    for (let activity in root.activities) {
   1.882 +      let description = root.activities[activity];
   1.883 +      activitiesToUnregister.push({ "manifest": aApp.manifestURL,
   1.884 +                                    "name": activity,
   1.885 +                                    "description": description });
   1.886 +    }
   1.887 +    return activitiesToUnregister;
   1.888 +  },
   1.889 +
   1.890 +  // |aAppsToUnregister| contains an array of apps to be unregistered, where
   1.891 +  // each element is an object in the format of {manifest: foo, app: bar}.
   1.892 +  _unregisterActivitiesForApps: function(aAppsToUnregister) {
   1.893 +    // Collect the activities to be unregistered for root and entry_points.
   1.894 +    let activitiesToUnregister = [];
   1.895 +    aAppsToUnregister.forEach(function (aApp) {
   1.896 +      let manifest = aApp.manifest;
   1.897 +      let app = aApp.app;
   1.898 +      activitiesToUnregister.push.apply(activitiesToUnregister,
   1.899 +        this._createActivitiesToUnregister(manifest, app, null));
   1.900 +
   1.901 +      if (!manifest.entry_points) {
   1.902 +        return;
   1.903 +      }
   1.904 +
   1.905 +      for (let entryPoint in manifest.entry_points) {
   1.906 +        activitiesToUnregister.push.apply(activitiesToUnregister,
   1.907 +          this._createActivitiesToUnregister(manifest, app, entryPoint));
   1.908 +      }
   1.909 +    }, this);
   1.910 +
   1.911 +    // Send the array carrying all the activities to be unregistered.
   1.912 +    cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister);
   1.913 +  },
   1.914 +
   1.915 +  // Better to directly use |_unregisterActivitiesForApps()| if we have
   1.916 +  // multiple apps to be unregistered for activities.
   1.917 +  _unregisterActivities: function(aManifest, aApp) {
   1.918 +    this._unregisterActivitiesForApps([{ manifest: aManifest, app: aApp }]);
   1.919 +  },
   1.920 +
   1.921 +  _processManifestForIds: function(aIds, aRunUpdate) {
   1.922 +    this._readManifests(aIds).then((aResults) => {
   1.923 +      let appsToRegister = [];
   1.924 +      aResults.forEach((aResult) => {
   1.925 +        let app = this.webapps[aResult.id];
   1.926 +        let manifest = aResult.manifest;
   1.927 +        if (!manifest) {
   1.928 +          // If we can't load the manifest, we probably have a corrupted
   1.929 +          // registry. We delete the app since we can't do anything with it.
   1.930 +          delete this.webapps[aResult.id];
   1.931 +          return;
   1.932 +        }
   1.933 +        app.name = manifest.name;
   1.934 +        app.csp = manifest.csp || "";
   1.935 +        app.role = manifest.role || "";
   1.936 +        if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
   1.937 +          app.redirects = this.sanitizeRedirects(manifest.redirects);
   1.938 +        }
   1.939 +        this._registerSystemMessages(manifest, app);
   1.940 +        this._registerInterAppConnections(manifest, app);
   1.941 +        appsToRegister.push({ manifest: manifest, app: app });
   1.942 +      });
   1.943 +      this._registerActivitiesForApps(appsToRegister, aRunUpdate);
   1.944 +    });
   1.945 +  },
   1.946 +
   1.947 +  observe: function(aSubject, aTopic, aData) {
   1.948 +    if (aTopic == "xpcom-shutdown") {
   1.949 +      this.messages.forEach((function(msgName) {
   1.950 +        ppmm.removeMessageListener(msgName, this);
   1.951 +      }).bind(this));
   1.952 +      Services.obs.removeObserver(this, "xpcom-shutdown");
   1.953 +      cpmm = null;
   1.954 +      ppmm = null;
   1.955 +    } else if (aTopic == "memory-pressure") {
   1.956 +      // Clear the manifest cache on memory pressure.
   1.957 +      this._manifestCache = {};
   1.958 +    }
   1.959 +  },
   1.960 +
   1.961 +  addMessageListener: function(aMsgNames, aApp, aMm) {
   1.962 +    aMsgNames.forEach(function (aMsgName) {
   1.963 +      let man = aApp && aApp.manifestURL;
   1.964 +      if (!(aMsgName in this.children)) {
   1.965 +        this.children[aMsgName] = [];
   1.966 +      }
   1.967 +
   1.968 +      let mmFound = this.children[aMsgName].some(function(mmRef) {
   1.969 +        if (mmRef.mm === aMm) {
   1.970 +          mmRef.refCount++;
   1.971 +          return true;
   1.972 +        }
   1.973 +        return false;
   1.974 +      });
   1.975 +
   1.976 +      if (!mmFound) {
   1.977 +        this.children[aMsgName].push({
   1.978 +          mm: aMm,
   1.979 +          refCount: 1
   1.980 +        });
   1.981 +      }
   1.982 +
   1.983 +      // If the state reported by the registration is outdated, update it now.
   1.984 +      if ((aMsgName === 'Webapps:FireEvent') ||
   1.985 +          (aMsgName === 'Webapps:UpdateState')) {
   1.986 +        if (man) {
   1.987 +          let app = this.getAppByManifestURL(aApp.manifestURL);
   1.988 +          if (app && ((aApp.installState !== app.installState) ||
   1.989 +                      (aApp.downloading !== app.downloading))) {
   1.990 +            debug("Got a registration from an outdated app: " +
   1.991 +                  aApp.manifestURL);
   1.992 +            let aEvent ={
   1.993 +              type: app.installState,
   1.994 +              app: app,
   1.995 +              manifestURL: app.manifestURL,
   1.996 +              manifest: app.manifest
   1.997 +            };
   1.998 +            aMm.sendAsyncMessage(aMsgName, aEvent);
   1.999 +          }
  1.1000 +        }
  1.1001 +      }
  1.1002 +    }, this);
  1.1003 +  },
  1.1004 +
  1.1005 +  removeMessageListener: function(aMsgNames, aMm) {
  1.1006 +    if (aMsgNames.length === 1 &&
  1.1007 +        aMsgNames[0] === "Webapps:Internal:AllMessages") {
  1.1008 +      for (let msgName in this.children) {
  1.1009 +        let msg = this.children[msgName];
  1.1010 +
  1.1011 +        for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) {
  1.1012 +          let mmRef = msg[mmI];
  1.1013 +          if (mmRef.mm === aMm) {
  1.1014 +            msg.splice(mmI, 1);
  1.1015 +          }
  1.1016 +        }
  1.1017 +
  1.1018 +        if (msg.length === 0) {
  1.1019 +          delete this.children[msgName];
  1.1020 +        }
  1.1021 +      }
  1.1022 +      return;
  1.1023 +    }
  1.1024 +
  1.1025 +    aMsgNames.forEach(function(aMsgName) {
  1.1026 +      if (!(aMsgName in this.children)) {
  1.1027 +        return;
  1.1028 +      }
  1.1029 +
  1.1030 +      let removeIndex;
  1.1031 +      this.children[aMsgName].some(function(mmRef, index) {
  1.1032 +        if (mmRef.mm === aMm) {
  1.1033 +          mmRef.refCount--;
  1.1034 +          if (mmRef.refCount === 0) {
  1.1035 +            removeIndex = index;
  1.1036 +          }
  1.1037 +          return true;
  1.1038 +        }
  1.1039 +        return false;
  1.1040 +      });
  1.1041 +
  1.1042 +      if (removeIndex) {
  1.1043 +        this.children[aMsgName].splice(removeIndex, 1);
  1.1044 +      }
  1.1045 +    }, this);
  1.1046 +  },
  1.1047 +
  1.1048 +  receiveMessage: function(aMessage) {
  1.1049 +    // nsIPrefBranch throws if pref does not exist, faster to simply write
  1.1050 +    // the pref instead of first checking if it is false.
  1.1051 +    Services.prefs.setBoolPref("dom.mozApps.used", true);
  1.1052 +
  1.1053 +    // We need to check permissions for calls coming from mozApps.mgmt.
  1.1054 +    // These are: getAll(), getNotInstalled(), applyDownload() and uninstall().
  1.1055 +    if (["Webapps:GetAll",
  1.1056 +         "Webapps:GetNotInstalled",
  1.1057 +         "Webapps:ApplyDownload",
  1.1058 +         "Webapps:Uninstall"].indexOf(aMessage.name) != -1) {
  1.1059 +      if (!aMessage.target.assertPermission("webapps-manage")) {
  1.1060 +        debug("mozApps message " + aMessage.name +
  1.1061 +        " from a content process with no 'webapps-manage' privileges.");
  1.1062 +        return null;
  1.1063 +      }
  1.1064 +    }
  1.1065 +
  1.1066 +    let msg = aMessage.data || {};
  1.1067 +    let mm = aMessage.target;
  1.1068 +    msg.mm = mm;
  1.1069 +
  1.1070 +    switch (aMessage.name) {
  1.1071 +      case "Webapps:Install": {
  1.1072 +#ifdef MOZ_ANDROID_SYNTHAPKS
  1.1073 +        Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
  1.1074 +#else
  1.1075 +        this.doInstall(msg, mm);
  1.1076 +#endif
  1.1077 +        break;
  1.1078 +      }
  1.1079 +      case "Webapps:GetSelf":
  1.1080 +        this.getSelf(msg, mm);
  1.1081 +        break;
  1.1082 +      case "Webapps:Uninstall":
  1.1083 +        this.doUninstall(msg, mm);
  1.1084 +        break;
  1.1085 +      case "Webapps:Launch":
  1.1086 +        this.doLaunch(msg, mm);
  1.1087 +        break;
  1.1088 +      case "Webapps:CheckInstalled":
  1.1089 +        this.checkInstalled(msg, mm);
  1.1090 +        break;
  1.1091 +      case "Webapps:GetInstalled":
  1.1092 +        this.getInstalled(msg, mm);
  1.1093 +        break;
  1.1094 +      case "Webapps:GetNotInstalled":
  1.1095 +        this.getNotInstalled(msg, mm);
  1.1096 +        break;
  1.1097 +      case "Webapps:GetAll":
  1.1098 +        this.doGetAll(msg, mm);
  1.1099 +        break;
  1.1100 +      case "Webapps:InstallPackage": {
  1.1101 +#ifdef MOZ_ANDROID_SYNTHAPKS
  1.1102 +        Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
  1.1103 +#else
  1.1104 +        this.doInstallPackage(msg, mm);
  1.1105 +#endif
  1.1106 +        break;
  1.1107 +      }
  1.1108 +      case "Webapps:RegisterForMessages":
  1.1109 +        this.addMessageListener(msg.messages, msg.app, mm);
  1.1110 +        break;
  1.1111 +      case "Webapps:UnregisterForMessages":
  1.1112 +        this.removeMessageListener(msg, mm);
  1.1113 +        break;
  1.1114 +      case "child-process-shutdown":
  1.1115 +        this.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
  1.1116 +        break;
  1.1117 +      case "Webapps:GetList":
  1.1118 +        this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], null, mm);
  1.1119 +        return this.webapps;
  1.1120 +      case "Webapps:Download":
  1.1121 +        this.startDownload(msg.manifestURL);
  1.1122 +        break;
  1.1123 +      case "Webapps:CancelDownload":
  1.1124 +        this.cancelDownload(msg.manifestURL);
  1.1125 +        break;
  1.1126 +      case "Webapps:CheckForUpdate":
  1.1127 +        this.checkForUpdate(msg, mm);
  1.1128 +        break;
  1.1129 +      case "Webapps:ApplyDownload":
  1.1130 +        this.applyDownload(msg.manifestURL);
  1.1131 +        break;
  1.1132 +      case "Activities:Register:OK":
  1.1133 +        this.notifyAppsRegistryReady();
  1.1134 +        break;
  1.1135 +      case "Webapps:Install:Return:Ack":
  1.1136 +        this.onInstallSuccessAck(msg.manifestURL);
  1.1137 +        break;
  1.1138 +      case "Webapps:AddReceipt":
  1.1139 +        this.addReceipt(msg, mm);
  1.1140 +        break;
  1.1141 +      case "Webapps:RemoveReceipt":
  1.1142 +        this.removeReceipt(msg, mm);
  1.1143 +        break;
  1.1144 +      case "Webapps:ReplaceReceipt":
  1.1145 +        this.replaceReceipt(msg, mm);
  1.1146 +        break;
  1.1147 +    }
  1.1148 +  },
  1.1149 +
  1.1150 +  getAppInfo: function getAppInfo(aAppId) {
  1.1151 +    return AppsUtils.getAppInfo(this.webapps, aAppId);
  1.1152 +  },
  1.1153 +
  1.1154 +  // Some messages can be listened by several content processes:
  1.1155 +  // Webapps:AddApp
  1.1156 +  // Webapps:RemoveApp
  1.1157 +  // Webapps:Install:Return:OK
  1.1158 +  // Webapps:Uninstall:Return:OK
  1.1159 +  // Webapps:Uninstall:Broadcast:Return:OK
  1.1160 +  // Webapps:FireEvent
  1.1161 +  // Webapps:checkForUpdate:Return:OK
  1.1162 +  // Webapps:UpdateState
  1.1163 +  broadcastMessage: function broadcastMessage(aMsgName, aContent) {
  1.1164 +    if (!(aMsgName in this.children)) {
  1.1165 +      return;
  1.1166 +    }
  1.1167 +    this.children[aMsgName].forEach(function(mmRef) {
  1.1168 +      mmRef.mm.sendAsyncMessage(aMsgName, aContent);
  1.1169 +    });
  1.1170 +  },
  1.1171 +
  1.1172 +  registerUpdateHandler: function(aHandler) {
  1.1173 +    this._updateHandlers.push(aHandler);
  1.1174 +  },
  1.1175 +
  1.1176 +  unregisterUpdateHandler: function(aHandler) {
  1.1177 +    let index = this._updateHandlers.indexOf(aHandler);
  1.1178 +    if (index != -1) {
  1.1179 +      this._updateHandlers.splice(index, 1);
  1.1180 +    }
  1.1181 +  },
  1.1182 +
  1.1183 +  notifyUpdateHandlers: function(aApp, aManifest, aZipPath) {
  1.1184 +    for (let updateHandler of this._updateHandlers) {
  1.1185 +      updateHandler(aApp, aManifest, aZipPath);
  1.1186 +    }
  1.1187 +  },
  1.1188 +
  1.1189 +  _getAppDir: function(aId) {
  1.1190 +    return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
  1.1191 +  },
  1.1192 +
  1.1193 +  _writeFile: function(aPath, aData) {
  1.1194 +    debug("Saving " + aPath);
  1.1195 +
  1.1196 +    let deferred = Promise.defer();
  1.1197 +
  1.1198 +    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  1.1199 +    file.initWithPath(aPath);
  1.1200 +
  1.1201 +    // Initialize the file output stream
  1.1202 +    let ostream = FileUtils.openSafeFileOutputStream(file);
  1.1203 +
  1.1204 +    // Obtain a converter to convert our data to a UTF-8 encoded input stream.
  1.1205 +    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  1.1206 +                      .createInstance(Ci.nsIScriptableUnicodeConverter);
  1.1207 +    converter.charset = "UTF-8";
  1.1208 +
  1.1209 +    // Asynchronously copy the data to the file.
  1.1210 +    let istream = converter.convertToInputStream(aData);
  1.1211 +    NetUtil.asyncCopy(istream, ostream, function(aResult) {
  1.1212 +      if (!Components.isSuccessCode(aResult)) {
  1.1213 +        deferred.reject()
  1.1214 +      } else {
  1.1215 +        deferred.resolve();
  1.1216 +      }
  1.1217 +    });
  1.1218 +
  1.1219 +    return deferred.promise;
  1.1220 +  },
  1.1221 +
  1.1222 +  doLaunch: function (aData, aMm) {
  1.1223 +    this.launch(
  1.1224 +      aData.manifestURL,
  1.1225 +      aData.startPoint,
  1.1226 +      aData.timestamp,
  1.1227 +      function onsuccess() {
  1.1228 +        aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData);
  1.1229 +      },
  1.1230 +      function onfailure(reason) {
  1.1231 +        aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData);
  1.1232 +      }
  1.1233 +    );
  1.1234 +  },
  1.1235 +
  1.1236 +  launch: function launch(aManifestURL, aStartPoint, aTimeStamp, aOnSuccess, aOnFailure) {
  1.1237 +    let app = this.getAppByManifestURL(aManifestURL);
  1.1238 +    if (!app) {
  1.1239 +      aOnFailure("NO_SUCH_APP");
  1.1240 +      return;
  1.1241 +    }
  1.1242 +
  1.1243 +    // Fire an error when trying to launch an app that is not
  1.1244 +    // yet fully installed.
  1.1245 +    if (app.installState == "pending") {
  1.1246 +      aOnFailure("PENDING_APP_NOT_LAUNCHABLE");
  1.1247 +      return;
  1.1248 +    }
  1.1249 +
  1.1250 +    // We have to clone the app object as nsIDOMApplication objects are
  1.1251 +    // stringified as an empty object. (see bug 830376)
  1.1252 +    let appClone = AppsUtils.cloneAppObject(app);
  1.1253 +    appClone.startPoint = aStartPoint;
  1.1254 +    appClone.timestamp = aTimeStamp;
  1.1255 +    Services.obs.notifyObservers(null, "webapps-launch", JSON.stringify(appClone));
  1.1256 +    aOnSuccess();
  1.1257 +  },
  1.1258 +
  1.1259 +  close: function close(aApp) {
  1.1260 +    debug("close");
  1.1261 +
  1.1262 +    // We have to clone the app object as nsIDOMApplication objects are
  1.1263 +    // stringified as an empty object. (see bug 830376)
  1.1264 +    let appClone = AppsUtils.cloneAppObject(aApp);
  1.1265 +    Services.obs.notifyObservers(null, "webapps-close", JSON.stringify(appClone));
  1.1266 +  },
  1.1267 +
  1.1268 +  cancelDownload: function cancelDownload(aManifestURL, aError) {
  1.1269 +    debug("cancelDownload " + aManifestURL);
  1.1270 +    let error = aError || "DOWNLOAD_CANCELED";
  1.1271 +    let download = AppDownloadManager.get(aManifestURL);
  1.1272 +    if (!download) {
  1.1273 +      debug("Could not find a download for " + aManifestURL);
  1.1274 +      return;
  1.1275 +    }
  1.1276 +
  1.1277 +    let app = this.webapps[download.appId];
  1.1278 +
  1.1279 +    if (download.cacheUpdate) {
  1.1280 +      try {
  1.1281 +        download.cacheUpdate.cancel();
  1.1282 +      } catch (e) {
  1.1283 +        debug (e);
  1.1284 +      }
  1.1285 +    } else if (download.channel) {
  1.1286 +      try {
  1.1287 +        download.channel.cancel(Cr.NS_BINDING_ABORTED);
  1.1288 +      } catch(e) { }
  1.1289 +    } else {
  1.1290 +      return;
  1.1291 +    }
  1.1292 +
  1.1293 +    this._saveApps().then(() => {
  1.1294 +      this.broadcastMessage("Webapps:UpdateState", {
  1.1295 +        app: {
  1.1296 +          progress: 0,
  1.1297 +          installState: download.previousState,
  1.1298 +          downloading: false
  1.1299 +        },
  1.1300 +        error: error,
  1.1301 +        manifestURL: app.manifestURL,
  1.1302 +      })
  1.1303 +      this.broadcastMessage("Webapps:FireEvent", {
  1.1304 +        eventType: "downloaderror",
  1.1305 +        manifestURL: app.manifestURL
  1.1306 +      });
  1.1307 +    });
  1.1308 +    AppDownloadManager.remove(aManifestURL);
  1.1309 +  },
  1.1310 +
  1.1311 +  startDownload: Task.async(function*(aManifestURL) {
  1.1312 +    debug("startDownload for " + aManifestURL);
  1.1313 +
  1.1314 +    let id = this._appIdForManifestURL(aManifestURL);
  1.1315 +    let app = this.webapps[id];
  1.1316 +    if (!app) {
  1.1317 +      debug("startDownload: No app found for " + aManifestURL);
  1.1318 +      return;
  1.1319 +    }
  1.1320 +
  1.1321 +    if (app.downloading) {
  1.1322 +      debug("app is already downloading. Ignoring.");
  1.1323 +      return;
  1.1324 +    }
  1.1325 +
  1.1326 +    // If the caller is trying to start a download but we have nothing to
  1.1327 +    // download, send an error.
  1.1328 +    if (!app.downloadAvailable) {
  1.1329 +      this.broadcastMessage("Webapps:UpdateState", {
  1.1330 +        error: "NO_DOWNLOAD_AVAILABLE",
  1.1331 +        manifestURL: app.manifestURL
  1.1332 +      });
  1.1333 +      this.broadcastMessage("Webapps:FireEvent", {
  1.1334 +        eventType: "downloaderror",
  1.1335 +        manifestURL: app.manifestURL
  1.1336 +      });
  1.1337 +      return;
  1.1338 +    }
  1.1339 +
  1.1340 +    // First of all, we check if the download is supposed to update an
  1.1341 +    // already installed application.
  1.1342 +    let isUpdate = (app.installState == "installed");
  1.1343 +
  1.1344 +    // An app download would only be triggered for two reasons: an app
  1.1345 +    // update or while retrying to download a previously failed or canceled
  1.1346 +    // instalation.
  1.1347 +    app.retryingDownload = !isUpdate;
  1.1348 +
  1.1349 +    // We need to get the update manifest here, not the webapp manifest.
  1.1350 +    // If this is an update, the update manifest is staged.
  1.1351 +    let file = FileUtils.getFile(DIRECTORY_NAME,
  1.1352 +                                 ["webapps", id,
  1.1353 +                                  isUpdate ? "staged-update.webapp"
  1.1354 +                                           : "update.webapp"],
  1.1355 +                                 true);
  1.1356 +
  1.1357 +    if (!file.exists()) {
  1.1358 +      // This is a hosted app, let's check if it has an appcache
  1.1359 +      // and download it.
  1.1360 +      let results = yield this._readManifests([{ id: id }]);
  1.1361 +
  1.1362 +      let jsonManifest = results[0].manifest;
  1.1363 +      let manifest = new ManifestHelper(jsonManifest, app.origin);
  1.1364 +
  1.1365 +      if (manifest.appcache_path) {
  1.1366 +        debug("appcache found");
  1.1367 +        this.startOfflineCacheDownload(manifest, app, null, isUpdate);
  1.1368 +      } else {
  1.1369 +        // Hosted app with no appcache, nothing to do, but we fire a
  1.1370 +        // downloaded event.
  1.1371 +        debug("No appcache found, sending 'downloaded' for " + aManifestURL);
  1.1372 +        app.downloadAvailable = false;
  1.1373 +
  1.1374 +        yield this._saveApps();
  1.1375 +
  1.1376 +        this.broadcastMessage("Webapps:UpdateState", {
  1.1377 +          app: app,
  1.1378 +          manifest: jsonManifest,
  1.1379 +          manifestURL: aManifestURL
  1.1380 +        });
  1.1381 +        this.broadcastMessage("Webapps:FireEvent", {
  1.1382 +          eventType: "downloadsuccess",
  1.1383 +          manifestURL: aManifestURL
  1.1384 +        });
  1.1385 +      }
  1.1386 +
  1.1387 +      return;
  1.1388 +    }
  1.1389 +
  1.1390 +    let json = yield AppsUtils.loadJSONAsync(file.path);
  1.1391 +    if (!json) {
  1.1392 +      debug("startDownload: No update manifest found at " + file.path + " " +
  1.1393 +            aManifestURL);
  1.1394 +      return;
  1.1395 +    }
  1.1396 +
  1.1397 +    let manifest = new ManifestHelper(json, app.manifestURL);
  1.1398 +    let [aId, aManifest] = yield this.downloadPackage(manifest, {
  1.1399 +        manifestURL: aManifestURL,
  1.1400 +        origin: app.origin,
  1.1401 +        installOrigin: app.installOrigin,
  1.1402 +        downloadSize: app.downloadSize
  1.1403 +      }, isUpdate);
  1.1404 +
  1.1405 +    // Success! Keep the zip in of TmpD, we'll move it out when
  1.1406 +    // applyDownload() will be called.
  1.1407 +    // Save the manifest in TmpD also
  1.1408 +    let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", aId,
  1.1409 +                               "manifest.webapp");
  1.1410 +    yield this._writeFile(manFile, JSON.stringify(aManifest));
  1.1411 +
  1.1412 +    app = this.webapps[aId];
  1.1413 +    // Set state and fire events.
  1.1414 +    app.downloading = false;
  1.1415 +    app.downloadAvailable = false;
  1.1416 +    app.readyToApplyDownload = true;
  1.1417 +    app.updateTime = Date.now();
  1.1418 +
  1.1419 +    yield this._saveApps();
  1.1420 +
  1.1421 +    this.broadcastMessage("Webapps:UpdateState", {
  1.1422 +      app: app,
  1.1423 +      manifestURL: aManifestURL
  1.1424 +    });
  1.1425 +    this.broadcastMessage("Webapps:FireEvent", {
  1.1426 +      eventType: "downloadsuccess",
  1.1427 +      manifestURL: aManifestURL
  1.1428 +    });
  1.1429 +    if (app.installState == "pending") {
  1.1430 +      // We restarted a failed download, apply it automatically.
  1.1431 +      this.applyDownload(aManifestURL);
  1.1432 +    }
  1.1433 +  }),
  1.1434 +
  1.1435 +  applyDownload: function applyDownload(aManifestURL) {
  1.1436 +    debug("applyDownload for " + aManifestURL);
  1.1437 +    let id = this._appIdForManifestURL(aManifestURL);
  1.1438 +    let app = this.webapps[id];
  1.1439 +    if (!app || (app && !app.readyToApplyDownload)) {
  1.1440 +      return;
  1.1441 +    }
  1.1442 +
  1.1443 +    // We need to get the old manifest to unregister web activities.
  1.1444 +    this.getManifestFor(aManifestURL).then((aOldManifest) => {
  1.1445 +      // Move the application.zip and manifest.webapp files out of TmpD
  1.1446 +      let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
  1.1447 +      let manFile = tmpDir.clone();
  1.1448 +      manFile.append("manifest.webapp");
  1.1449 +      let appFile = tmpDir.clone();
  1.1450 +      appFile.append("application.zip");
  1.1451 +
  1.1452 +      let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
  1.1453 +      appFile.moveTo(dir, "application.zip");
  1.1454 +      manFile.moveTo(dir, "manifest.webapp");
  1.1455 +
  1.1456 +      // Move the staged update manifest to a non staged one.
  1.1457 +      let staged = dir.clone();
  1.1458 +      staged.append("staged-update.webapp");
  1.1459 +
  1.1460 +      // If we are applying after a restarted download, we have no
  1.1461 +      // staged update manifest.
  1.1462 +      if (staged.exists()) {
  1.1463 +        staged.moveTo(dir, "update.webapp");
  1.1464 +      }
  1.1465 +
  1.1466 +      try {
  1.1467 +        tmpDir.remove(true);
  1.1468 +      } catch(e) { }
  1.1469 +
  1.1470 +      // Clean up the deprecated manifest cache if needed.
  1.1471 +      if (id in this._manifestCache) {
  1.1472 +        delete this._manifestCache[id];
  1.1473 +      }
  1.1474 +
  1.1475 +      // Flush the zip reader cache to make sure we use the new application.zip
  1.1476 +      // when re-launching the application.
  1.1477 +      let zipFile = dir.clone();
  1.1478 +      zipFile.append("application.zip");
  1.1479 +      Services.obs.notifyObservers(zipFile, "flush-cache-entry", null);
  1.1480 +
  1.1481 +      // Get the manifest, and set properties.
  1.1482 +      this.getManifestFor(aManifestURL).then((aData) => {
  1.1483 +        app.downloading = false;
  1.1484 +        app.downloadAvailable = false;
  1.1485 +        app.downloadSize = 0;
  1.1486 +        app.installState = "installed";
  1.1487 +        app.readyToApplyDownload = false;
  1.1488 +
  1.1489 +        // Update the staged properties.
  1.1490 +        if (app.staged) {
  1.1491 +          for (let prop in app.staged) {
  1.1492 +            app[prop] = app.staged[prop];
  1.1493 +          }
  1.1494 +          delete app.staged;
  1.1495 +        }
  1.1496 +
  1.1497 +        delete app.retryingDownload;
  1.1498 +
  1.1499 +        // Update the asm.js scripts we need to compile.
  1.1500 +        ScriptPreloader.preload(app, aData)
  1.1501 +          .then(() => this._saveApps()).then(() => {
  1.1502 +          // Update the handlers and permissions for this app.
  1.1503 +          this.updateAppHandlers(aOldManifest, aData, app);
  1.1504 +
  1.1505 +          AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => {
  1.1506 +            let appObject = AppsUtils.cloneAppObject(app);
  1.1507 +            appObject.updateManifest = aUpdateManifest;
  1.1508 +            this.notifyUpdateHandlers(appObject, aData, appFile.path);
  1.1509 +          });
  1.1510 +
  1.1511 +          if (supportUseCurrentProfile()) {
  1.1512 +            PermissionsInstaller.installPermissions(
  1.1513 +              { manifest: aData,
  1.1514 +                origin: app.origin,
  1.1515 +                manifestURL: app.manifestURL },
  1.1516 +              true);
  1.1517 +          }
  1.1518 +          this.updateDataStore(this.webapps[id].localId, app.origin,
  1.1519 +                               app.manifestURL, aData, app.appStatus);
  1.1520 +          this.broadcastMessage("Webapps:UpdateState", {
  1.1521 +            app: app,
  1.1522 +            manifest: aData,
  1.1523 +            manifestURL: app.manifestURL
  1.1524 +          });
  1.1525 +          this.broadcastMessage("Webapps:FireEvent", {
  1.1526 +            eventType: "downloadapplied",
  1.1527 +            manifestURL: app.manifestURL
  1.1528 +          });
  1.1529 +        });
  1.1530 +      });
  1.1531 +    });
  1.1532 +  },
  1.1533 +
  1.1534 +  startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
  1.1535 +    if (!aManifest.appcache_path) {
  1.1536 +      return;
  1.1537 +    }
  1.1538 +
  1.1539 +    // If the manifest has an appcache_path property, use it to populate the
  1.1540 +    // appcache.
  1.1541 +    let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(),
  1.1542 +                                         null, null);
  1.1543 +    let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null);
  1.1544 +
  1.1545 +    // We determine the app's 'installState' according to its previous
  1.1546 +    // state. Cancelled downloads should remain as 'pending'. Successfully
  1.1547 +    // installed apps should morph to 'updating'.
  1.1548 +    if (aIsUpdate) {
  1.1549 +      aApp.installState = "updating";
  1.1550 +    }
  1.1551 +
  1.1552 +    // We set the 'downloading' flag and update the apps registry right before
  1.1553 +    // starting the app download/update.
  1.1554 +    aApp.downloading = true;
  1.1555 +    aApp.progress = 0;
  1.1556 +    DOMApplicationRegistry._saveApps().then(() => {
  1.1557 +      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
  1.1558 +        app: {
  1.1559 +          downloading: true,
  1.1560 +          installState: aApp.installState,
  1.1561 +          progress: 0
  1.1562 +        },
  1.1563 +        manifestURL: aApp.manifestURL
  1.1564 +      });
  1.1565 +      let cacheUpdate = updateSvc.scheduleAppUpdate(
  1.1566 +        appcacheURI, docURI, aApp.localId, false, aProfileDir);
  1.1567 +
  1.1568 +      // We save the download details for potential further usage like
  1.1569 +      // cancelling it.
  1.1570 +      let download = {
  1.1571 +        cacheUpdate: cacheUpdate,
  1.1572 +        appId: this._appIdForManifestURL(aApp.manifestURL),
  1.1573 +        previousState: aIsUpdate ? "installed" : "pending"
  1.1574 +      };
  1.1575 +      AppDownloadManager.add(aApp.manifestURL, download);
  1.1576 +
  1.1577 +      cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
  1.1578 +
  1.1579 +    });
  1.1580 +  },
  1.1581 +
  1.1582 +  // Returns the MD5 hash of the manifest.
  1.1583 +  computeManifestHash: function(aManifest) {
  1.1584 +    return AppsUtils.computeHash(JSON.stringify(aManifest));
  1.1585 +  },
  1.1586 +
  1.1587 +  // Updates the redirect mapping, activities and system message handlers.
  1.1588 +  // aOldManifest can be null if we don't have any handler to unregister.
  1.1589 +  updateAppHandlers: function(aOldManifest, aNewManifest, aApp) {
  1.1590 +    debug("updateAppHandlers: old=" + aOldManifest + " new=" + aNewManifest);
  1.1591 +    this.notifyAppsRegistryStart();
  1.1592 +    if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
  1.1593 +      aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects);
  1.1594 +    }
  1.1595 +
  1.1596 +    if (supportSystemMessages()) {
  1.1597 +      if (aOldManifest) {
  1.1598 +        this._unregisterActivities(aOldManifest, aApp);
  1.1599 +      }
  1.1600 +      this._registerSystemMessages(aNewManifest, aApp);
  1.1601 +      this._registerActivities(aNewManifest, aApp, true);
  1.1602 +      this._registerInterAppConnections(aNewManifest, aApp);
  1.1603 +    } else {
  1.1604 +      // Nothing else to do but notifying we're ready.
  1.1605 +      this.notifyAppsRegistryReady();
  1.1606 +    }
  1.1607 +  },
  1.1608 +
  1.1609 +  checkForUpdate: function(aData, aMm) {
  1.1610 +    debug("checkForUpdate for " + aData.manifestURL);
  1.1611 +
  1.1612 +    function sendError(aError) {
  1.1613 +      aData.error = aError;
  1.1614 +      aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
  1.1615 +    }
  1.1616 +
  1.1617 +    let id = this._appIdForManifestURL(aData.manifestURL);
  1.1618 +    let app = this.webapps[id];
  1.1619 +
  1.1620 +    // We cannot update an app that does not exists.
  1.1621 +    if (!app) {
  1.1622 +      sendError("NO_SUCH_APP");
  1.1623 +      return;
  1.1624 +    }
  1.1625 +
  1.1626 +    // We cannot update an app that is not fully installed.
  1.1627 +    if (app.installState !== "installed") {
  1.1628 +      sendError("PENDING_APP_NOT_UPDATABLE");
  1.1629 +      return;
  1.1630 +    }
  1.1631 +
  1.1632 +    // We may be able to remove this when Bug 839071 is fixed.
  1.1633 +    if (app.downloading) {
  1.1634 +      sendError("APP_IS_DOWNLOADING");
  1.1635 +      return;
  1.1636 +    }
  1.1637 +
  1.1638 +    // If the app is packaged and its manifestURL has an app:// scheme,
  1.1639 +    // then we can't have an update.
  1.1640 +    if (app.origin.startsWith("app://") &&
  1.1641 +        app.manifestURL.startsWith("app://")) {
  1.1642 +      aData.error = "NOT_UPDATABLE";
  1.1643 +      aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
  1.1644 +      return;
  1.1645 +    }
  1.1646 +
  1.1647 +    // For non-removable hosted apps that lives in the core apps dir we
  1.1648 +    // only check the appcache because we can't modify the manifest even
  1.1649 +    // if it has changed.
  1.1650 +    let onlyCheckAppCache = false;
  1.1651 +
  1.1652 +#ifdef MOZ_WIDGET_GONK
  1.1653 +    let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
  1.1654 +    onlyCheckAppCache = (app.basePath == appDir.path);
  1.1655 +#endif
  1.1656 +
  1.1657 +    if (onlyCheckAppCache) {
  1.1658 +      // Bail out for packaged apps.
  1.1659 +      if (app.origin.startsWith("app://")) {
  1.1660 +        aData.error = "NOT_UPDATABLE";
  1.1661 +        aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
  1.1662 +        return;
  1.1663 +      }
  1.1664 +
  1.1665 +      // We need the manifest to check if we have an appcache.
  1.1666 +      this._readManifests([{ id: id }]).then((aResult) => {
  1.1667 +        let manifest = aResult[0].manifest;
  1.1668 +        if (!manifest.appcache_path) {
  1.1669 +          aData.error = "NOT_UPDATABLE";
  1.1670 +          aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
  1.1671 +          return;
  1.1672 +        }
  1.1673 +
  1.1674 +        debug("Checking only appcache for " + aData.manifestURL);
  1.1675 +        // Check if the appcache is updatable, and send "downloadavailable" or
  1.1676 +        // "downloadapplied".
  1.1677 +        let updateObserver = {
  1.1678 +          observe: function(aSubject, aTopic, aObsData) {
  1.1679 +            debug("onlyCheckAppCache updateSvc.checkForUpdate return for " +
  1.1680 +                  app.manifestURL + " - event is " + aTopic);
  1.1681 +            if (aTopic == "offline-cache-update-available") {
  1.1682 +              app.downloadAvailable = true;
  1.1683 +              this._saveApps().then(() => {
  1.1684 +                this.broadcastMessage("Webapps:UpdateState", {
  1.1685 +                  app: app,
  1.1686 +                  manifestURL: app.manifestURL
  1.1687 +                });
  1.1688 +                this.broadcastMessage("Webapps:FireEvent", {
  1.1689 +                  eventType: "downloadavailable",
  1.1690 +                  manifestURL: app.manifestURL,
  1.1691 +                  requestID: aData.requestID
  1.1692 +                });
  1.1693 +              });
  1.1694 +            } else {
  1.1695 +              aData.error = "NOT_UPDATABLE";
  1.1696 +              aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
  1.1697 +            }
  1.1698 +          }
  1.1699 +        };
  1.1700 +        let helper = new ManifestHelper(manifest, aData.manifestURL);
  1.1701 +        debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " +
  1.1702 +              helper.fullAppcachePath());
  1.1703 +        updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null),
  1.1704 +                                 app.localId, false, updateObserver);
  1.1705 +      });
  1.1706 +      return;
  1.1707 +    }
  1.1708 +
  1.1709 +    // On xhr load request event
  1.1710 +    function onload(xhr, oldManifest) {
  1.1711 +      debug("Got http status=" + xhr.status + " for " + aData.manifestURL);
  1.1712 +      let oldHash = app.manifestHash;
  1.1713 +      let isPackage = app.origin.startsWith("app://");
  1.1714 +
  1.1715 +      if (xhr.status == 200) {
  1.1716 +        let manifest = xhr.response;
  1.1717 +        if (manifest == null) {
  1.1718 +          sendError("MANIFEST_PARSE_ERROR");
  1.1719 +          return;
  1.1720 +        }
  1.1721 +
  1.1722 +        if (!AppsUtils.checkManifest(manifest, app)) {
  1.1723 +          sendError("INVALID_MANIFEST");
  1.1724 +          return;
  1.1725 +        } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
  1.1726 +          sendError("INSTALL_FROM_DENIED");
  1.1727 +          return;
  1.1728 +        } else {
  1.1729 +          AppsUtils.ensureSameAppName(oldManifest, manifest, app);
  1.1730 +
  1.1731 +          let hash = this.computeManifestHash(manifest);
  1.1732 +          debug("Manifest hash = " + hash);
  1.1733 +          if (isPackage) {
  1.1734 +            if (!app.staged) {
  1.1735 +              app.staged = { };
  1.1736 +            }
  1.1737 +            app.staged.manifestHash = hash;
  1.1738 +            app.staged.etag = xhr.getResponseHeader("Etag");
  1.1739 +          } else {
  1.1740 +            app.manifestHash = hash;
  1.1741 +            app.etag = xhr.getResponseHeader("Etag");
  1.1742 +          }
  1.1743 +
  1.1744 +          app.lastCheckedUpdate = Date.now();
  1.1745 +          if (isPackage) {
  1.1746 +            if (oldHash != hash) {
  1.1747 +              this.updatePackagedApp(aData, id, app, manifest);
  1.1748 +            } else {
  1.1749 +              this._saveApps().then(() => {
  1.1750 +                // Like if we got a 304, just send a 'downloadapplied'
  1.1751 +                // or downloadavailable event.
  1.1752 +                let eventType = app.downloadAvailable ? "downloadavailable"
  1.1753 +                                                      : "downloadapplied";
  1.1754 +                aMm.sendAsyncMessage("Webapps:UpdateState", {
  1.1755 +                  app: app,
  1.1756 +                  manifestURL: app.manifestURL
  1.1757 +                });
  1.1758 +                aMm.sendAsyncMessage("Webapps:FireEvent", {
  1.1759 +                  eventType: eventType,
  1.1760 +                  manifestURL: app.manifestURL,
  1.1761 +                  requestID: aData.requestID
  1.1762 +                });
  1.1763 +              });
  1.1764 +            }
  1.1765 +          } else {
  1.1766 +            // Update only the appcache if the manifest has not changed
  1.1767 +            // based on the hash value.
  1.1768 +            this.updateHostedApp(aData, id, app, oldManifest,
  1.1769 +                                 oldHash == hash ? null : manifest);
  1.1770 +          }
  1.1771 +        }
  1.1772 +      } else if (xhr.status == 304) {
  1.1773 +        // The manifest has not changed.
  1.1774 +        if (isPackage) {
  1.1775 +          app.lastCheckedUpdate = Date.now();
  1.1776 +          this._saveApps().then(() => {
  1.1777 +            // If the app is a packaged app, we just send a 'downloadapplied'
  1.1778 +            // or downloadavailable event.
  1.1779 +            let eventType = app.downloadAvailable ? "downloadavailable"
  1.1780 +                                                  : "downloadapplied";
  1.1781 +            aMm.sendAsyncMessage("Webapps:UpdateState", {
  1.1782 +              app: app,
  1.1783 +              manifestURL: app.manifestURL
  1.1784 +            });
  1.1785 +            aMm.sendAsyncMessage("Webapps:FireEvent", {
  1.1786 +              eventType: eventType,
  1.1787 +              manifestURL: app.manifestURL,
  1.1788 +              requestID: aData.requestID
  1.1789 +            });
  1.1790 +          });
  1.1791 +        } else {
  1.1792 +          // For hosted apps, even if the manifest has not changed, we check
  1.1793 +          // for offline cache updates.
  1.1794 +          this.updateHostedApp(aData, id, app, oldManifest, null);
  1.1795 +        }
  1.1796 +      } else {
  1.1797 +        sendError("MANIFEST_URL_ERROR");
  1.1798 +      }
  1.1799 +    }
  1.1800 +
  1.1801 +    // Try to download a new manifest.
  1.1802 +    function doRequest(oldManifest, headers) {
  1.1803 +      headers = headers || [];
  1.1804 +      let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
  1.1805 +                  .createInstance(Ci.nsIXMLHttpRequest);
  1.1806 +      xhr.open("GET", aData.manifestURL, true);
  1.1807 +      xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  1.1808 +      headers.forEach(function(aHeader) {
  1.1809 +        debug("Adding header: " + aHeader.name + ": " + aHeader.value);
  1.1810 +        xhr.setRequestHeader(aHeader.name, aHeader.value);
  1.1811 +      });
  1.1812 +      xhr.responseType = "json";
  1.1813 +      if (app.etag) {
  1.1814 +        debug("adding manifest etag:" + app.etag);
  1.1815 +        xhr.setRequestHeader("If-None-Match", app.etag);
  1.1816 +      }
  1.1817 +      xhr.channel.notificationCallbacks =
  1.1818 +        this.createLoadContext(app.installerAppId, app.installerIsBrowser);
  1.1819 +
  1.1820 +      xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
  1.1821 +      xhr.addEventListener("error", (function() {
  1.1822 +        sendError("NETWORK_ERROR");
  1.1823 +      }).bind(this), false);
  1.1824 +
  1.1825 +      debug("Checking manifest at " + aData.manifestURL);
  1.1826 +      xhr.send(null);
  1.1827 +    }
  1.1828 +
  1.1829 +    // Read the current app manifest file
  1.1830 +    this._readManifests([{ id: id }]).then((aResult) => {
  1.1831 +      let extraHeaders = [];
  1.1832 +#ifdef MOZ_WIDGET_GONK
  1.1833 +      let pingManifestURL;
  1.1834 +      try {
  1.1835 +        pingManifestURL = Services.prefs.getCharPref("ping.manifestURL");
  1.1836 +      } catch(e) { }
  1.1837 +
  1.1838 +      if (pingManifestURL && pingManifestURL == aData.manifestURL) {
  1.1839 +        // Get the device info.
  1.1840 +        let device = libcutils.property_get("ro.product.model");
  1.1841 +        extraHeaders.push({ name: "X-MOZ-B2G-DEVICE",
  1.1842 +                            value: device || "unknown" });
  1.1843 +      }
  1.1844 +#endif
  1.1845 +      doRequest.call(this, aResult[0].manifest, extraHeaders);
  1.1846 +    });
  1.1847 +  },
  1.1848 +
  1.1849 +  // Creates a nsILoadContext object with a given appId and isBrowser flag.
  1.1850 +  createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
  1.1851 +    return {
  1.1852 +       associatedWindow: null,
  1.1853 +       topWindow : null,
  1.1854 +       appId: aAppId,
  1.1855 +       isInBrowserElement: aIsBrowser,
  1.1856 +       usePrivateBrowsing: false,
  1.1857 +       isContent: false,
  1.1858 +
  1.1859 +       isAppOfType: function(appType) {
  1.1860 +         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  1.1861 +       },
  1.1862 +
  1.1863 +       QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
  1.1864 +                                              Ci.nsIInterfaceRequestor,
  1.1865 +                                              Ci.nsISupports]),
  1.1866 +       getInterface: function(iid) {
  1.1867 +         if (iid.equals(Ci.nsILoadContext))
  1.1868 +           return this;
  1.1869 +         throw Cr.NS_ERROR_NO_INTERFACE;
  1.1870 +       }
  1.1871 +     }
  1.1872 +  },
  1.1873 +
  1.1874 +  updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
  1.1875 +    debug("updatePackagedApp");
  1.1876 +
  1.1877 +    // Store the new update manifest.
  1.1878 +    let dir = this._getAppDir(aId).path;
  1.1879 +    let manFile = OS.Path.join(dir, "staged-update.webapp");
  1.1880 +    yield this._writeFile(manFile, JSON.stringify(aNewManifest));
  1.1881 +
  1.1882 +    let manifest = new ManifestHelper(aNewManifest, aApp.manifestURL);
  1.1883 +    // A package is available: set downloadAvailable to fire the matching
  1.1884 +    // event.
  1.1885 +    aApp.downloadAvailable = true;
  1.1886 +    aApp.downloadSize = manifest.size;
  1.1887 +    aApp.updateManifest = aNewManifest;
  1.1888 +    yield this._saveApps();
  1.1889 +
  1.1890 +    this.broadcastMessage("Webapps:UpdateState", {
  1.1891 +      app: aApp,
  1.1892 +      manifestURL: aApp.manifestURL
  1.1893 +    });
  1.1894 +    this.broadcastMessage("Webapps:FireEvent", {
  1.1895 +      eventType: "downloadavailable",
  1.1896 +      manifestURL: aApp.manifestURL,
  1.1897 +      requestID: aData.requestID
  1.1898 +    });
  1.1899 +  }),
  1.1900 +
  1.1901 +  // A hosted app is updated if the app manifest or the appcache needs
  1.1902 +  // updating. Even if the app manifest has not changed, we still check
  1.1903 +  // for changes in the app cache.
  1.1904 +  // 'aNewManifest' would contain the updated app manifest if
  1.1905 +  // it has actually been updated, while 'aOldManifest' contains the
  1.1906 +  // stored app manifest.
  1.1907 +  updateHostedApp: Task.async(function*(aData, aId, aApp, aOldManifest, aNewManifest) {
  1.1908 +    debug("updateHostedApp " + aData.manifestURL);
  1.1909 +
  1.1910 +    // Clean up the deprecated manifest cache if needed.
  1.1911 +    if (aId in this._manifestCache) {
  1.1912 +      delete this._manifestCache[aId];
  1.1913 +    }
  1.1914 +
  1.1915 +    aApp.manifest = aNewManifest || aOldManifest;
  1.1916 +
  1.1917 +    let manifest;
  1.1918 +    if (aNewManifest) {
  1.1919 +      this.updateAppHandlers(aOldManifest, aNewManifest, aApp);
  1.1920 +
  1.1921 +      this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest);
  1.1922 +
  1.1923 +      // Store the new manifest.
  1.1924 +      let dir = this._getAppDir(aId).path;
  1.1925 +      let manFile = OS.Path.join(dir, "manifest.webapp");
  1.1926 +      yield this._writeFile(manFile, JSON.stringify(aNewManifest));
  1.1927 +
  1.1928 +      manifest = new ManifestHelper(aNewManifest, aApp.origin);
  1.1929 +
  1.1930 +      if (supportUseCurrentProfile()) {
  1.1931 +        // Update the permissions for this app.
  1.1932 +        PermissionsInstaller.installPermissions({
  1.1933 +          manifest: aApp.manifest,
  1.1934 +          origin: aApp.origin,
  1.1935 +          manifestURL: aData.manifestURL
  1.1936 +        }, true);
  1.1937 +      }
  1.1938 +
  1.1939 +      this.updateDataStore(this.webapps[aId].localId, aApp.origin,
  1.1940 +                           aApp.manifestURL, aApp.manifest, aApp.appStatus);
  1.1941 +
  1.1942 +      aApp.name = manifest.name;
  1.1943 +      aApp.csp = manifest.csp || "";
  1.1944 +      aApp.role = manifest.role || "";
  1.1945 +      aApp.updateTime = Date.now();
  1.1946 +    } else {
  1.1947 +      manifest = new ManifestHelper(aOldManifest, aApp.origin);
  1.1948 +    }
  1.1949 +
  1.1950 +    // Update the registry.
  1.1951 +    this.webapps[aId] = aApp;
  1.1952 +    yield this._saveApps();
  1.1953 +
  1.1954 +    if (!manifest.appcache_path) {
  1.1955 +      this.broadcastMessage("Webapps:UpdateState", {
  1.1956 +        app: aApp,
  1.1957 +        manifest: aApp.manifest,
  1.1958 +        manifestURL: aApp.manifestURL
  1.1959 +      });
  1.1960 +      this.broadcastMessage("Webapps:FireEvent", {
  1.1961 +        eventType: "downloadapplied",
  1.1962 +        manifestURL: aApp.manifestURL,
  1.1963 +        requestID: aData.requestID
  1.1964 +      });
  1.1965 +    } else {
  1.1966 +      // Check if the appcache is updatable, and send "downloadavailable" or
  1.1967 +      // "downloadapplied".
  1.1968 +      debug("updateHostedApp: updateSvc.checkForUpdate for " +
  1.1969 +            manifest.fullAppcachePath());
  1.1970 +
  1.1971 +      let updateDeferred = Promise.defer();
  1.1972 +
  1.1973 +      updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null),
  1.1974 +                               aApp.localId, false,
  1.1975 +                               (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic));
  1.1976 +
  1.1977 +      let topic = yield updateDeferred.promise;
  1.1978 +
  1.1979 +      debug("updateHostedApp: updateSvc.checkForUpdate return for " +
  1.1980 +            aApp.manifestURL + " - event is " + topic);
  1.1981 +
  1.1982 +      let eventType =
  1.1983 +        topic == "offline-cache-update-available" ? "downloadavailable"
  1.1984 +                                                  : "downloadapplied";
  1.1985 +
  1.1986 +      aApp.downloadAvailable = (eventType == "downloadavailable");
  1.1987 +      yield this._saveApps();
  1.1988 +
  1.1989 +      this.broadcastMessage("Webapps:UpdateState", {
  1.1990 +        app: aApp,
  1.1991 +        manifest: aApp.manifest,
  1.1992 +        manifestURL: aApp.manifestURL
  1.1993 +      });
  1.1994 +      this.broadcastMessage("Webapps:FireEvent", {
  1.1995 +        eventType: eventType,
  1.1996 +        manifestURL: aApp.manifestURL,
  1.1997 +        requestID: aData.requestID
  1.1998 +      });
  1.1999 +    }
  1.2000 +
  1.2001 +    delete aApp.manifest;
  1.2002 +  }),
  1.2003 +
  1.2004 +  // Downloads the manifest and run checks, then eventually triggers the
  1.2005 +  // installation UI.
  1.2006 +  doInstall: function doInstall(aData, aMm) {
  1.2007 +    let app = aData.app;
  1.2008 +
  1.2009 +    let sendError = function sendError(aError) {
  1.2010 +      aData.error = aError;
  1.2011 +      aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
  1.2012 +      Cu.reportError("Error installing app from: " + app.installOrigin +
  1.2013 +                     ": " + aError);
  1.2014 +    }.bind(this);
  1.2015 +
  1.2016 +    if (app.receipts.length > 0) {
  1.2017 +      for (let receipt of app.receipts) {
  1.2018 +        let error = this.isReceipt(receipt);
  1.2019 +        if (error) {
  1.2020 +          sendError(error);
  1.2021 +          return;
  1.2022 +        }
  1.2023 +      }
  1.2024 +    }
  1.2025 +
  1.2026 +    // Hosted apps can't be trusted or certified, so just check that the
  1.2027 +    // manifest doesn't ask for those.
  1.2028 +    function checkAppStatus(aManifest) {
  1.2029 +      let manifestStatus = aManifest.type || "web";
  1.2030 +      return manifestStatus === "web";
  1.2031 +    }
  1.2032 +
  1.2033 +    let checkManifest = (function() {
  1.2034 +      if (!app.manifest) {
  1.2035 +        sendError("MANIFEST_PARSE_ERROR");
  1.2036 +        return false;
  1.2037 +      }
  1.2038 +
  1.2039 +      // Disallow multiple hosted apps installations from the same origin for now.
  1.2040 +      // We will remove this code after multiple apps per origin are supported (bug 778277).
  1.2041 +      // This will also disallow reinstalls from the same origin for now.
  1.2042 +      for (let id in this.webapps) {
  1.2043 +        if (this.webapps[id].origin == app.origin &&
  1.2044 +            !this.webapps[id].packageHash &&
  1.2045 +            this._isLaunchable(this.webapps[id])) {
  1.2046 +          sendError("MULTIPLE_APPS_PER_ORIGIN_FORBIDDEN");
  1.2047 +          return false;
  1.2048 +        }
  1.2049 +      }
  1.2050 +
  1.2051 +      if (!AppsUtils.checkManifest(app.manifest, app)) {
  1.2052 +        sendError("INVALID_MANIFEST");
  1.2053 +        return false;
  1.2054 +      }
  1.2055 +
  1.2056 +      if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) {
  1.2057 +        sendError("INSTALL_FROM_DENIED");
  1.2058 +        return false;
  1.2059 +      }
  1.2060 +
  1.2061 +      if (!checkAppStatus(app.manifest)) {
  1.2062 +        sendError("INVALID_SECURITY_LEVEL");
  1.2063 +        return false;
  1.2064 +      }
  1.2065 +
  1.2066 +      return true;
  1.2067 +    }).bind(this);
  1.2068 +
  1.2069 +    let installApp = (function() {
  1.2070 +      app.manifestHash = this.computeManifestHash(app.manifest);
  1.2071 +      // We allow bypassing the install confirmation process to facilitate
  1.2072 +      // automation.
  1.2073 +      let prefName = "dom.mozApps.auto_confirm_install";
  1.2074 +      if (Services.prefs.prefHasUserValue(prefName) &&
  1.2075 +          Services.prefs.getBoolPref(prefName)) {
  1.2076 +        this.confirmInstall(aData);
  1.2077 +      } else {
  1.2078 +        Services.obs.notifyObservers(aMm, "webapps-ask-install",
  1.2079 +                                     JSON.stringify(aData));
  1.2080 +      }
  1.2081 +    }).bind(this);
  1.2082 +
  1.2083 +    // We may already have the manifest (e.g. AutoInstall),
  1.2084 +    // in which case we don't need to load it.
  1.2085 +    if (app.manifest) {
  1.2086 +      if (checkManifest()) {
  1.2087 +        installApp();
  1.2088 +      }
  1.2089 +      return;
  1.2090 +    }
  1.2091 +
  1.2092 +    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
  1.2093 +                .createInstance(Ci.nsIXMLHttpRequest);
  1.2094 +    xhr.open("GET", app.manifestURL, true);
  1.2095 +    xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  1.2096 +    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
  1.2097 +                                                               aData.isBrowser);
  1.2098 +    xhr.responseType = "json";
  1.2099 +
  1.2100 +    xhr.addEventListener("load", (function() {
  1.2101 +      if (xhr.status == 200) {
  1.2102 +        if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
  1.2103 +                                                xhr.getResponseHeader("content-type"))) {
  1.2104 +          sendError("INVALID_MANIFEST");
  1.2105 +          return;
  1.2106 +        }
  1.2107 +
  1.2108 +        app.manifest = xhr.response;
  1.2109 +        if (checkManifest()) {
  1.2110 +          app.etag = xhr.getResponseHeader("Etag");
  1.2111 +          installApp();
  1.2112 +        }
  1.2113 +      } else {
  1.2114 +        sendError("MANIFEST_URL_ERROR");
  1.2115 +      }
  1.2116 +    }).bind(this), false);
  1.2117 +
  1.2118 +    xhr.addEventListener("error", (function() {
  1.2119 +      sendError("NETWORK_ERROR");
  1.2120 +    }).bind(this), false);
  1.2121 +
  1.2122 +    xhr.send(null);
  1.2123 +  },
  1.2124 +
  1.2125 +  doInstallPackage: function doInstallPackage(aData, aMm) {
  1.2126 +    let app = aData.app;
  1.2127 +
  1.2128 +    let sendError = function sendError(aError) {
  1.2129 +      aData.error = aError;
  1.2130 +      aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
  1.2131 +      Cu.reportError("Error installing packaged app from: " +
  1.2132 +                     app.installOrigin + ": " + aError);
  1.2133 +    }.bind(this);
  1.2134 +
  1.2135 +    if (app.receipts.length > 0) {
  1.2136 +      for (let receipt of app.receipts) {
  1.2137 +        let error = this.isReceipt(receipt);
  1.2138 +        if (error) {
  1.2139 +          sendError(error);
  1.2140 +          return;
  1.2141 +        }
  1.2142 +      }
  1.2143 +    }
  1.2144 +
  1.2145 +    let checkUpdateManifest = (function() {
  1.2146 +      let manifest = app.updateManifest;
  1.2147 +
  1.2148 +      // Disallow reinstalls from the same manifest URL for now.
  1.2149 +      let id = this._appIdForManifestURL(app.manifestURL);
  1.2150 +      if (id !== null && this._isLaunchable(this.webapps[id])) {
  1.2151 +        sendError("REINSTALL_FORBIDDEN");
  1.2152 +        return false;
  1.2153 +      }
  1.2154 +
  1.2155 +      if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) {
  1.2156 +        sendError("INVALID_MANIFEST");
  1.2157 +        return false;
  1.2158 +      }
  1.2159 +
  1.2160 +      if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
  1.2161 +        sendError("INSTALL_FROM_DENIED");
  1.2162 +        return false;
  1.2163 +      }
  1.2164 +
  1.2165 +      return true;
  1.2166 +    }).bind(this);
  1.2167 +
  1.2168 +    let installApp = (function() {
  1.2169 +      app.manifestHash = this.computeManifestHash(app.updateManifest);
  1.2170 +
  1.2171 +      // We allow bypassing the install confirmation process to facilitate
  1.2172 +      // automation.
  1.2173 +      let prefName = "dom.mozApps.auto_confirm_install";
  1.2174 +      if (Services.prefs.prefHasUserValue(prefName) &&
  1.2175 +          Services.prefs.getBoolPref(prefName)) {
  1.2176 +        this.confirmInstall(aData);
  1.2177 +      } else {
  1.2178 +        Services.obs.notifyObservers(aMm, "webapps-ask-install",
  1.2179 +                                     JSON.stringify(aData));
  1.2180 +      }
  1.2181 +    }).bind(this);
  1.2182 +
  1.2183 +    // We may already have the manifest (e.g. AutoInstall),
  1.2184 +    // in which case we don't need to load it.
  1.2185 +    if (app.updateManifest) {
  1.2186 +      if (checkUpdateManifest()) {
  1.2187 +        installApp();
  1.2188 +      }
  1.2189 +      return;
  1.2190 +    }
  1.2191 +
  1.2192 +    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
  1.2193 +                .createInstance(Ci.nsIXMLHttpRequest);
  1.2194 +    xhr.open("GET", app.manifestURL, true);
  1.2195 +    xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  1.2196 +    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
  1.2197 +                                                               aData.isBrowser);
  1.2198 +    xhr.responseType = "json";
  1.2199 +
  1.2200 +    xhr.addEventListener("load", (function() {
  1.2201 +      if (xhr.status == 200) {
  1.2202 +        if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
  1.2203 +                                                xhr.getResponseHeader("content-type"))) {
  1.2204 +          sendError("INVALID_MANIFEST");
  1.2205 +          return;
  1.2206 +        }
  1.2207 +
  1.2208 +        app.updateManifest = xhr.response;
  1.2209 +        if (!app.updateManifest) {
  1.2210 +          sendError("MANIFEST_PARSE_ERROR");
  1.2211 +          return;
  1.2212 +        }
  1.2213 +        if (checkUpdateManifest()) {
  1.2214 +          app.etag = xhr.getResponseHeader("Etag");
  1.2215 +          debug("at install package got app etag=" + app.etag);
  1.2216 +          installApp();
  1.2217 +        }
  1.2218 +      }
  1.2219 +      else {
  1.2220 +        sendError("MANIFEST_URL_ERROR");
  1.2221 +      }
  1.2222 +    }).bind(this), false);
  1.2223 +
  1.2224 +    xhr.addEventListener("error", (function() {
  1.2225 +      sendError("NETWORK_ERROR");
  1.2226 +    }).bind(this), false);
  1.2227 +
  1.2228 +    xhr.send(null);
  1.2229 +  },
  1.2230 +
  1.2231 +  denyInstall: function(aData) {
  1.2232 +    let packageId = aData.app.packageId;
  1.2233 +    if (packageId) {
  1.2234 +      let dir = FileUtils.getDir("TmpD", ["webapps", packageId],
  1.2235 +                                 true, true);
  1.2236 +      try {
  1.2237 +        dir.remove(true);
  1.2238 +      } catch(e) {
  1.2239 +      }
  1.2240 +    }
  1.2241 +    aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
  1.2242 +  },
  1.2243 +
  1.2244 +  // This function is called after we called the onsuccess callback on the
  1.2245 +  // content side. This let the webpage the opportunity to set event handlers
  1.2246 +  // on the app before we start firing progress events.
  1.2247 +  queuedDownload: {},
  1.2248 +  queuedPackageDownload: {},
  1.2249 +
  1.2250 +onInstallSuccessAck: function onInstallSuccessAck(aManifestURL,
  1.2251 +                                                  aDontNeedNetwork) {
  1.2252 +    // If we are offline, register to run when we'll be online.
  1.2253 +    if ((Services.io.offline) && !aDontNeedNetwork) {
  1.2254 +      let onlineWrapper = {
  1.2255 +        observe: function(aSubject, aTopic, aData) {
  1.2256 +          Services.obs.removeObserver(onlineWrapper,
  1.2257 +                                      "network:offline-status-changed");
  1.2258 +          DOMApplicationRegistry.onInstallSuccessAck(aManifestURL);
  1.2259 +        }
  1.2260 +      };
  1.2261 +      Services.obs.addObserver(onlineWrapper,
  1.2262 +                               "network:offline-status-changed", false);
  1.2263 +      return;
  1.2264 +    }
  1.2265 +
  1.2266 +    let cacheDownload = this.queuedDownload[aManifestURL];
  1.2267 +    if (cacheDownload) {
  1.2268 +      this.startOfflineCacheDownload(cacheDownload.manifest,
  1.2269 +                                     cacheDownload.app,
  1.2270 +                                     cacheDownload.profileDir);
  1.2271 +      delete this.queuedDownload[aManifestURL];
  1.2272 +
  1.2273 +      return;
  1.2274 +    }
  1.2275 +
  1.2276 +    let packageDownload = this.queuedPackageDownload[aManifestURL];
  1.2277 +    if (packageDownload) {
  1.2278 +      let manifest = packageDownload.manifest;
  1.2279 +      let newApp = packageDownload.app;
  1.2280 +      let installSuccessCallback = packageDownload.callback;
  1.2281 +
  1.2282 +      delete this.queuedPackageDownload[aManifestURL];
  1.2283 +
  1.2284 +      this.downloadPackage(manifest, newApp, false).then(
  1.2285 +        this._onDownloadPackage.bind(this, newApp, installSuccessCallback)
  1.2286 +      );
  1.2287 +    }
  1.2288 +  },
  1.2289 +
  1.2290 +  _setupApp: function(aData, aId) {
  1.2291 +    let app = aData.app;
  1.2292 +
  1.2293 +    // app can be uninstalled
  1.2294 +    app.removable = true;
  1.2295 +
  1.2296 +    if (aData.isPackage) {
  1.2297 +      // Override the origin with the correct id.
  1.2298 +      app.origin = "app://" + aId;
  1.2299 +    }
  1.2300 +
  1.2301 +    app.id = aId;
  1.2302 +    app.installTime = Date.now();
  1.2303 +    app.lastUpdateCheck = Date.now();
  1.2304 +
  1.2305 +    return app;
  1.2306 +  },
  1.2307 +
  1.2308 +  _cloneApp: function(aData, aNewApp, aManifest, aId, aLocalId) {
  1.2309 +    let appObject = AppsUtils.cloneAppObject(aNewApp);
  1.2310 +    appObject.appStatus =
  1.2311 +      aNewApp.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
  1.2312 +
  1.2313 +    if (aManifest.appcache_path) {
  1.2314 +      appObject.installState = "pending";
  1.2315 +      appObject.downloadAvailable = true;
  1.2316 +      appObject.downloading = true;
  1.2317 +      appObject.downloadSize = 0;
  1.2318 +      appObject.readyToApplyDownload = false;
  1.2319 +    } else if (aManifest.package_path) {
  1.2320 +      appObject.installState = "pending";
  1.2321 +      appObject.downloadAvailable = true;
  1.2322 +      appObject.downloading = true;
  1.2323 +      appObject.downloadSize = aManifest.size;
  1.2324 +      appObject.readyToApplyDownload = false;
  1.2325 +    } else {
  1.2326 +      appObject.installState = "installed";
  1.2327 +      appObject.downloadAvailable = false;
  1.2328 +      appObject.downloading = false;
  1.2329 +      appObject.readyToApplyDownload = false;
  1.2330 +    }
  1.2331 +
  1.2332 +    appObject.localId = aLocalId;
  1.2333 +    appObject.basePath = OS.Path.dirname(this.appsFile);
  1.2334 +    appObject.name = aManifest.name;
  1.2335 +    appObject.csp = aManifest.csp || "";
  1.2336 +    appObject.role = aManifest.role || "";
  1.2337 +    appObject.installerAppId = aData.appId;
  1.2338 +    appObject.installerIsBrowser = aData.isBrowser;
  1.2339 +
  1.2340 +    return appObject;
  1.2341 +  },
  1.2342 +
  1.2343 +  _writeManifestFile: function(aId, aIsPackage, aJsonManifest) {
  1.2344 +    debug("_writeManifestFile");
  1.2345 +
  1.2346 +    // For packaged apps, keep the update manifest distinct from the app manifest.
  1.2347 +    let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp";
  1.2348 +
  1.2349 +    let dir = this._getAppDir(aId).path;
  1.2350 +    let manFile = OS.Path.join(dir, manifestName);
  1.2351 +    this._writeFile(manFile, JSON.stringify(aJsonManifest));
  1.2352 +  },
  1.2353 +
  1.2354 +  // Add an app that is already installed to the registry.
  1.2355 +  addInstalledApp: Task.async(function*(aApp, aManifest, aUpdateManifest) {
  1.2356 +    if (this.getAppLocalIdByManifestURL(aApp.manifestURL) !=
  1.2357 +        Ci.nsIScriptSecurityManager.NO_APP_ID) {
  1.2358 +      return;
  1.2359 +    }
  1.2360 +
  1.2361 +    let app = AppsUtils.cloneAppObject(aApp);
  1.2362 +
  1.2363 +    if (!AppsUtils.checkManifest(aManifest, app) ||
  1.2364 +        (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) {
  1.2365 +      return;
  1.2366 +    }
  1.2367 +
  1.2368 +    app.name = aManifest.name;
  1.2369 +
  1.2370 +    app.csp = aManifest.csp || "";
  1.2371 +
  1.2372 +    app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
  1.2373 +
  1.2374 +    app.removable = true;
  1.2375 +
  1.2376 +    // Reuse the app ID if the scheme is "app".
  1.2377 +    let uri = Services.io.newURI(app.origin, null, null);
  1.2378 +    if (uri.scheme == "app") {
  1.2379 +      app.id = uri.host;
  1.2380 +    } else {
  1.2381 +      app.id = this.makeAppId();
  1.2382 +    }
  1.2383 +
  1.2384 +    app.localId = this._nextLocalId();
  1.2385 +
  1.2386 +    app.basePath = OS.Path.dirname(this.appsFile);
  1.2387 +
  1.2388 +    app.progress = 0.0;
  1.2389 +    app.installState = "installed";
  1.2390 +    app.downloadAvailable = false;
  1.2391 +    app.downloading = false;
  1.2392 +    app.readyToApplyDownload = false;
  1.2393 +
  1.2394 +    if (aUpdateManifest && aUpdateManifest.size) {
  1.2395 +      app.downloadSize = aUpdateManifest.size;
  1.2396 +    }
  1.2397 +
  1.2398 +    app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest ||
  1.2399 +                                                            aManifest));
  1.2400 +
  1.2401 +    let zipFile = WebappOSUtils.getPackagePath(app);
  1.2402 +    app.packageHash = yield this._computeFileHash(zipFile);
  1.2403 +
  1.2404 +    app.role = aManifest.role || "";
  1.2405 +
  1.2406 +    app.redirects = this.sanitizeRedirects(aManifest.redirects);
  1.2407 +
  1.2408 +    this.webapps[app.id] = app;
  1.2409 +
  1.2410 +    // Store the manifest in the manifest cache, so we don't need to re-read it
  1.2411 +    this._manifestCache[app.id] = app.manifest;
  1.2412 +
  1.2413 +    // Store the manifest and the updateManifest.
  1.2414 +    this._writeManifestFile(app.id, false, aManifest);
  1.2415 +    if (aUpdateManifest) {
  1.2416 +      this._writeManifestFile(app.id, true, aUpdateManifest);
  1.2417 +    }
  1.2418 +
  1.2419 +    this._saveApps().then(() => {
  1.2420 +      this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app });
  1.2421 +    });
  1.2422 +  }),
  1.2423 +
  1.2424 +  confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) {
  1.2425 +    debug("confirmInstall");
  1.2426 +
  1.2427 +    let origin = Services.io.newURI(aData.app.origin, null, null);
  1.2428 +    let id = this._appIdForManifestURL(aData.app.manifestURL);
  1.2429 +    let manifestURL = origin.resolve(aData.app.manifestURL);
  1.2430 +    let localId = this.getAppLocalIdByManifestURL(manifestURL);
  1.2431 +
  1.2432 +    let isReinstall = false;
  1.2433 +
  1.2434 +    // Installing an application again is considered as an update.
  1.2435 +    if (id) {
  1.2436 +      isReinstall = true;
  1.2437 +      let dir = this._getAppDir(id);
  1.2438 +      try {
  1.2439 +        dir.remove(true);
  1.2440 +      } catch(e) { }
  1.2441 +    } else {
  1.2442 +      id = this.makeAppId();
  1.2443 +      localId = this._nextLocalId();
  1.2444 +    }
  1.2445 +
  1.2446 +    let app = this._setupApp(aData, id);
  1.2447 +
  1.2448 +    let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
  1.2449 +    this._writeManifestFile(id, aData.isPackage, jsonManifest);
  1.2450 +
  1.2451 +    debug("app.origin: " + app.origin);
  1.2452 +    let manifest = new ManifestHelper(jsonManifest, app.origin);
  1.2453 +
  1.2454 +    let appObject = this._cloneApp(aData, app, manifest, id, localId);
  1.2455 +
  1.2456 +    this.webapps[id] = appObject;
  1.2457 +
  1.2458 +    // For package apps, the permissions are not in the mini-manifest, so
  1.2459 +    // don't update the permissions yet.
  1.2460 +    if (!aData.isPackage) {
  1.2461 +      if (supportUseCurrentProfile()) {
  1.2462 +        PermissionsInstaller.installPermissions(
  1.2463 +          {
  1.2464 +            origin: appObject.origin,
  1.2465 +            manifestURL: appObject.manifestURL,
  1.2466 +            manifest: jsonManifest
  1.2467 +          },
  1.2468 +          isReinstall,
  1.2469 +          this.uninstall.bind(this, aData, aData.mm)
  1.2470 +        );
  1.2471 +      }
  1.2472 +
  1.2473 +      this.updateDataStore(this.webapps[id].localId,  this.webapps[id].origin,
  1.2474 +                           this.webapps[id].manifestURL, jsonManifest,
  1.2475 +                           this.webapps[id].appStatus);
  1.2476 +    }
  1.2477 +
  1.2478 +    for each (let prop in ["installState", "downloadAvailable", "downloading",
  1.2479 +                           "downloadSize", "readyToApplyDownload"]) {
  1.2480 +      aData.app[prop] = appObject[prop];
  1.2481 +    }
  1.2482 +
  1.2483 +    if (manifest.appcache_path) {
  1.2484 +      this.queuedDownload[app.manifestURL] = {
  1.2485 +        manifest: manifest,
  1.2486 +        app: appObject,
  1.2487 +        profileDir: aProfileDir
  1.2488 +      }
  1.2489 +    }
  1.2490 +
  1.2491 +    // We notify about the successful installation via mgmt.oninstall and the
  1.2492 +    // corresponging DOMRequest.onsuccess event as soon as the app is properly
  1.2493 +    // saved in the registry.
  1.2494 +    this._saveApps().then(() => {
  1.2495 +      this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
  1.2496 +      if (aData.isPackage && aData.apkInstall && !aData.requestID) {
  1.2497 +        // Skip directly to onInstallSuccessAck, since there isn't
  1.2498 +        // a WebappsRegistry to receive Webapps:Install:Return:OK and respond
  1.2499 +        // Webapps:Install:Return:Ack when an app is being auto-installed.
  1.2500 +        this.onInstallSuccessAck(app.manifestURL);
  1.2501 +      } else {
  1.2502 +        // Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify
  1.2503 +        // the installing page about the successful install, after which it'll
  1.2504 +        // respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck.
  1.2505 +        this.broadcastMessage("Webapps:Install:Return:OK", aData);
  1.2506 +      }
  1.2507 +      if (!aData.isPackage) {
  1.2508 +        this.updateAppHandlers(null, app.manifest, app);
  1.2509 +        if (aInstallSuccessCallback) {
  1.2510 +          aInstallSuccessCallback(app.manifest);
  1.2511 +        }
  1.2512 +      }
  1.2513 +      Services.obs.notifyObservers(null, "webapps-installed",
  1.2514 +        JSON.stringify({ manifestURL: app.manifestURL }));
  1.2515 +    });
  1.2516 +
  1.2517 +    let dontNeedNetwork = false;
  1.2518 +    if (manifest.package_path) {
  1.2519 +      // If it is a local app then it must been installed from a local file
  1.2520 +      // instead of web.
  1.2521 +#ifdef MOZ_ANDROID_SYNTHAPKS
  1.2522 +      // In that case, we would already have the manifest, not just the update
  1.2523 +      // manifest.
  1.2524 +      dontNeedNetwork = !!aData.app.manifest;
  1.2525 +#else
  1.2526 +      if (aData.app.localInstallPath) {
  1.2527 +        dontNeedNetwork = true;
  1.2528 +        jsonManifest.package_path = "file://" + aData.app.localInstallPath;
  1.2529 +      }
  1.2530 +#endif
  1.2531 +
  1.2532 +      // origin for install apps is meaningless here, since it's app:// and this
  1.2533 +      // can't be used to resolve package paths.
  1.2534 +      manifest = new ManifestHelper(jsonManifest, app.manifestURL);
  1.2535 +
  1.2536 +      this.queuedPackageDownload[app.manifestURL] = {
  1.2537 +        manifest: manifest,
  1.2538 +        app: appObject,
  1.2539 +        callback: aInstallSuccessCallback
  1.2540 +      };
  1.2541 +    }
  1.2542 +
  1.2543 +    if (aData.forceSuccessAck) {
  1.2544 +      // If it's a local install, there's no content process so just
  1.2545 +      // ack the install.
  1.2546 +      this.onInstallSuccessAck(app.manifestURL, dontNeedNetwork);
  1.2547 +    }
  1.2548 +  },
  1.2549 +
  1.2550 +/**
  1.2551 +   * Install the package after successfully downloading it
  1.2552 +   *
  1.2553 +   * Bound params:
  1.2554 +   *
  1.2555 +   * @param aNewApp {Object} the new app data
  1.2556 +   * @param aInstallSuccessCallback {Function}
  1.2557 +   *        the callback to call on install success
  1.2558 +   *
  1.2559 +   * Passed params:
  1.2560 +   *
  1.2561 +   * @param aId {Integer} the unique ID of the application
  1.2562 +   * @param aManifest {Object} The manifest of the application
  1.2563 +   */
  1.2564 +  _onDownloadPackage: Task.async(function*(aNewApp, aInstallSuccessCallback,
  1.2565 +                               [aId, aManifest]) {
  1.2566 +    debug("_onDownloadPackage");
  1.2567 +    // Success! Move the zip out of TmpD.
  1.2568 +    let app = this.webapps[aId];
  1.2569 +    let zipFile =
  1.2570 +      FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
  1.2571 +    let dir = this._getAppDir(aId);
  1.2572 +    zipFile.moveTo(dir, "application.zip");
  1.2573 +    let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
  1.2574 +    try {
  1.2575 +      tmpDir.remove(true);
  1.2576 +    } catch(e) { }
  1.2577 +
  1.2578 +    // Save the manifest
  1.2579 +    let manFile = OS.Path.join(dir.path, "manifest.webapp");
  1.2580 +    yield this._writeFile(manFile, JSON.stringify(aManifest));
  1.2581 +    // Set state and fire events.
  1.2582 +    app.installState = "installed";
  1.2583 +    app.downloading = false;
  1.2584 +    app.downloadAvailable = false;
  1.2585 +
  1.2586 +    yield this._saveApps();
  1.2587 +
  1.2588 +    this.updateAppHandlers(null, aManifest, aNewApp);
  1.2589 +    // Clear the manifest cache in case it holds the update manifest.
  1.2590 +    if (aId in this._manifestCache) {
  1.2591 +      delete this._manifestCache[aId];
  1.2592 +    }
  1.2593 +
  1.2594 +    this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp });
  1.2595 +    Services.obs.notifyObservers(null, "webapps-installed",
  1.2596 +      JSON.stringify({ manifestURL: aNewApp.manifestURL }));
  1.2597 +
  1.2598 +    if (supportUseCurrentProfile()) {
  1.2599 +      // Update the permissions for this app.
  1.2600 +      PermissionsInstaller.installPermissions({
  1.2601 +        manifest: aManifest,
  1.2602 +        origin: aNewApp.origin,
  1.2603 +        manifestURL: aNewApp.manifestURL
  1.2604 +      }, true);
  1.2605 +    }
  1.2606 +
  1.2607 +    this.updateDataStore(this.webapps[aId].localId, aNewApp.origin,
  1.2608 +                         aNewApp.manifestURL, aManifest, aNewApp.appStatus);
  1.2609 +
  1.2610 +    this.broadcastMessage("Webapps:UpdateState", {
  1.2611 +      app: app,
  1.2612 +      manifest: aManifest,
  1.2613 +      manifestURL: aNewApp.manifestURL
  1.2614 +    });
  1.2615 +
  1.2616 +    // Check if we have asm.js code to preload for this application.
  1.2617 +    yield ScriptPreloader.preload(aNewApp, aManifest);
  1.2618 +
  1.2619 +    this.broadcastMessage("Webapps:FireEvent", {
  1.2620 +      eventType: ["downloadsuccess", "downloadapplied"],
  1.2621 +      manifestURL: aNewApp.manifestURL
  1.2622 +    });
  1.2623 +
  1.2624 +    if (aInstallSuccessCallback) {
  1.2625 +      aInstallSuccessCallback(aManifest, zipFile.path);
  1.2626 +    }
  1.2627 +  }),
  1.2628 +
  1.2629 +  _nextLocalId: function() {
  1.2630 +    let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
  1.2631 +
  1.2632 +    while (this.getManifestURLByLocalId(id)) {
  1.2633 +      id++;
  1.2634 +    }
  1.2635 +
  1.2636 +    Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
  1.2637 +    Services.prefs.savePrefFile(null);
  1.2638 +    return id;
  1.2639 +  },
  1.2640 +
  1.2641 +  _appIdForManifestURL: function(aURI) {
  1.2642 +    for (let id in this.webapps) {
  1.2643 +      if (this.webapps[id].manifestURL == aURI)
  1.2644 +        return id;
  1.2645 +    }
  1.2646 +    return null;
  1.2647 +  },
  1.2648 +
  1.2649 +  makeAppId: function() {
  1.2650 +    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
  1.2651 +    return uuidGenerator.generateUUID().toString();
  1.2652 +  },
  1.2653 +
  1.2654 +  _saveApps: function() {
  1.2655 +    return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2));
  1.2656 +  },
  1.2657 +
  1.2658 +  /**
  1.2659 +    * Asynchronously reads a list of manifests
  1.2660 +    */
  1.2661 +
  1.2662 +  _manifestCache: {},
  1.2663 +
  1.2664 +  _readManifests: function(aData) {
  1.2665 +    return Task.spawn(function*() {
  1.2666 +      for (let elem of aData) {
  1.2667 +        let id = elem.id;
  1.2668 +
  1.2669 +        if (!this._manifestCache[id]) {
  1.2670 +          // the manifest file used to be named manifest.json, so fallback on this.
  1.2671 +          let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath()
  1.2672 +                          ? "coreAppsDir" : DIRECTORY_NAME;
  1.2673 +
  1.2674 +          let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true);
  1.2675 +
  1.2676 +          let fileNames = ["manifest.webapp", "update.webapp", "manifest.json"];
  1.2677 +          for (let fileName of fileNames) {
  1.2678 +            this._manifestCache[id] = yield AppsUtils.loadJSONAsync(OS.Path.join(dir.path, fileName));
  1.2679 +            if (this._manifestCache[id]) {
  1.2680 +              break;
  1.2681 +            }
  1.2682 +          }
  1.2683 +        }
  1.2684 +
  1.2685 +        elem.manifest = this._manifestCache[id];
  1.2686 +      }
  1.2687 +
  1.2688 +      return aData;
  1.2689 +    }.bind(this)).then(null, Cu.reportError);
  1.2690 +  },
  1.2691 +
  1.2692 +  downloadPackage: function(aManifest, aNewApp, aIsUpdate, aOnSuccess) {
  1.2693 +    // Here are the steps when installing a package:
  1.2694 +    // - create a temp directory where to store the app.
  1.2695 +    // - download the zip in this directory.
  1.2696 +    // - check the signature on the zip.
  1.2697 +    // - extract the manifest from the zip and check it.
  1.2698 +    // - ask confirmation to the user.
  1.2699 +    // - add the new app to the registry.
  1.2700 +    // If we fail at any step, we revert the previous ones and return an error.
  1.2701 +
  1.2702 +    // We define these outside the task to use them in its reject handler.
  1.2703 +    let id = this._appIdForManifestURL(aNewApp.manifestURL);
  1.2704 +    let oldApp = this.webapps[id];
  1.2705 +
  1.2706 +    return Task.spawn((function*() {
  1.2707 +      yield this._ensureSufficientStorage(aNewApp);
  1.2708 +
  1.2709 +      let fullPackagePath = aManifest.fullPackagePath();
  1.2710 +
  1.2711 +      // Check if it's a local file install (we've downloaded/sideloaded the
  1.2712 +      // package already, it existed on the build, or it came with an APK).
  1.2713 +      // Note that this variable also controls whether files signed with expired
  1.2714 +      // certificates are accepted or not. If isLocalFileInstall is true and the
  1.2715 +      // device date is earlier than the build generation date, then the signature
  1.2716 +      // will be accepted even if the certificate is expired.
  1.2717 +      let isLocalFileInstall =
  1.2718 +        Services.io.extractScheme(fullPackagePath) === 'file';
  1.2719 +
  1.2720 +      debug("About to download " + fullPackagePath);
  1.2721 +
  1.2722 +      let requestChannel = this._getRequestChannel(fullPackagePath,
  1.2723 +                                                   isLocalFileInstall,
  1.2724 +                                                   oldApp,
  1.2725 +                                                   aNewApp);
  1.2726 +
  1.2727 +      AppDownloadManager.add(
  1.2728 +        aNewApp.manifestURL,
  1.2729 +        {
  1.2730 +          channel: requestChannel,
  1.2731 +          appId: id,
  1.2732 +          previousState: aIsUpdate ? "installed" : "pending"
  1.2733 +        }
  1.2734 +      );
  1.2735 +
  1.2736 +      // We set the 'downloading' flag to true right before starting the fetch.
  1.2737 +      oldApp.downloading = true;
  1.2738 +
  1.2739 +      // We determine the app's 'installState' according to its previous
  1.2740 +      // state. Cancelled download should remain as 'pending'. Successfully
  1.2741 +      // installed apps should morph to 'updating'.
  1.2742 +      oldApp.installState = aIsUpdate ? "updating" : "pending";
  1.2743 +
  1.2744 +      // initialize the progress to 0 right now
  1.2745 +      oldApp.progress = 0;
  1.2746 +
  1.2747 +      let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp);
  1.2748 +      let hash = yield this._computeFileHash(zipFile.path);
  1.2749 +
  1.2750 +      let responseStatus = requestChannel.responseStatus;
  1.2751 +      let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash);
  1.2752 +
  1.2753 +      if (oldPackage) {
  1.2754 +        debug("package's etag or hash unchanged; sending 'applied' event");
  1.2755 +        // The package's Etag or hash has not changed.
  1.2756 +        // We send a "applied" event right away.
  1.2757 +        this._sendAppliedEvent(aNewApp, oldApp, id);
  1.2758 +        return;
  1.2759 +      }
  1.2760 +
  1.2761 +      let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp,
  1.2762 +              isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash);
  1.2763 +
  1.2764 +      AppDownloadManager.remove(aNewApp.manifestURL);
  1.2765 +
  1.2766 +      return [oldApp.id, newManifest];
  1.2767 +
  1.2768 +    }).bind(this)).then(
  1.2769 +      aOnSuccess,
  1.2770 +      this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate)
  1.2771 +    );
  1.2772 +  },
  1.2773 +
  1.2774 +  _ensureSufficientStorage: function(aNewApp) {
  1.2775 +    let deferred = Promise.defer();
  1.2776 +
  1.2777 +    let navigator = Services.wm.getMostRecentWindow(chromeWindowType)
  1.2778 +                            .navigator;
  1.2779 +    let deviceStorage = null;
  1.2780 +
  1.2781 +    if (navigator.getDeviceStorage) {
  1.2782 +      deviceStorage = navigator.getDeviceStorage("apps");
  1.2783 +    }
  1.2784 +
  1.2785 +    if (deviceStorage) {
  1.2786 +      let req = deviceStorage.freeSpace();
  1.2787 +      req.onsuccess = req.onerror = e => {
  1.2788 +        let freeBytes = e.target.result;
  1.2789 +        let sufficientStorage = this._checkDownloadSize(freeBytes, aNewApp);
  1.2790 +        if (sufficientStorage) {
  1.2791 +          deferred.resolve();
  1.2792 +        } else {
  1.2793 +          deferred.reject("INSUFFICIENT_STORAGE");
  1.2794 +        }
  1.2795 +      }
  1.2796 +    } else {
  1.2797 +      debug("No deviceStorage");
  1.2798 +      // deviceStorage isn't available, so use FileUtils to find the size of
  1.2799 +      // available storage.
  1.2800 +      let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true);
  1.2801 +      try {
  1.2802 +        let sufficientStorage = this._checkDownloadSize(dir.diskSpaceAvailable,
  1.2803 +                                                        aNewApp);
  1.2804 +        if (sufficientStorage) {
  1.2805 +          deferred.resolve();
  1.2806 +        } else {
  1.2807 +          deferred.reject("INSUFFICIENT_STORAGE");
  1.2808 +        }
  1.2809 +      } catch(ex) {
  1.2810 +        // If disk space information isn't available, we'll end up here.
  1.2811 +        // We should proceed anyway, otherwise devices that support neither
  1.2812 +        // deviceStorage nor diskSpaceAvailable will never be able to install
  1.2813 +        // packaged apps.
  1.2814 +        deferred.resolve();
  1.2815 +      }
  1.2816 +    }
  1.2817 +
  1.2818 +    return deferred.promise;
  1.2819 +  },
  1.2820 +
  1.2821 +  _checkDownloadSize: function(aFreeBytes, aNewApp) {
  1.2822 +    if (aFreeBytes) {
  1.2823 +      debug("Free storage: " + aFreeBytes + ". Download size: " +
  1.2824 +            aNewApp.downloadSize);
  1.2825 +      if (aFreeBytes <=
  1.2826 +          aNewApp.downloadSize + AppDownloadManager.MIN_REMAINING_FREESPACE) {
  1.2827 +        return false;
  1.2828 +      }
  1.2829 +    }
  1.2830 +    return true;
  1.2831 +  },
  1.2832 +
  1.2833 +  _getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp,
  1.2834 +                               aNewApp) {
  1.2835 +    let requestChannel;
  1.2836 +
  1.2837 +    if (aIsLocalFileInstall) {
  1.2838 +      requestChannel = NetUtil.newChannel(aFullPackagePath)
  1.2839 +                              .QueryInterface(Ci.nsIFileChannel);
  1.2840 +    } else {
  1.2841 +      requestChannel = NetUtil.newChannel(aFullPackagePath)
  1.2842 +                              .QueryInterface(Ci.nsIHttpChannel);
  1.2843 +      requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  1.2844 +    }
  1.2845 +
  1.2846 +    if (aOldApp.packageEtag && !aIsLocalFileInstall) {
  1.2847 +      debug("Add If-None-Match header: " + aOldApp.packageEtag);
  1.2848 +      requestChannel.setRequestHeader("If-None-Match", aOldApp.packageEtag,
  1.2849 +                                      false);
  1.2850 +    }
  1.2851 +
  1.2852 +    let lastProgressTime = 0;
  1.2853 +
  1.2854 +    requestChannel.notificationCallbacks = {
  1.2855 +      QueryInterface: function(aIID) {
  1.2856 +        if (aIID.equals(Ci.nsISupports)          ||
  1.2857 +            aIID.equals(Ci.nsIProgressEventSink) ||
  1.2858 +            aIID.equals(Ci.nsILoadContext))
  1.2859 +          return this;
  1.2860 +        throw Cr.NS_ERROR_NO_INTERFACE;
  1.2861 +      },
  1.2862 +      getInterface: function(aIID) {
  1.2863 +        return this.QueryInterface(aIID);
  1.2864 +      },
  1.2865 +      onProgress: (function(aRequest, aContext, aProgress, aProgressMax) {
  1.2866 +        aOldApp.progress = aProgress;
  1.2867 +        let now = Date.now();
  1.2868 +        if (now - lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
  1.2869 +          debug("onProgress: " + aProgress + "/" + aProgressMax);
  1.2870 +          this._sendDownloadProgressEvent(aNewApp, aProgress);
  1.2871 +          lastProgressTime = now;
  1.2872 +          this._saveApps();
  1.2873 +        }
  1.2874 +      }).bind(this),
  1.2875 +      onStatus: function(aRequest, aContext, aStatus, aStatusArg) { },
  1.2876 +
  1.2877 +      // nsILoadContext
  1.2878 +      appId: aOldApp.installerAppId,
  1.2879 +      isInBrowserElement: aOldApp.installerIsBrowser,
  1.2880 +      usePrivateBrowsing: false,
  1.2881 +      isContent: false,
  1.2882 +      associatedWindow: null,
  1.2883 +      topWindow : null,
  1.2884 +      isAppOfType: function(appType) {
  1.2885 +        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  1.2886 +      }
  1.2887 +    };
  1.2888 +
  1.2889 +    return requestChannel;
  1.2890 +  },
  1.2891 +
  1.2892 +  _sendDownloadProgressEvent: function(aNewApp, aProgress) {
  1.2893 +    this.broadcastMessage("Webapps:UpdateState", {
  1.2894 +      app: {
  1.2895 +        progress: aProgress
  1.2896 +      },
  1.2897 +      manifestURL: aNewApp.manifestURL
  1.2898 +    });
  1.2899 +    this.broadcastMessage("Webapps:FireEvent", {
  1.2900 +      eventType: "progress",
  1.2901 +      manifestURL: aNewApp.manifestURL
  1.2902 +    });
  1.2903 +  },
  1.2904 +
  1.2905 +  _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
  1.2906 +    let deferred = Promise.defer();
  1.2907 +
  1.2908 +    // Staging the zip in TmpD until all the checks are done.
  1.2909 +    let zipFile =
  1.2910 +      FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
  1.2911 +
  1.2912 +    // We need an output stream to write the channel content to the zip file.
  1.2913 +    let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
  1.2914 +                         .createInstance(Ci.nsIFileOutputStream);
  1.2915 +    // write, create, truncate
  1.2916 +    outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
  1.2917 +    let bufferedOutputStream =
  1.2918 +      Cc['@mozilla.org/network/buffered-output-stream;1']
  1.2919 +        .createInstance(Ci.nsIBufferedOutputStream);
  1.2920 +    bufferedOutputStream.init(outputStream, 1024);
  1.2921 +
  1.2922 +    // Create a listener that will give data to the file output stream.
  1.2923 +    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
  1.2924 +                     .createInstance(Ci.nsISimpleStreamListener);
  1.2925 +
  1.2926 +    listener.init(bufferedOutputStream, {
  1.2927 +      onStartRequest: function(aRequest, aContext) {
  1.2928 +        // Nothing to do there anymore.
  1.2929 +      },
  1.2930 +
  1.2931 +      onStopRequest: function(aRequest, aContext, aStatusCode) {
  1.2932 +        bufferedOutputStream.close();
  1.2933 +        outputStream.close();
  1.2934 +
  1.2935 +        if (!Components.isSuccessCode(aStatusCode)) {
  1.2936 +          deferred.reject("NETWORK_ERROR");
  1.2937 +          return;
  1.2938 +        }
  1.2939 +
  1.2940 +        // If we get a 4XX or a 5XX http status, bail out like if we had a
  1.2941 +        // network error.
  1.2942 +        let responseStatus = aRequestChannel.responseStatus;
  1.2943 +        if (responseStatus >= 400 && responseStatus <= 599) {
  1.2944 +          // unrecoverable error, don't bug the user
  1.2945 +          aOldApp.downloadAvailable = false;
  1.2946 +          deferred.reject("NETWORK_ERROR");
  1.2947 +          return;
  1.2948 +        }
  1.2949 +
  1.2950 +        deferred.resolve(zipFile);
  1.2951 +      }
  1.2952 +    });
  1.2953 +    aRequestChannel.asyncOpen(listener, null);
  1.2954 +
  1.2955 +    // send a first progress event to correctly set the DOM object's properties
  1.2956 +    this._sendDownloadProgressEvent(aNewApp, 0);
  1.2957 +
  1.2958 +    return deferred.promise;
  1.2959 +  },
  1.2960 +
  1.2961 +  /**
  1.2962 +   * Compute the MD5 hash of a file, doing async IO off the main thread.
  1.2963 +   *
  1.2964 +   * @param   {String} aFilePath
  1.2965 +   *                   the path of the file to hash
  1.2966 +   * @returns {String} the MD5 hash of the file
  1.2967 +   */
  1.2968 +  _computeFileHash: function(aFilePath) {
  1.2969 +    let deferred = Promise.defer();
  1.2970 +
  1.2971 +    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  1.2972 +    file.initWithPath(aFilePath);
  1.2973 +
  1.2974 +    NetUtil.asyncFetch(file, function(inputStream, status) {
  1.2975 +      if (!Components.isSuccessCode(status)) {
  1.2976 +        debug("Error reading " + aFilePath + ": " + e);
  1.2977 +        deferred.reject();
  1.2978 +        return;
  1.2979 +      }
  1.2980 +
  1.2981 +      let hasher = Cc["@mozilla.org/security/hash;1"]
  1.2982 +                     .createInstance(Ci.nsICryptoHash);
  1.2983 +      // We want to use the MD5 algorithm.
  1.2984 +      hasher.init(hasher.MD5);
  1.2985 +
  1.2986 +      const PR_UINT32_MAX = 0xffffffff;
  1.2987 +      hasher.updateFromStream(inputStream, PR_UINT32_MAX);
  1.2988 +
  1.2989 +      // Return the two-digit hexadecimal code for a byte.
  1.2990 +      function toHexString(charCode) {
  1.2991 +        return ("0" + charCode.toString(16)).slice(-2);
  1.2992 +      }
  1.2993 +
  1.2994 +      // We're passing false to get the binary hash and not base64.
  1.2995 +      let data = hasher.finish(false);
  1.2996 +      // Convert the binary hash data to a hex string.
  1.2997 +      let hash = [toHexString(data.charCodeAt(i)) for (i in data)].join("");
  1.2998 +      debug("File hash computed: " + hash);
  1.2999 +
  1.3000 +      deferred.resolve(hash);
  1.3001 +    });
  1.3002 +
  1.3003 +    return deferred.promise;
  1.3004 +  },
  1.3005 +
  1.3006 +  /**
  1.3007 +   * Send an "applied" event right away for the package being installed.
  1.3008 +   *
  1.3009 +   * XXX We use this to exit the app update process early when the downloaded
  1.3010 +   * package is identical to the last one we installed.  Presumably we do
  1.3011 +   * something similar after updating the app, and we could refactor both cases
  1.3012 +   * to use the same code to send the "applied" event.
  1.3013 +   *
  1.3014 +   * @param aNewApp {Object} the new app data
  1.3015 +   * @param aOldApp {Object} the currently stored app data
  1.3016 +   * @param aId {String} the unique id of the app
  1.3017 +   */
  1.3018 +  _sendAppliedEvent: function(aNewApp, aOldApp, aId) {
  1.3019 +    aOldApp.downloading = false;
  1.3020 +    aOldApp.downloadAvailable = false;
  1.3021 +    aOldApp.downloadSize = 0;
  1.3022 +    aOldApp.installState = "installed";
  1.3023 +    aOldApp.readyToApplyDownload = false;
  1.3024 +    if (aOldApp.staged && aOldApp.staged.manifestHash) {
  1.3025 +      // If we're here then the manifest has changed but the package
  1.3026 +      // hasn't. Let's clear this, so we don't keep offering
  1.3027 +      // a bogus update to the user
  1.3028 +      aOldApp.manifestHash = aOldApp.staged.manifestHash;
  1.3029 +      aOldApp.etag = aOldApp.staged.etag || aOldApp.etag;
  1.3030 +      aOldApp.staged = {};
  1.3031 +
  1.3032 +      // Move the staged update manifest to a non staged one.
  1.3033 +      try {
  1.3034 +        let staged = this._getAppDir(aId);
  1.3035 +        staged.append("staged-update.webapp");
  1.3036 +        staged.moveTo(staged.parent, "update.webapp");
  1.3037 +      } catch (ex) {
  1.3038 +        // We don't really mind much if this fails.
  1.3039 +      }
  1.3040 +    }
  1.3041 +
  1.3042 +    // Save the updated registry, and cleanup the tmp directory.
  1.3043 +    this._saveApps().then(() => {
  1.3044 +      this.broadcastMessage("Webapps:UpdateState", {
  1.3045 +        app: aOldApp,
  1.3046 +        manifestURL: aNewApp.manifestURL
  1.3047 +      });
  1.3048 +      this.broadcastMessage("Webapps:FireEvent", {
  1.3049 +        manifestURL: aNewApp.manifestURL,
  1.3050 +        eventType: ["downloadsuccess", "downloadapplied"]
  1.3051 +      });
  1.3052 +    });
  1.3053 +    let file = FileUtils.getFile("TmpD", ["webapps", aId], false);
  1.3054 +    if (file && file.exists()) {
  1.3055 +      file.remove(true);
  1.3056 +    }
  1.3057 +  },
  1.3058 +
  1.3059 +  _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall,
  1.3060 +                                aIsUpdate, aManifest, aRequestChannel, aHash) {
  1.3061 +    return Task.spawn((function*() {
  1.3062 +      let zipReader, isSigned, newManifest;
  1.3063 +
  1.3064 +      try {
  1.3065 +        [zipReader, isSigned] = yield this._openPackage(aZipFile, aOldApp,
  1.3066 +                                                        aIsLocalFileInstall);
  1.3067 +        newManifest = yield this._readPackage(aOldApp, aNewApp,
  1.3068 +                aIsLocalFileInstall, aIsUpdate, aManifest, aRequestChannel,
  1.3069 +                aHash, zipReader, isSigned);
  1.3070 +      } catch (e) {
  1.3071 +        debug("package open/read error: " + e);
  1.3072 +        // Something bad happened when opening/reading the package.
  1.3073 +        // Unrecoverable error, don't bug the user.
  1.3074 +        // Apps with installState 'pending' does not produce any
  1.3075 +        // notification, so we are safe with its current
  1.3076 +        // downloadAvailable state.
  1.3077 +        if (aOldApp.installState !== "pending") {
  1.3078 +          aOldApp.downloadAvailable = false;
  1.3079 +        }
  1.3080 +        if (typeof e == 'object') {
  1.3081 +          Cu.reportError("Error while reading package:" + e);
  1.3082 +          throw "INVALID_PACKAGE";
  1.3083 +        } else {
  1.3084 +          throw e;
  1.3085 +        }
  1.3086 +      } finally {
  1.3087 +        if (zipReader) {
  1.3088 +          zipReader.close();
  1.3089 +        }
  1.3090 +      }
  1.3091 +
  1.3092 +      return newManifest;
  1.3093 +
  1.3094 +    }).bind(this));
  1.3095 +  },
  1.3096 +
  1.3097 +  _openPackage: function(aZipFile, aApp, aIsLocalFileInstall) {
  1.3098 +    return Task.spawn((function*() {
  1.3099 +      let certDb;
  1.3100 +      try {
  1.3101 +        certDb = Cc["@mozilla.org/security/x509certdb;1"]
  1.3102 +                   .getService(Ci.nsIX509CertDB);
  1.3103 +      } catch (e) {
  1.3104 +        debug("nsIX509CertDB error: " + e);
  1.3105 +        // unrecoverable error, don't bug the user
  1.3106 +        aApp.downloadAvailable = false;
  1.3107 +        throw "CERTDB_ERROR";
  1.3108 +      }
  1.3109 +
  1.3110 +      let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin,
  1.3111 +                                                              aApp.manifestURL,
  1.3112 +                                                              aZipFile,
  1.3113 +                                                              certDb);
  1.3114 +
  1.3115 +      // We cannot really know if the system date is correct or
  1.3116 +      // not. What we can know is if it's after the build date or not,
  1.3117 +      // and assume the build date is correct (which we cannot
  1.3118 +      // really know either).
  1.3119 +      let isLaterThanBuildTime = Date.now() > PLATFORM_BUILD_ID_TIME;
  1.3120 +
  1.3121 +      let isSigned;
  1.3122 +
  1.3123 +      if (Components.isSuccessCode(result)) {
  1.3124 +        isSigned = true;
  1.3125 +      } else if (result == Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY ||
  1.3126 +                 result == Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY ||
  1.3127 +                 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING) {
  1.3128 +        throw "APP_PACKAGE_CORRUPTED";
  1.3129 +      } else if (result == Cr.NS_ERROR_FILE_CORRUPTED ||
  1.3130 +                 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE ||
  1.3131 +                 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID ||
  1.3132 +                 result == Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID) {
  1.3133 +        throw "APP_PACKAGE_INVALID";
  1.3134 +      } else if ((!aIsLocalFileInstall || isLaterThanBuildTime) &&
  1.3135 +                 (result != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)) {
  1.3136 +        throw "INVALID_SIGNATURE";
  1.3137 +      } else {
  1.3138 +        // If it's a localFileInstall and the validation failed
  1.3139 +        // because of a expired certificate, just assume it was valid
  1.3140 +        // and that the error occurred because the system time has not
  1.3141 +        // been set yet.
  1.3142 +        isSigned = (aIsLocalFileInstall &&
  1.3143 +                    (getNSPRErrorCode(result) ==
  1.3144 +                     SEC_ERROR_EXPIRED_CERTIFICATE));
  1.3145 +
  1.3146 +        zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
  1.3147 +                      .createInstance(Ci.nsIZipReader);
  1.3148 +        zipReader.open(aZipFile);
  1.3149 +      }
  1.3150 +
  1.3151 +      return [zipReader, isSigned];
  1.3152 +
  1.3153 +    }).bind(this));
  1.3154 +  },
  1.3155 +
  1.3156 +  _openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) {
  1.3157 +    let deferred = Promise.defer();
  1.3158 +
  1.3159 +    let root = TrustedRootCertificate.index;
  1.3160 +
  1.3161 +    let useReviewerCerts = false;
  1.3162 +    try {
  1.3163 +      useReviewerCerts = Services.prefs.
  1.3164 +                           getBoolPref("dom.mozApps.use_reviewer_certs");
  1.3165 +    } catch (ex) { }
  1.3166 +
  1.3167 +    // We'll use the reviewer and dev certificates only if the pref is set to
  1.3168 +    // true.
  1.3169 +    if (useReviewerCerts) {
  1.3170 +      let manifestPath = Services.io.newURI(aManifestURL, null, null).path;
  1.3171 +
  1.3172 +      switch (aInstallOrigin) {
  1.3173 +        case "https://marketplace.firefox.com":
  1.3174 +          root = manifestPath.startsWith("/reviewers/")
  1.3175 +               ? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot
  1.3176 +               : Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot;
  1.3177 +          break;
  1.3178 +
  1.3179 +        case "https://marketplace-dev.allizom.org":
  1.3180 +          root = manifestPath.startsWith("/reviewers/")
  1.3181 +               ? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot
  1.3182 +               : Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot;
  1.3183 +          break;
  1.3184 +      }
  1.3185 +    }
  1.3186 +
  1.3187 +    aCertDb.openSignedAppFileAsync(
  1.3188 +       root, aZipFile,
  1.3189 +       function(aRv, aZipReader) {
  1.3190 +         deferred.resolve([aRv, aZipReader]);
  1.3191 +       }
  1.3192 +    );
  1.3193 +
  1.3194 +    return deferred.promise;
  1.3195 +  },
  1.3196 +
  1.3197 +  _readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate,
  1.3198 +                         aManifest, aRequestChannel, aHash, aZipReader,
  1.3199 +                         aIsSigned) {
  1.3200 +    this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall);
  1.3201 +
  1.3202 +    if (!aZipReader.hasEntry("manifest.webapp")) {
  1.3203 +      throw "MISSING_MANIFEST";
  1.3204 +    }
  1.3205 +
  1.3206 +    let istream = aZipReader.getInputStream("manifest.webapp");
  1.3207 +
  1.3208 +    // Obtain a converter to read from a UTF-8 encoded input stream.
  1.3209 +    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  1.3210 +                      .createInstance(Ci.nsIScriptableUnicodeConverter);
  1.3211 +    converter.charset = "UTF-8";
  1.3212 +
  1.3213 +    let newManifest = JSON.parse(converter.ConvertToUnicode(
  1.3214 +          NetUtil.readInputStreamToString(istream, istream.available()) || ""));
  1.3215 +
  1.3216 +    if (!AppsUtils.checkManifest(newManifest, aOldApp)) {
  1.3217 +      throw "INVALID_MANIFEST";
  1.3218 +    }
  1.3219 +
  1.3220 +    // For app updates we don't forbid apps to rename themselves but
  1.3221 +    // we still retain the old name of the app. In the future we
  1.3222 +    // will use UI to allow updates to rename an app after we check
  1.3223 +    // with the user that the rename is ok.
  1.3224 +    if (aIsUpdate) {
  1.3225 +      // Call ensureSameAppName before compareManifests as `manifest`
  1.3226 +      // has been normalized to avoid app rename.
  1.3227 +      AppsUtils.ensureSameAppName(aManifest._manifest, newManifest, aOldApp);
  1.3228 +    }
  1.3229 +
  1.3230 +    if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) {
  1.3231 +      throw "MANIFEST_MISMATCH";
  1.3232 +    }
  1.3233 +
  1.3234 +    if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) {
  1.3235 +      throw "INSTALL_FROM_DENIED";
  1.3236 +    }
  1.3237 +
  1.3238 +    // Local file installs can be privileged even without the signature.
  1.3239 +    let maxStatus = aIsSigned || aIsLocalFileInstall
  1.3240 +                    ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
  1.3241 +                    : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
  1.3242 +
  1.3243 +    if (AppsUtils.getAppManifestStatus(newManifest) > maxStatus) {
  1.3244 +      throw "INVALID_SECURITY_LEVEL";
  1.3245 +    }
  1.3246 +
  1.3247 +    aOldApp.appStatus = AppsUtils.getAppManifestStatus(newManifest);
  1.3248 +
  1.3249 +    this._saveEtag(aIsUpdate, aOldApp, aRequestChannel, aHash, newManifest);
  1.3250 +    this._checkOrigin(aIsSigned || aIsLocalFileInstall, aOldApp, newManifest,
  1.3251 +                      aIsUpdate);
  1.3252 +    this._getIds(aIsSigned, aZipReader, converter, aNewApp, aOldApp, aIsUpdate);
  1.3253 +
  1.3254 +    return newManifest;
  1.3255 +  },
  1.3256 +
  1.3257 +  _checkSignature: function(aApp, aIsSigned, aIsLocalFileInstall) {
  1.3258 +    // XXX Security: You CANNOT safely add a new app store for
  1.3259 +    // installing privileged apps just by modifying this pref and
  1.3260 +    // adding the signing cert for that store to the cert trust
  1.3261 +    // database. *Any* origin listed can install apps signed with
  1.3262 +    // *any* certificate trusted; we don't try to maintain a strong
  1.3263 +    // association between certificate with installOrign. The
  1.3264 +    // expectation here is that in production builds the pref will
  1.3265 +    // contain exactly one origin. However, in custom development
  1.3266 +    // builds it may contain more than one origin so we can test
  1.3267 +    // different stages (dev, staging, prod) of the same app store.
  1.3268 +    //
  1.3269 +    // Only allow signed apps to be installed from a whitelist of
  1.3270 +    // domains, and require all packages installed from any of the
  1.3271 +    // domains on the whitelist to be signed. This is a stopgap until
  1.3272 +    // we have a real story for handling multiple app stores signing
  1.3273 +    // apps.
  1.3274 +    let signedAppOriginsStr =
  1.3275 +      Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from");
  1.3276 +    // If it's a local install and it's signed then we assume
  1.3277 +    // the app origin is a valid signer.
  1.3278 +    let isSignedAppOrigin = (aIsSigned && aIsLocalFileInstall) ||
  1.3279 +                             signedAppOriginsStr.split(",").
  1.3280 +                                   indexOf(aApp.installOrigin) > -1;
  1.3281 +    if (!aIsSigned && isSignedAppOrigin) {
  1.3282 +      // Packaged apps installed from these origins must be signed;
  1.3283 +      // if not, assume somebody stripped the signature.
  1.3284 +      throw "INVALID_SIGNATURE";
  1.3285 +    } else if (aIsSigned && !isSignedAppOrigin) {
  1.3286 +      // Other origins are *prohibited* from installing signed apps.
  1.3287 +      // One reason is that our app revocation mechanism requires
  1.3288 +      // strong cooperation from the host of the mini-manifest, which
  1.3289 +      // we assume to be under the control of the install origin,
  1.3290 +      // even if it has a different origin.
  1.3291 +      throw "INSTALL_FROM_DENIED";
  1.3292 +    }
  1.3293 +  },
  1.3294 +
  1.3295 +  _saveEtag: function(aIsUpdate, aOldApp, aRequestChannel, aHash, aManifest) {
  1.3296 +    // Save the new Etag for the package.
  1.3297 +    if (aIsUpdate) {
  1.3298 +      if (!aOldApp.staged) {
  1.3299 +        aOldApp.staged = { };
  1.3300 +      }
  1.3301 +      try {
  1.3302 +        aOldApp.staged.packageEtag = aRequestChannel.getResponseHeader("Etag");
  1.3303 +      } catch(e) { }
  1.3304 +      aOldApp.staged.packageHash = aHash;
  1.3305 +      aOldApp.staged.appStatus = AppsUtils.getAppManifestStatus(aManifest);
  1.3306 +    } else {
  1.3307 +      try {
  1.3308 +        aOldApp.packageEtag = aRequestChannel.getResponseHeader("Etag");
  1.3309 +      } catch(e) { }
  1.3310 +      aOldApp.packageHash = aHash;
  1.3311 +      aOldApp.appStatus = AppsUtils.getAppManifestStatus(aManifest);
  1.3312 +    }
  1.3313 +  },
  1.3314 +
  1.3315 +  _checkOrigin: function(aIsSigned, aOldApp, aManifest, aIsUpdate) {
  1.3316 +    // Check if the app declares which origin it will use.
  1.3317 +    if (aIsSigned &&
  1.3318 +        aOldApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
  1.3319 +        aManifest.origin !== undefined) {
  1.3320 +      let uri;
  1.3321 +      try {
  1.3322 +        uri = Services.io.newURI(aManifest.origin, null, null);
  1.3323 +      } catch(e) {
  1.3324 +        throw "INVALID_ORIGIN";
  1.3325 +      }
  1.3326 +      if (uri.scheme != "app") {
  1.3327 +        throw "INVALID_ORIGIN";
  1.3328 +      }
  1.3329 +
  1.3330 +      if (aIsUpdate) {
  1.3331 +        // Changing the origin during an update is not allowed.
  1.3332 +        if (uri.prePath != aOldApp.origin) {
  1.3333 +          throw "INVALID_ORIGIN_CHANGE";
  1.3334 +        }
  1.3335 +        // Nothing else to do for an update... since the
  1.3336 +        // origin can't change we don't need to move the
  1.3337 +        // app nor can we have a duplicated origin
  1.3338 +      } else {
  1.3339 +        debug("Setting origin to " + uri.prePath +
  1.3340 +              " for " + aOldApp.manifestURL);
  1.3341 +        let newId = uri.prePath.substring(6); // "app://".length
  1.3342 +        if (newId in this.webapps) {
  1.3343 +          throw "DUPLICATE_ORIGIN";
  1.3344 +        }
  1.3345 +        aOldApp.origin = uri.prePath;
  1.3346 +        // Update the registry.
  1.3347 +        let oldId = aOldApp.id;
  1.3348 +        aOldApp.id = newId;
  1.3349 +        this.webapps[newId] = aOldApp;
  1.3350 +        delete this.webapps[oldId];
  1.3351 +        // Rename the directories where the files are installed.
  1.3352 +        [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) {
  1.3353 +          let parent = FileUtils.getDir(aDir, ["webapps"], true, true);
  1.3354 +          let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true);
  1.3355 +          dir.moveTo(parent, newId);
  1.3356 +        });
  1.3357 +        // Signals that we need to swap the old id with the new app.
  1.3358 +        this.broadcastMessage("Webapps:RemoveApp", { id: oldId });
  1.3359 +        this.broadcastMessage("Webapps:AddApp", { id: newId,
  1.3360 +                                                  app: aOldApp });
  1.3361 +      }
  1.3362 +    }
  1.3363 +  },
  1.3364 +
  1.3365 +  _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp,
  1.3366 +                    aIsUpdate) {
  1.3367 +    // Get ids.json if the file is signed
  1.3368 +    if (aIsSigned) {
  1.3369 +      let idsStream;
  1.3370 +      try {
  1.3371 +        idsStream = aZipReader.getInputStream("META-INF/ids.json");
  1.3372 +      } catch (e) {
  1.3373 +        throw aZipReader.hasEntry("META-INF/ids.json")
  1.3374 +               ? e
  1.3375 +               : "MISSING_IDS_JSON";
  1.3376 +      }
  1.3377 +
  1.3378 +      let ids = JSON.parse(aConverter.ConvertToUnicode(NetUtil.
  1.3379 +             readInputStreamToString( idsStream, idsStream.available()) || ""));
  1.3380 +      if ((!ids.id) || !Number.isInteger(ids.version) ||
  1.3381 +          (ids.version <= 0)) {
  1.3382 +         throw "INVALID_IDS_JSON";
  1.3383 +      }
  1.3384 +      let storeId = aNewApp.installOrigin + "#" + ids.id;
  1.3385 +      this._checkForStoreIdMatch(aIsUpdate, aOldApp, storeId, ids.version);
  1.3386 +      aOldApp.storeId = storeId;
  1.3387 +      aOldApp.storeVersion = ids.version;
  1.3388 +    }
  1.3389 +  },
  1.3390 +
  1.3391 +  // aStoreId must be a string of the form
  1.3392 +  //   <installOrigin>#<storeId from ids.json>
  1.3393 +  // aStoreVersion must be a positive integer.
  1.3394 +  _checkForStoreIdMatch: function(aIsUpdate, aNewApp, aStoreId, aStoreVersion) {
  1.3395 +    // Things to check:
  1.3396 +    // 1. if it's a update:
  1.3397 +    //   a. We should already have this storeId, or the original storeId must
  1.3398 +    //      start with STORE_ID_PENDING_PREFIX
  1.3399 +    //   b. The manifestURL for the stored app should be the same one we're
  1.3400 +    //      updating
  1.3401 +    //   c. And finally the version of the update should be higher than the one
  1.3402 +    //      on the already installed package
  1.3403 +    // 2. else
  1.3404 +    //   a. We should not have this storeId on the list
  1.3405 +    // We're currently launching WRONG_APP_STORE_ID for all the mismatch kind of
  1.3406 +    // errors, and APP_STORE_VERSION_ROLLBACK for the version error.
  1.3407 +
  1.3408 +    // Does an app with this storeID exist already?
  1.3409 +    let appId = this.getAppLocalIdByStoreId(aStoreId);
  1.3410 +    let isInstalled = appId != Ci.nsIScriptSecurityManager.NO_APP_ID;
  1.3411 +    if (aIsUpdate) {
  1.3412 +      let isDifferent = aNewApp.localId !== appId;
  1.3413 +      let isPending = aNewApp.storeId.indexOf(STORE_ID_PENDING_PREFIX) == 0;
  1.3414 +
  1.3415 +      if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) {
  1.3416 +        throw "WRONG_APP_STORE_ID";
  1.3417 +      }
  1.3418 +
  1.3419 +      if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) {
  1.3420 +        throw "APP_STORE_VERSION_ROLLBACK";
  1.3421 +      }
  1.3422 +
  1.3423 +    } else if (isInstalled) {
  1.3424 +      throw "WRONG_APP_STORE_ID";
  1.3425 +    }
  1.3426 +  },
  1.3427 +
  1.3428 +  // Removes the directory we created, and sends an error to the DOM side.
  1.3429 +  _revertDownloadPackage: function(aId, aOldApp, aNewApp, aIsUpdate, aError) {
  1.3430 +    debug("Cleanup: " + aError + "\n" + aError.stack);
  1.3431 +    let dir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
  1.3432 +    try {
  1.3433 +      dir.remove(true);
  1.3434 +    } catch (e) { }
  1.3435 +
  1.3436 +    // We avoid notifying the error to the DOM side if the app download
  1.3437 +    // was cancelled via cancelDownload, which already sends its own
  1.3438 +    // notification.
  1.3439 +    if (aOldApp.isCanceling) {
  1.3440 +      delete aOldApp.isCanceling;
  1.3441 +      return;
  1.3442 +    }
  1.3443 +
  1.3444 +    let download = AppDownloadManager.get(aNewApp.manifestURL);
  1.3445 +    aOldApp.downloading = false;
  1.3446 +
  1.3447 +    // If there were not enough storage to download the package we
  1.3448 +    // won't have a record of the download details, so we just set the
  1.3449 +    // installState to 'pending' at first download and to 'installed' when
  1.3450 +    // updating.
  1.3451 +    aOldApp.installState = download ? download.previousState
  1.3452 +                                    : aIsUpdate ? "installed"
  1.3453 +                                                : "pending";
  1.3454 +
  1.3455 +    if (aOldApp.staged) {
  1.3456 +      delete aOldApp.staged;
  1.3457 +    }
  1.3458 +
  1.3459 +    this._saveApps().then(() => {
  1.3460 +      this.broadcastMessage("Webapps:UpdateState", {
  1.3461 +        app: aOldApp,
  1.3462 +        error: aError,
  1.3463 +        manifestURL: aNewApp.manifestURL
  1.3464 +      });
  1.3465 +      this.broadcastMessage("Webapps:FireEvent", {
  1.3466 +        eventType: "downloaderror",
  1.3467 +        manifestURL:  aNewApp.manifestURL
  1.3468 +      });
  1.3469 +    });
  1.3470 +    AppDownloadManager.remove(aNewApp.manifestURL);
  1.3471 +  },
  1.3472 +
  1.3473 +  doUninstall: function(aData, aMm) {
  1.3474 +    this.uninstall(aData.manifestURL,
  1.3475 +      function onsuccess() {
  1.3476 +        aMm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData);
  1.3477 +      },
  1.3478 +      function onfailure() {
  1.3479 +        // Fall-through, fails to uninstall the desired app because:
  1.3480 +        //   - we cannot find the app to be uninstalled.
  1.3481 +        //   - the app to be uninstalled is not removable.
  1.3482 +        aMm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData);
  1.3483 +      }
  1.3484 +    );
  1.3485 +  },
  1.3486 +
  1.3487 +  uninstall: function(aManifestURL, aOnSuccess, aOnFailure) {
  1.3488 +    debug("uninstall " + aManifestURL);
  1.3489 +
  1.3490 +    let app = this.getAppByManifestURL(aManifestURL);
  1.3491 +    if (!app) {
  1.3492 +      aOnFailure("NO_SUCH_APP");
  1.3493 +      return;
  1.3494 +    }
  1.3495 +    let id = app.id;
  1.3496 +
  1.3497 +    if (!app.removable) {
  1.3498 +      debug("Error: cannot uninstall a non-removable app.");
  1.3499 +      aOnFailure("NON_REMOVABLE_APP");
  1.3500 +      return;
  1.3501 +    }
  1.3502 +
  1.3503 +    // Check if we are downloading something for this app, and cancel the
  1.3504 +    // download if needed.
  1.3505 +    this.cancelDownload(app.manifestURL);
  1.3506 +
  1.3507 +    // Clean up the deprecated manifest cache if needed.
  1.3508 +    if (id in this._manifestCache) {
  1.3509 +      delete this._manifestCache[id];
  1.3510 +    }
  1.3511 +
  1.3512 +    // Clear private data first.
  1.3513 +    this._clearPrivateData(app.localId, false);
  1.3514 +
  1.3515 +    // Then notify observers.
  1.3516 +    // We have to clone the app object as nsIDOMApplication objects are
  1.3517 +    // stringified as an empty object. (see bug 830376)
  1.3518 +    let appClone = AppsUtils.cloneAppObject(app);
  1.3519 +    Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(appClone));
  1.3520 +
  1.3521 +    if (supportSystemMessages()) {
  1.3522 +      this._readManifests([{ id: id }]).then((aResult) => {
  1.3523 +        this._unregisterActivities(aResult[0].manifest, app);
  1.3524 +      });
  1.3525 +    }
  1.3526 +
  1.3527 +    let dir = this._getAppDir(id);
  1.3528 +    try {
  1.3529 +      dir.remove(true);
  1.3530 +    } catch (e) {}
  1.3531 +
  1.3532 +    delete this.webapps[id];
  1.3533 +
  1.3534 +    this._saveApps().then(() => {
  1.3535 +      this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", appClone);
  1.3536 +      // Catch exception on callback call to ensure notifying observers after
  1.3537 +      try {
  1.3538 +        if (aOnSuccess) {
  1.3539 +          aOnSuccess();
  1.3540 +        }
  1.3541 +      } catch(ex) {
  1.3542 +        Cu.reportError("DOMApplicationRegistry: Exception on app uninstall: " +
  1.3543 +                       ex + "\n" + ex.stack);
  1.3544 +      }
  1.3545 +      this.broadcastMessage("Webapps:RemoveApp", { id: id });
  1.3546 +    });
  1.3547 +  },
  1.3548 +
  1.3549 +  getSelf: function(aData, aMm) {
  1.3550 +    aData.apps = [];
  1.3551 +
  1.3552 +    if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
  1.3553 +        aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
  1.3554 +      aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
  1.3555 +      return;
  1.3556 +    }
  1.3557 +
  1.3558 +    let tmp = [];
  1.3559 +
  1.3560 +    for (let id in this.webapps) {
  1.3561 +      if (this.webapps[id].origin == aData.origin &&
  1.3562 +          this.webapps[id].localId == aData.appId &&
  1.3563 +          this._isLaunchable(this.webapps[id])) {
  1.3564 +        let app = AppsUtils.cloneAppObject(this.webapps[id]);
  1.3565 +        aData.apps.push(app);
  1.3566 +        tmp.push({ id: id });
  1.3567 +        break;
  1.3568 +      }
  1.3569 +    }
  1.3570 +
  1.3571 +    if (!aData.apps.length) {
  1.3572 +      aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
  1.3573 +      return;
  1.3574 +    }
  1.3575 +
  1.3576 +    this._readManifests(tmp).then((aResult) => {
  1.3577 +      for (let i = 0; i < aResult.length; i++)
  1.3578 +        aData.apps[i].manifest = aResult[i].manifest;
  1.3579 +      aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
  1.3580 +    });
  1.3581 +  },
  1.3582 +
  1.3583 +  checkInstalled: function(aData, aMm) {
  1.3584 +    aData.app = null;
  1.3585 +    let tmp = [];
  1.3586 +
  1.3587 +    for (let appId in this.webapps) {
  1.3588 +      if (this.webapps[appId].manifestURL == aData.manifestURL &&
  1.3589 +          this._isLaunchable(this.webapps[appId])) {
  1.3590 +        aData.app = AppsUtils.cloneAppObject(this.webapps[appId]);
  1.3591 +        tmp.push({ id: appId });
  1.3592 +        break;
  1.3593 +      }
  1.3594 +    }
  1.3595 +
  1.3596 +    this._readManifests(tmp).then((aResult) => {
  1.3597 +      for (let i = 0; i < aResult.length; i++) {
  1.3598 +        aData.app.manifest = aResult[i].manifest;
  1.3599 +        break;
  1.3600 +      }
  1.3601 +      aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", aData);
  1.3602 +    });
  1.3603 +  },
  1.3604 +
  1.3605 +  getInstalled: function(aData, aMm) {
  1.3606 +    aData.apps = [];
  1.3607 +    let tmp = [];
  1.3608 +
  1.3609 +    for (let id in this.webapps) {
  1.3610 +      if (this.webapps[id].installOrigin == aData.origin &&
  1.3611 +          this._isLaunchable(this.webapps[id])) {
  1.3612 +        aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
  1.3613 +        tmp.push({ id: id });
  1.3614 +      }
  1.3615 +    }
  1.3616 +
  1.3617 +    this._readManifests(tmp).then((aResult) => {
  1.3618 +      for (let i = 0; i < aResult.length; i++)
  1.3619 +        aData.apps[i].manifest = aResult[i].manifest;
  1.3620 +      aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", aData);
  1.3621 +    });
  1.3622 +  },
  1.3623 +
  1.3624 +  getNotInstalled: function(aData, aMm) {
  1.3625 +    aData.apps = [];
  1.3626 +    let tmp = [];
  1.3627 +
  1.3628 +    for (let id in this.webapps) {
  1.3629 +      if (!this._isLaunchable(this.webapps[id])) {
  1.3630 +        aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
  1.3631 +        tmp.push({ id: id });
  1.3632 +      }
  1.3633 +    }
  1.3634 +
  1.3635 +    this._readManifests(tmp).then((aResult) => {
  1.3636 +      for (let i = 0; i < aResult.length; i++)
  1.3637 +        aData.apps[i].manifest = aResult[i].manifest;
  1.3638 +      aMm.sendAsyncMessage("Webapps:GetNotInstalled:Return:OK", aData);
  1.3639 +    });
  1.3640 +  },
  1.3641 +
  1.3642 +  doGetAll: function(aData, aMm) {
  1.3643 +    this.getAll(function (apps) {
  1.3644 +      aData.apps = apps;
  1.3645 +      aMm.sendAsyncMessage("Webapps:GetAll:Return:OK", aData);
  1.3646 +    });
  1.3647 +  },
  1.3648 +
  1.3649 +  getAll: function(aCallback) {
  1.3650 +    debug("getAll");
  1.3651 +    let apps = [];
  1.3652 +    let tmp = [];
  1.3653 +
  1.3654 +    for (let id in this.webapps) {
  1.3655 +      let app = AppsUtils.cloneAppObject(this.webapps[id]);
  1.3656 +      if (!this._isLaunchable(app))
  1.3657 +        continue;
  1.3658 +
  1.3659 +      apps.push(app);
  1.3660 +      tmp.push({ id: id });
  1.3661 +    }
  1.3662 +
  1.3663 +    this._readManifests(tmp).then((aResult) => {
  1.3664 +      for (let i = 0; i < aResult.length; i++)
  1.3665 +        apps[i].manifest = aResult[i].manifest;
  1.3666 +      aCallback(apps);
  1.3667 +    });
  1.3668 +  },
  1.3669 +
  1.3670 +  /* Check if |data| is actually a receipt */
  1.3671 +  isReceipt: function(data) {
  1.3672 +    try {
  1.3673 +      // The receipt data shouldn't be too big (allow up to 1 MiB of data)
  1.3674 +      const MAX_RECEIPT_SIZE = 1048576;
  1.3675 +
  1.3676 +      if (data.length > MAX_RECEIPT_SIZE) {
  1.3677 +        return "RECEIPT_TOO_BIG";
  1.3678 +      }
  1.3679 +
  1.3680 +      // Marketplace receipts are JWK + "~" + JWT
  1.3681 +      // Other receipts may contain only the JWT
  1.3682 +      let receiptParts = data.split('~');
  1.3683 +      let jwtData = null;
  1.3684 +      if (receiptParts.length == 2) {
  1.3685 +        jwtData = receiptParts[1];
  1.3686 +      } else {
  1.3687 +        jwtData = receiptParts[0];
  1.3688 +      }
  1.3689 +
  1.3690 +      let segments = jwtData.split('.');
  1.3691 +      if (segments.length != 3) {
  1.3692 +        return "INVALID_SEGMENTS_NUMBER";
  1.3693 +      }
  1.3694 +
  1.3695 +      // We need to translate the base64 alphabet used in JWT to our base64 alphabet
  1.3696 +      // before calling atob.
  1.3697 +      let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+')
  1.3698 +                                                      .replace(/_/g, '/')));
  1.3699 +      if (!decodedReceipt) {
  1.3700 +        return "INVALID_RECEIPT_ENCODING";
  1.3701 +      }
  1.3702 +
  1.3703 +      // Required values for a receipt
  1.3704 +      if (!decodedReceipt.typ) {
  1.3705 +        return "RECEIPT_TYPE_REQUIRED";
  1.3706 +      }
  1.3707 +      if (!decodedReceipt.product) {
  1.3708 +        return "RECEIPT_PRODUCT_REQUIRED";
  1.3709 +      }
  1.3710 +      if (!decodedReceipt.user) {
  1.3711 +        return "RECEIPT_USER_REQUIRED";
  1.3712 +      }
  1.3713 +      if (!decodedReceipt.iss) {
  1.3714 +        return "RECEIPT_ISS_REQUIRED";
  1.3715 +      }
  1.3716 +      if (!decodedReceipt.nbf) {
  1.3717 +        return "RECEIPT_NBF_REQUIRED";
  1.3718 +      }
  1.3719 +      if (!decodedReceipt.iat) {
  1.3720 +        return "RECEIPT_IAT_REQUIRED";
  1.3721 +      }
  1.3722 +
  1.3723 +      let allowedTypes = [ "purchase-receipt", "developer-receipt",
  1.3724 +                           "reviewer-receipt", "test-receipt" ];
  1.3725 +      if (allowedTypes.indexOf(decodedReceipt.typ) < 0) {
  1.3726 +        return "RECEIPT_TYPE_UNSUPPORTED";
  1.3727 +      }
  1.3728 +    } catch (e) {
  1.3729 +      return "RECEIPT_ERROR";
  1.3730 +    }
  1.3731 +
  1.3732 +    return null;
  1.3733 +  },
  1.3734 +
  1.3735 +  addReceipt: function(aData, aMm) {
  1.3736 +    debug("addReceipt " + aData.manifestURL);
  1.3737 +
  1.3738 +    let receipt = aData.receipt;
  1.3739 +
  1.3740 +    if (!receipt) {
  1.3741 +      aData.error = "INVALID_PARAMETERS";
  1.3742 +      aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
  1.3743 +      return;
  1.3744 +    }
  1.3745 +
  1.3746 +    let error = this.isReceipt(receipt);
  1.3747 +    if (error) {
  1.3748 +      aData.error = error;
  1.3749 +      aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
  1.3750 +      return;
  1.3751 +    }
  1.3752 +
  1.3753 +    let id = this._appIdForManifestURL(aData.manifestURL);
  1.3754 +    let app = this.webapps[id];
  1.3755 +
  1.3756 +    if (!app.receipts) {
  1.3757 +      app.receipts = [];
  1.3758 +    } else if (app.receipts.length > 500) {
  1.3759 +      aData.error = "TOO_MANY_RECEIPTS";
  1.3760 +      aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
  1.3761 +      return;
  1.3762 +    }
  1.3763 +
  1.3764 +    let index = app.receipts.indexOf(receipt);
  1.3765 +    if (index >= 0) {
  1.3766 +      aData.error = "RECEIPT_ALREADY_EXISTS";
  1.3767 +      aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
  1.3768 +      return;
  1.3769 +    }
  1.3770 +
  1.3771 +    app.receipts.push(receipt);
  1.3772 +
  1.3773 +    this._saveApps().then(() => {
  1.3774 +      aData.receipts = app.receipts;
  1.3775 +      aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData);
  1.3776 +    });
  1.3777 +  },
  1.3778 +
  1.3779 +  removeReceipt: function(aData, aMm) {
  1.3780 +    debug("removeReceipt " + aData.manifestURL);
  1.3781 +
  1.3782 +    let receipt = aData.receipt;
  1.3783 +
  1.3784 +    if (!receipt) {
  1.3785 +      aData.error = "INVALID_PARAMETERS";
  1.3786 +      aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
  1.3787 +      return;
  1.3788 +    }
  1.3789 +
  1.3790 +    let id = this._appIdForManifestURL(aData.manifestURL);
  1.3791 +    let app = this.webapps[id];
  1.3792 +
  1.3793 +    if (!app.receipts) {
  1.3794 +      aData.error = "NO_SUCH_RECEIPT";
  1.3795 +      aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
  1.3796 +      return;
  1.3797 +    }
  1.3798 +
  1.3799 +    let index = app.receipts.indexOf(receipt);
  1.3800 +    if (index == -1) {
  1.3801 +      aData.error = "NO_SUCH_RECEIPT";
  1.3802 +      aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
  1.3803 +      return;
  1.3804 +    }
  1.3805 +
  1.3806 +    app.receipts.splice(index, 1);
  1.3807 +
  1.3808 +    this._saveApps().then(() => {
  1.3809 +      aData.receipts = app.receipts;
  1.3810 +      aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData);
  1.3811 +    });
  1.3812 +  },
  1.3813 +
  1.3814 +  replaceReceipt: function(aData, aMm) {
  1.3815 +    debug("replaceReceipt " + aData.manifestURL);
  1.3816 +
  1.3817 +    let oldReceipt = aData.oldReceipt;
  1.3818 +    let newReceipt = aData.newReceipt;
  1.3819 +
  1.3820 +    if (!oldReceipt || !newReceipt) {
  1.3821 +      aData.error = "INVALID_PARAMETERS";
  1.3822 +      aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
  1.3823 +      return;
  1.3824 +    }
  1.3825 +
  1.3826 +    let error = this.isReceipt(newReceipt);
  1.3827 +    if (error) {
  1.3828 +      aData.error = error;
  1.3829 +      aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
  1.3830 +      return;
  1.3831 +    }
  1.3832 +
  1.3833 +    let id = this._appIdForManifestURL(aData.manifestURL);
  1.3834 +    let app = this.webapps[id];
  1.3835 +
  1.3836 +    if (!app.receipts) {
  1.3837 +      aData.error = "NO_SUCH_RECEIPT";
  1.3838 +      aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
  1.3839 +      return;
  1.3840 +    }
  1.3841 +
  1.3842 +    let oldIndex = app.receipts.indexOf(oldReceipt);
  1.3843 +    if (oldIndex == -1) {
  1.3844 +      aData.error = "NO_SUCH_RECEIPT";
  1.3845 +      aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
  1.3846 +      return;
  1.3847 +    }
  1.3848 +
  1.3849 +    app.receipts[oldIndex] = newReceipt;
  1.3850 +
  1.3851 +    this._saveApps().then(() => {
  1.3852 +      aData.receipts = app.receipts;
  1.3853 +      aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData);
  1.3854 +    });
  1.3855 +  },
  1.3856 +
  1.3857 +  getManifestFor: function(aManifestURL) {
  1.3858 +    let id = this._appIdForManifestURL(aManifestURL);
  1.3859 +    let app = this.webapps[id];
  1.3860 +    if (!id || (app.installState == "pending" && !app.retryingDownload)) {
  1.3861 +      return Promise.resolve(null);
  1.3862 +    }
  1.3863 +
  1.3864 +    return this._readManifests([{ id: id }]).then((aResult) => {
  1.3865 +      return aResult[0].manifest;
  1.3866 +    });
  1.3867 +  },
  1.3868 +
  1.3869 +  getAppByManifestURL: function(aManifestURL) {
  1.3870 +    return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
  1.3871 +  },
  1.3872 +
  1.3873 +  getCSPByLocalId: function(aLocalId) {
  1.3874 +    debug("getCSPByLocalId:" + aLocalId);
  1.3875 +    return AppsUtils.getCSPByLocalId(this.webapps, aLocalId);
  1.3876 +  },
  1.3877 +
  1.3878 +  getAppLocalIdByStoreId: function(aStoreId) {
  1.3879 +    debug("getAppLocalIdByStoreId:" + aStoreId);
  1.3880 +    return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
  1.3881 +  },
  1.3882 +
  1.3883 +  getAppByLocalId: function(aLocalId) {
  1.3884 +    return AppsUtils.getAppByLocalId(this.webapps, aLocalId);
  1.3885 +  },
  1.3886 +
  1.3887 +  getManifestURLByLocalId: function(aLocalId) {
  1.3888 +    return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
  1.3889 +  },
  1.3890 +
  1.3891 +  getAppLocalIdByManifestURL: function(aManifestURL) {
  1.3892 +    return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
  1.3893 +  },
  1.3894 +
  1.3895 +  getCoreAppsBasePath: function() {
  1.3896 +    return AppsUtils.getCoreAppsBasePath();
  1.3897 +  },
  1.3898 +
  1.3899 +  getWebAppsBasePath: function() {
  1.3900 +    return OS.Path.dirname(this.appsFile);
  1.3901 +  },
  1.3902 +
  1.3903 +  _isLaunchable: function(aApp) {
  1.3904 +    if (this.allAppsLaunchable)
  1.3905 +      return true;
  1.3906 +
  1.3907 +    return WebappOSUtils.isLaunchable(aApp);
  1.3908 +  },
  1.3909 +
  1.3910 +  _notifyCategoryAndObservers: function(subject, topic, data,  msg) {
  1.3911 +    const serviceMarker = "service,";
  1.3912 +
  1.3913 +    // First create observers from the category manager.
  1.3914 +    let cm =
  1.3915 +      Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
  1.3916 +    let enumerator = cm.enumerateCategory(topic);
  1.3917 +
  1.3918 +    let observers = [];
  1.3919 +
  1.3920 +    while (enumerator.hasMoreElements()) {
  1.3921 +      let entry =
  1.3922 +        enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
  1.3923 +      let contractID = cm.getCategoryEntry(topic, entry);
  1.3924 +
  1.3925 +      let factoryFunction;
  1.3926 +      if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
  1.3927 +        contractID = contractID.substring(serviceMarker.length);
  1.3928 +        factoryFunction = "getService";
  1.3929 +      }
  1.3930 +      else {
  1.3931 +        factoryFunction = "createInstance";
  1.3932 +      }
  1.3933 +
  1.3934 +      try {
  1.3935 +        let handler = Cc[contractID][factoryFunction]();
  1.3936 +        if (handler) {
  1.3937 +          let observer = handler.QueryInterface(Ci.nsIObserver);
  1.3938 +          observers.push(observer);
  1.3939 +        }
  1.3940 +      } catch(e) { }
  1.3941 +    }
  1.3942 +
  1.3943 +    // Next enumerate the registered observers.
  1.3944 +    enumerator = Services.obs.enumerateObservers(topic);
  1.3945 +    while (enumerator.hasMoreElements()) {
  1.3946 +      try {
  1.3947 +        let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver);
  1.3948 +        if (observers.indexOf(observer) == -1) {
  1.3949 +          observers.push(observer);
  1.3950 +        }
  1.3951 +      } catch (e) { }
  1.3952 +    }
  1.3953 +
  1.3954 +    observers.forEach(function (observer) {
  1.3955 +      try {
  1.3956 +        observer.observe(subject, topic, data);
  1.3957 +      } catch(e) { }
  1.3958 +    });
  1.3959 +    // Send back an answer to the child.
  1.3960 +    if (msg) {
  1.3961 +      ppmm.broadcastAsyncMessage("Webapps:ClearBrowserData:Return", msg);
  1.3962 +    }
  1.3963 +  },
  1.3964 +
  1.3965 +  registerBrowserElementParentForApp: function(bep, appId) {
  1.3966 +    let mm = bep._mm;
  1.3967 +
  1.3968 +    // Make a listener function that holds on to this appId.
  1.3969 +    let listener = this.receiveAppMessage.bind(this, appId);
  1.3970 +
  1.3971 +    this.frameMessages.forEach(function(msgName) {
  1.3972 +      mm.addMessageListener(msgName, listener);
  1.3973 +    });
  1.3974 +  },
  1.3975 +
  1.3976 +  receiveAppMessage: function(appId, message) {
  1.3977 +    switch (message.name) {
  1.3978 +      case "Webapps:ClearBrowserData":
  1.3979 +        this._clearPrivateData(appId, true, message.data);
  1.3980 +        break;
  1.3981 +    }
  1.3982 +  },
  1.3983 +
  1.3984 +  _clearPrivateData: function(appId, browserOnly, msg) {
  1.3985 +    let subject = {
  1.3986 +      appId: appId,
  1.3987 +      browserOnly: browserOnly,
  1.3988 +      QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
  1.3989 +    };
  1.3990 +    this._notifyCategoryAndObservers(subject, "webapps-clear-data", null, msg);
  1.3991 +  }
  1.3992 +};
  1.3993 +
  1.3994 +/**
  1.3995 + * Appcache download observer
  1.3996 + */
  1.3997 +let AppcacheObserver = function(aApp) {
  1.3998 +  debug("Creating AppcacheObserver for " + aApp.origin +
  1.3999 +        " - " + aApp.installState);
  1.4000 +  this.app = aApp;
  1.4001 +  this.startStatus = aApp.installState;
  1.4002 +  this.lastProgressTime = 0;
  1.4003 +  // Send a first progress event to correctly set the DOM object's properties.
  1.4004 +  this._sendProgressEvent();
  1.4005 +};
  1.4006 +
  1.4007 +AppcacheObserver.prototype = {
  1.4008 +  // nsIOfflineCacheUpdateObserver implementation
  1.4009 +  _sendProgressEvent: function() {
  1.4010 +    let app = this.app;
  1.4011 +    DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
  1.4012 +      app: app,
  1.4013 +      manifestURL: app.manifestURL
  1.4014 +    });
  1.4015 +    DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
  1.4016 +      eventType: "progress",
  1.4017 +      manifestURL: app.manifestURL
  1.4018 +    });
  1.4019 +  },
  1.4020 +
  1.4021 +  updateStateChanged: function appObs_Update(aUpdate, aState) {
  1.4022 +    let mustSave = false;
  1.4023 +    let app = this.app;
  1.4024 +
  1.4025 +    debug("Offline cache state change for " + app.origin + " : " + aState);
  1.4026 +
  1.4027 +    var self = this;
  1.4028 +    let setStatus = function appObs_setStatus(aStatus, aProgress) {
  1.4029 +      debug("Offlinecache setStatus to " + aStatus + " with progress " +
  1.4030 +            aProgress + " for " + app.origin);
  1.4031 +      mustSave = (app.installState != aStatus);
  1.4032 +
  1.4033 +      app.installState = aStatus;
  1.4034 +      app.progress = aProgress;
  1.4035 +      if (aStatus != "installed") {
  1.4036 +        self._sendProgressEvent();
  1.4037 +        return;
  1.4038 +      }
  1.4039 +
  1.4040 +      app.updateTime = Date.now();
  1.4041 +      app.downloading = false;
  1.4042 +      app.downloadAvailable = false;
  1.4043 +      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
  1.4044 +        app: app,
  1.4045 +        manifestURL: app.manifestURL
  1.4046 +      });
  1.4047 +      DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
  1.4048 +        eventType: ["downloadsuccess", "downloadapplied"],
  1.4049 +        manifestURL: app.manifestURL
  1.4050 +      });
  1.4051 +    }
  1.4052 +
  1.4053 +    let setError = function appObs_setError(aError) {
  1.4054 +      debug("Offlinecache setError to " + aError);
  1.4055 +      app.downloading = false;
  1.4056 +      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
  1.4057 +        app: app,
  1.4058 +        manifestURL: app.manifestURL
  1.4059 +      });
  1.4060 +      DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
  1.4061 +        error: aError,
  1.4062 +        eventType: "downloaderror",
  1.4063 +        manifestURL: app.manifestURL
  1.4064 +      });
  1.4065 +      mustSave = true;
  1.4066 +    }
  1.4067 +
  1.4068 +    switch (aState) {
  1.4069 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR:
  1.4070 +        aUpdate.removeObserver(this);
  1.4071 +        AppDownloadManager.remove(app.manifestURL);
  1.4072 +        setError("APP_CACHE_DOWNLOAD_ERROR");
  1.4073 +        break;
  1.4074 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE:
  1.4075 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED:
  1.4076 +        aUpdate.removeObserver(this);
  1.4077 +        AppDownloadManager.remove(app.manifestURL);
  1.4078 +        setStatus("installed", aUpdate.byteProgress);
  1.4079 +        break;
  1.4080 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING:
  1.4081 +        setStatus(this.startStatus, aUpdate.byteProgress);
  1.4082 +        break;
  1.4083 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED:
  1.4084 +      case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS:
  1.4085 +        let now = Date.now();
  1.4086 +        if (now - this.lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
  1.4087 +          setStatus(this.startStatus, aUpdate.byteProgress);
  1.4088 +          this.lastProgressTime = now;
  1.4089 +        }
  1.4090 +        break;
  1.4091 +    }
  1.4092 +
  1.4093 +    // Status changed, update the stored version.
  1.4094 +    if (mustSave) {
  1.4095 +      DOMApplicationRegistry._saveApps();
  1.4096 +    }
  1.4097 +  },
  1.4098 +
  1.4099 +  applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) {
  1.4100 +    // Nothing to do.
  1.4101 +  }
  1.4102 +};
  1.4103 +
  1.4104 +DOMApplicationRegistry.init();

mercurial