dom/apps/src/Webapps.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rwxr-xr-x

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial