michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: // Possible errors thrown by the signature verifier. michael@0: const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; michael@0: const SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11); michael@0: michael@0: // We need this to decide if we should accept or not files signed with expired michael@0: // certificates. michael@0: function buildIDToTime() { michael@0: let platformBuildID = michael@0: Cc["@mozilla.org/xre/app-info;1"] michael@0: .getService(Ci.nsIXULAppInfo).platformBuildID; michael@0: let platformBuildIDDate = new Date(); michael@0: platformBuildIDDate.setUTCFullYear(platformBuildID.substr(0,4), michael@0: platformBuildID.substr(4,2) - 1, michael@0: platformBuildID.substr(6,2)); michael@0: platformBuildIDDate.setUTCHours(platformBuildID.substr(8,2), michael@0: platformBuildID.substr(10,2), michael@0: platformBuildID.substr(12,2)); michael@0: return platformBuildIDDate.getTime(); michael@0: } michael@0: michael@0: const PLATFORM_BUILD_ID_TIME = buildIDToTime(); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"]; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import('resource://gre/modules/ActivitiesService.jsm'); michael@0: Cu.import("resource://gre/modules/AppsUtils.jsm"); michael@0: Cu.import("resource://gre/modules/AppDownloadManager.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate", michael@0: "resource://gre/modules/StoreTrustAnchor.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller", michael@0: "resource://gre/modules/PermissionsInstaller.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller", michael@0: "resource://gre/modules/OfflineCacheInstaller.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker", michael@0: "resource://gre/modules/SystemMessagePermissionsChecker.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils", michael@0: "resource://gre/modules/WebappOSUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader", michael@0: "resource://gre/modules/ScriptPreloader.jsm"); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: XPCOMUtils.defineLazyGetter(this, "libcutils", function() { michael@0: Cu.import("resource://gre/modules/systemlibs.js"); michael@0: return libcutils; michael@0: }); michael@0: #endif michael@0: michael@0: function debug(aMsg) { michael@0: #ifdef DEBUG michael@0: dump("-*- Webapps.jsm : " + aMsg + "\n"); michael@0: #endif michael@0: } michael@0: michael@0: function getNSPRErrorCode(err) { michael@0: return -1 * ((err) & 0xffff); michael@0: } michael@0: michael@0: function supportUseCurrentProfile() { michael@0: return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile"); michael@0: } michael@0: michael@0: function supportSystemMessages() { michael@0: return Services.prefs.getBoolPref("dom.sysmsg.enabled"); michael@0: } michael@0: michael@0: // Minimum delay between two progress events while downloading, in ms. michael@0: const MIN_PROGRESS_EVENT_DELAY = 1500; michael@0: michael@0: const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org"; michael@0: michael@0: const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser"; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageBroadcaster"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() { michael@0: return Cc["@mozilla.org/inter-app-communication-service;1"] michael@0: .getService(Ci.nsIInterAppCommService); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService", michael@0: "@mozilla.org/datastore-service;1", michael@0: "nsIDataStoreService"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "msgmgr", function() { michael@0: return Cc["@mozilla.org/system-message-internal;1"] michael@0: .getService(Ci.nsISystemMessagesInternal); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "updateSvc", function() { michael@0: return Cc["@mozilla.org/offlinecacheupdate-service;1"] michael@0: .getService(Ci.nsIOfflineCacheUpdateService); michael@0: }); michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: const DIRECTORY_NAME = "webappsDir"; michael@0: #elifdef ANDROID michael@0: const DIRECTORY_NAME = "webappsDir"; michael@0: #else michael@0: // If we're executing in the context of the webapp runtime, the data files michael@0: // are in a different directory (currently the Firefox profile that installed michael@0: // the webapp); otherwise, they're in the current profile. michael@0: const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD"; michael@0: #endif michael@0: michael@0: // We'll use this to identify privileged apps that have been preinstalled michael@0: // For those apps we'll set michael@0: // STORE_ID_PENDING_PREFIX + installOrigin michael@0: // as the storeID. This ensures it's unique and can't be set from a legit michael@0: // store even by error. michael@0: const STORE_ID_PENDING_PREFIX = "#unknownID#"; michael@0: michael@0: this.DOMApplicationRegistry = { michael@0: // Path to the webapps.json file where we store the registry data. michael@0: appsFile: null, michael@0: webapps: { }, michael@0: children: [ ], michael@0: allAppsLaunchable: false, michael@0: _updateHandlers: [ ], michael@0: michael@0: init: function() { michael@0: this.messages = ["Webapps:Install", "Webapps:Uninstall", michael@0: "Webapps:GetSelf", "Webapps:CheckInstalled", michael@0: "Webapps:GetInstalled", "Webapps:GetNotInstalled", michael@0: "Webapps:Launch", "Webapps:GetAll", michael@0: "Webapps:InstallPackage", michael@0: "Webapps:GetList", "Webapps:RegisterForMessages", michael@0: "Webapps:UnregisterForMessages", michael@0: "Webapps:CancelDownload", "Webapps:CheckForUpdate", michael@0: "Webapps:Download", "Webapps:ApplyDownload", michael@0: "Webapps:Install:Return:Ack", "Webapps:AddReceipt", michael@0: "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt", michael@0: "child-process-shutdown"]; michael@0: michael@0: this.frameMessages = ["Webapps:ClearBrowserData"]; michael@0: michael@0: this.messages.forEach((function(msgName) { michael@0: ppmm.addMessageListener(msgName, this); michael@0: }).bind(this)); michael@0: michael@0: cpmm.addMessageListener("Activities:Register:OK", this); michael@0: michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: Services.obs.addObserver(this, "memory-pressure", false); michael@0: michael@0: AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this)); michael@0: michael@0: this.appsFile = FileUtils.getFile(DIRECTORY_NAME, michael@0: ["webapps", "webapps.json"], true).path; michael@0: michael@0: this.loadAndUpdateApps(); michael@0: }, michael@0: michael@0: // loads the current registry, that could be empty on first run. michael@0: loadCurrentRegistry: function() { michael@0: return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => { michael@0: if (!aData) { michael@0: return; michael@0: } michael@0: michael@0: this.webapps = aData; michael@0: let appDir = OS.Path.dirname(this.appsFile); michael@0: for (let id in this.webapps) { michael@0: let app = this.webapps[id]; michael@0: if (!app) { michael@0: delete this.webapps[id]; michael@0: continue; michael@0: } michael@0: michael@0: app.id = id; michael@0: michael@0: // Make sure we have a localId michael@0: if (app.localId === undefined) { michael@0: app.localId = this._nextLocalId(); michael@0: } michael@0: michael@0: if (app.basePath === undefined) { michael@0: app.basePath = appDir; michael@0: } michael@0: michael@0: // Default to removable apps. michael@0: if (app.removable === undefined) { michael@0: app.removable = true; michael@0: } michael@0: michael@0: // Default to a non privileged status. michael@0: if (app.appStatus === undefined) { michael@0: app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED; michael@0: } michael@0: michael@0: // Default to NO_APP_ID and not in browser. michael@0: if (app.installerAppId === undefined) { michael@0: app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID; michael@0: } michael@0: if (app.installerIsBrowser === undefined) { michael@0: app.installerIsBrowser = false; michael@0: } michael@0: michael@0: // Default installState to "installed", and reset if we shutdown michael@0: // during an update. michael@0: if (app.installState === undefined || michael@0: app.installState === "updating") { michael@0: app.installState = "installed"; michael@0: } michael@0: michael@0: // Default storeId to "" and storeVersion to 0 michael@0: if (this.webapps[id].storeId === undefined) { michael@0: this.webapps[id].storeId = ""; michael@0: } michael@0: if (this.webapps[id].storeVersion === undefined) { michael@0: this.webapps[id].storeVersion = 0; michael@0: } michael@0: michael@0: // Default role to "". michael@0: if (this.webapps[id].role === undefined) { michael@0: this.webapps[id].role = ""; michael@0: } michael@0: michael@0: // At startup we can't be downloading, and the $TMP directory michael@0: // will be empty so we can't just apply a staged update. michael@0: app.downloading = false; michael@0: app.readyToApplyDownload = false; michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // Notify we are starting with registering apps. michael@0: _registryStarted: Promise.defer(), michael@0: notifyAppsRegistryStart: function notifyAppsRegistryStart() { michael@0: Services.obs.notifyObservers(this, "webapps-registry-start", null); michael@0: this._registryStarted.resolve(); michael@0: }, michael@0: michael@0: get registryStarted() { michael@0: return this._registryStarted.promise; michael@0: }, michael@0: michael@0: // Notify we are done with registering apps and save a copy of the registry. michael@0: _registryReady: Promise.defer(), michael@0: notifyAppsRegistryReady: function notifyAppsRegistryReady() { michael@0: this._registryReady.resolve(); michael@0: Services.obs.notifyObservers(this, "webapps-registry-ready", null); michael@0: this._saveApps(); michael@0: }, michael@0: michael@0: get registryReady() { michael@0: return this._registryReady.promise; michael@0: }, michael@0: michael@0: // Ensure that the .to property in redirects is a relative URL. michael@0: sanitizeRedirects: function sanitizeRedirects(aSource) { michael@0: if (!aSource) { michael@0: return null; michael@0: } michael@0: michael@0: let res = []; michael@0: for (let i = 0; i < aSource.length; i++) { michael@0: let redirect = aSource[i]; michael@0: if (redirect.from && redirect.to && michael@0: isAbsoluteURI(redirect.from) && michael@0: !isAbsoluteURI(redirect.to)) { michael@0: res.push(redirect); michael@0: } michael@0: } michael@0: return res.length > 0 ? res : null; michael@0: }, michael@0: michael@0: // Registers all the activities and system messages. michael@0: registerAppsHandlers: function(aRunUpdate) { michael@0: this.notifyAppsRegistryStart(); michael@0: let ids = []; michael@0: for (let id in this.webapps) { michael@0: ids.push({ id: id }); michael@0: } michael@0: if (supportSystemMessages()) { michael@0: this._processManifestForIds(ids, aRunUpdate); michael@0: } else { michael@0: // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on michael@0: // _processManifestForIds so as to not reading the manifests michael@0: // twice michael@0: this._readManifests(ids).then((aResults) => { michael@0: aResults.forEach((aResult) => { michael@0: if (!aResult.manifest) { michael@0: // If we can't load the manifest, we probably have a corrupted michael@0: // registry. We delete the app since we can't do anything with it. michael@0: delete this.webapps[aResult.id]; michael@0: return; michael@0: } michael@0: let app = this.webapps[aResult.id]; michael@0: app.csp = aResult.manifest.csp || ""; michael@0: app.role = aResult.manifest.role || ""; michael@0: if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { michael@0: app.redirects = this.sanitizeRedirects(aResult.redirects); michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: // Nothing else to do but notifying we're ready. michael@0: this.notifyAppsRegistryReady(); michael@0: } michael@0: }, michael@0: michael@0: updateDataStoreForApp: function(aId) { michael@0: if (!this.webapps[aId]) { michael@0: return; michael@0: } michael@0: michael@0: // Create or Update the DataStore for this app michael@0: this._readManifests([{ id: aId }]).then((aResult) => { michael@0: let app = this.webapps[aId]; michael@0: this.updateDataStore(app.localId, app.origin, app.manifestURL, michael@0: aResult[0].manifest, app.appStatus); michael@0: }); michael@0: }, michael@0: michael@0: updatePermissionsForApp: function(aId) { michael@0: if (!this.webapps[aId]) { michael@0: return; michael@0: } michael@0: michael@0: // Install the permissions for this app, as if we were updating michael@0: // to cleanup the old ones if needed. michael@0: // TODO It's not clear what this should do when there are multiple profiles. michael@0: if (supportUseCurrentProfile()) { michael@0: this._readManifests([{ id: aId }]).then((aResult) => { michael@0: let data = aResult[0]; michael@0: PermissionsInstaller.installPermissions({ michael@0: manifest: data.manifest, michael@0: manifestURL: this.webapps[aId].manifestURL, michael@0: origin: this.webapps[aId].origin michael@0: }, true, function() { michael@0: debug("Error installing permissions for " + aId); michael@0: }); michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: updateOfflineCacheForApp: function(aId) { michael@0: let app = this.webapps[aId]; michael@0: this._readManifests([{ id: aId }]).then((aResult) => { michael@0: let manifest = new ManifestHelper(aResult[0].manifest, app.origin); michael@0: OfflineCacheInstaller.installCache({ michael@0: cachePath: app.cachePath, michael@0: appId: aId, michael@0: origin: Services.io.newURI(app.origin, null, null), michael@0: localId: app.localId, michael@0: appcache_path: manifest.fullAppcachePath() michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: // Installs a 3rd party app. michael@0: installPreinstalledApp: function installPreinstalledApp(aId) { michael@0: #ifdef MOZ_WIDGET_GONK michael@0: let app = this.webapps[aId]; michael@0: let baseDir; michael@0: try { michael@0: baseDir = FileUtils.getDir("coreAppsDir", ["webapps", aId], false); michael@0: if (!baseDir.exists()) { michael@0: return; michael@0: } else if (!baseDir.directoryEntries.hasMoreElements()) { michael@0: debug("Error: Core app in " + baseDir.path + " is empty"); michael@0: return; michael@0: } michael@0: } catch(e) { michael@0: // In ENG builds, we don't have apps in coreAppsDir. michael@0: return; michael@0: } michael@0: michael@0: let filesToMove; michael@0: let isPackage; michael@0: michael@0: let updateFile = baseDir.clone(); michael@0: updateFile.append("update.webapp"); michael@0: if (!updateFile.exists()) { michael@0: // The update manifest is missing, this is a hosted app only if there is michael@0: // no application.zip michael@0: let appFile = baseDir.clone(); michael@0: appFile.append("application.zip"); michael@0: if (appFile.exists()) { michael@0: return; michael@0: } michael@0: michael@0: isPackage = false; michael@0: filesToMove = ["manifest.webapp"]; michael@0: } else { michael@0: isPackage = true; michael@0: filesToMove = ["application.zip", "update.webapp"]; michael@0: } michael@0: michael@0: debug("Installing 3rd party app : " + aId + michael@0: " from " + baseDir.path); michael@0: michael@0: // We copy this app to DIRECTORY_NAME/$aId, and set the base path as needed. michael@0: let destDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); michael@0: michael@0: filesToMove.forEach(function(aFile) { michael@0: let file = baseDir.clone(); michael@0: file.append(aFile); michael@0: try { michael@0: file.copyTo(destDir, aFile); michael@0: } catch(e) { michael@0: debug("Error: Failed to copy " + file.path + " to " + destDir.path); michael@0: } michael@0: }); michael@0: michael@0: app.installState = "installed"; michael@0: app.cachePath = app.basePath; michael@0: app.basePath = OS.Path.dirname(this.appsFile); michael@0: michael@0: if (!isPackage) { michael@0: return; michael@0: } michael@0: michael@0: app.origin = "app://" + aId; michael@0: michael@0: // Do this for all preinstalled apps... we can't know at this michael@0: // point if the updates will be signed or not and it doesn't michael@0: // hurt to have it always. michael@0: app.storeId = STORE_ID_PENDING_PREFIX + app.installOrigin; michael@0: michael@0: // Extract the manifest.webapp file from application.zip. michael@0: let zipFile = baseDir.clone(); michael@0: zipFile.append("application.zip"); michael@0: let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] michael@0: .createInstance(Ci.nsIZipReader); michael@0: try { michael@0: debug("Opening " + zipFile.path); michael@0: zipReader.open(zipFile); michael@0: if (!zipReader.hasEntry("manifest.webapp")) { michael@0: throw "MISSING_MANIFEST"; michael@0: } michael@0: let manifestFile = destDir.clone(); michael@0: manifestFile.append("manifest.webapp"); michael@0: zipReader.extract("manifest.webapp", manifestFile); michael@0: } catch(e) { michael@0: // If we are unable to extract the manifest, cleanup and remove this app. michael@0: debug("Cleaning up: " + e); michael@0: destDir.remove(true); michael@0: delete this.webapps[aId]; michael@0: } finally { michael@0: zipReader.close(); michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: // For hosted apps, uninstall an app served from http:// if we have michael@0: // one installed from the same url with an https:// scheme. michael@0: removeIfHttpsDuplicate: function(aId) { michael@0: #ifdef MOZ_WIDGET_GONK michael@0: let app = this.webapps[aId]; michael@0: if (!app || !app.origin.startsWith("http://")) { michael@0: return; michael@0: } michael@0: michael@0: let httpsManifestURL = michael@0: "https://" + app.manifestURL.substring("http://".length); michael@0: michael@0: // This will uninstall the http apps and remove any data hold by this michael@0: // app. Bug 948105 tracks data migration from http to https apps. michael@0: for (let id in this.webapps) { michael@0: if (this.webapps[id].manifestURL === httpsManifestURL) { michael@0: debug("Found a http/https match: " + app.manifestURL + " / " + michael@0: this.webapps[id].manifestURL); michael@0: this.uninstall(app.manifestURL, function() {}, function() {}); michael@0: return; michael@0: } michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: // Implements the core of bug 787439 michael@0: // if at first run, go through these steps: michael@0: // a. load the core apps registry. michael@0: // b. uninstall any core app from the current registry but not in the michael@0: // new core apps registry. michael@0: // c. for all apps in the new core registry, install them if they are not michael@0: // yet in the current registry, and run installPermissions() michael@0: installSystemApps: function() { michael@0: return Task.spawn(function() { michael@0: let file; michael@0: try { michael@0: file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false); michael@0: } catch(e) { } michael@0: michael@0: if (!file || !file.exists()) { michael@0: return; michael@0: } michael@0: michael@0: // a michael@0: let data = yield AppsUtils.loadJSONAsync(file.path); michael@0: if (!data) { michael@0: return; michael@0: } michael@0: michael@0: // b : core apps are not removable. michael@0: for (let id in this.webapps) { michael@0: if (id in data || this.webapps[id].removable) michael@0: continue; michael@0: // Remove the permissions, cookies and private data for this app. michael@0: let localId = this.webapps[id].localId; michael@0: let permMgr = Cc["@mozilla.org/permissionmanager;1"] michael@0: .getService(Ci.nsIPermissionManager); michael@0: permMgr.removePermissionsForApp(localId, false); michael@0: Services.cookies.removeCookiesForApp(localId, false); michael@0: this._clearPrivateData(localId, false); michael@0: delete this.webapps[id]; michael@0: } michael@0: michael@0: let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); michael@0: // c michael@0: for (let id in data) { michael@0: // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org) michael@0: // Use that property to check if they are new or not. michael@0: if (!(id in this.webapps)) { michael@0: this.webapps[id] = data[id]; michael@0: this.webapps[id].basePath = appDir.path; michael@0: michael@0: this.webapps[id].id = id; michael@0: michael@0: // Create a new localId. michael@0: this.webapps[id].localId = this._nextLocalId(); michael@0: michael@0: // Core apps are not removable. michael@0: if (this.webapps[id].removable === undefined) { michael@0: this.webapps[id].removable = false; michael@0: } michael@0: } else { michael@0: // we fall into this case if the app is present in /system/b2g/webapps/webapps.json michael@0: // and in /data/local/webapps/webapps.json: this happens when updating gaia apps michael@0: // Confere bug 989876 michael@0: this.webapps[id].updateTime = data[id].updateTime; michael@0: this.webapps[id].lastUpdateCheck = data[id].updateTime; michael@0: } michael@0: } michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: loadAndUpdateApps: function() { michael@0: return Task.spawn(function() { michael@0: let runUpdate = AppsUtils.isFirstRun(Services.prefs); michael@0: michael@0: yield this.loadCurrentRegistry(); michael@0: michael@0: if (runUpdate) { michael@0: #ifdef MOZ_WIDGET_GONK michael@0: yield this.installSystemApps(); michael@0: #endif michael@0: michael@0: // At first run, install preloaded apps and set up their permissions. michael@0: for (let id in this.webapps) { michael@0: this.installPreinstalledApp(id); michael@0: this.removeIfHttpsDuplicate(id); michael@0: if (!this.webapps[id]) { michael@0: continue; michael@0: } michael@0: this.updateOfflineCacheForApp(id); michael@0: this.updatePermissionsForApp(id); michael@0: } michael@0: // Need to update the persisted list of apps since michael@0: // installPreinstalledApp() removes the ones failing to install. michael@0: this._saveApps(); michael@0: } michael@0: michael@0: // DataStores must be initialized at startup. michael@0: for (let id in this.webapps) { michael@0: this.updateDataStoreForApp(id); michael@0: } michael@0: michael@0: this.registerAppsHandlers(runUpdate); michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) { michael@0: // Just Certified Apps can use DataStores michael@0: let prefName = "dom.testing.datastore_enabled_for_hosted_apps"; michael@0: if (aAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED && michael@0: (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID || michael@0: !Services.prefs.getBoolPref(prefName))) { michael@0: return; michael@0: } michael@0: michael@0: if ('datastores-owned' in aManifest) { michael@0: for (let name in aManifest['datastores-owned']) { michael@0: let readonly = "access" in aManifest['datastores-owned'][name] michael@0: ? aManifest['datastores-owned'][name].access == 'readonly' michael@0: : false; michael@0: michael@0: dataStoreService.installDataStore(aId, name, aOrigin, aManifestURL, michael@0: readonly); michael@0: } michael@0: } michael@0: michael@0: if ('datastores-access' in aManifest) { michael@0: for (let name in aManifest['datastores-access']) { michael@0: let readonly = ("readonly" in aManifest['datastores-access'][name]) && michael@0: !aManifest['datastores-access'][name].readonly michael@0: ? false : true; michael@0: michael@0: dataStoreService.installAccessDataStore(aId, name, aOrigin, michael@0: aManifestURL, readonly); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // |aEntryPoint| is either the entry_point name or the null in which case we michael@0: // use the root of the manifest. michael@0: // michael@0: // TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...). michael@0: _registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) { michael@0: let root = aManifest; michael@0: if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { michael@0: root = aManifest.entry_points[aEntryPoint]; michael@0: } michael@0: michael@0: if (!root.messages || !Array.isArray(root.messages) || michael@0: root.messages.length == 0) { michael@0: return; michael@0: } michael@0: michael@0: let manifest = new ManifestHelper(aManifest, aApp.origin); michael@0: let launchPath = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), null, null); michael@0: let manifestURL = Services.io.newURI(aApp.manifestURL, null, null); michael@0: root.messages.forEach(function registerPages(aMessage) { michael@0: let href = launchPath; michael@0: let messageName; michael@0: if (typeof(aMessage) === "object" && Object.keys(aMessage).length === 1) { michael@0: messageName = Object.keys(aMessage)[0]; michael@0: let uri; michael@0: try { michael@0: uri = manifest.resolveFromOrigin(aMessage[messageName]); michael@0: } catch(e) { michael@0: debug("system message url (" + aMessage[messageName] + ") is invalid, skipping. " + michael@0: "Error is: " + e); michael@0: return; michael@0: } michael@0: href = Services.io.newURI(uri, null, null); michael@0: } else { michael@0: messageName = aMessage; michael@0: } michael@0: michael@0: if (SystemMessagePermissionsChecker michael@0: .isSystemMessagePermittedToRegister(messageName, michael@0: aApp.origin, michael@0: aManifest)) { michael@0: msgmgr.registerPage(messageName, href, manifestURL); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // |aEntryPoint| is either the entry_point name or the null in which case we michael@0: // use the root of the manifest. michael@0: // michael@0: // TODO Bug 908094 Refine _registerInterAppConnectionsForEntryPoint(...). michael@0: _registerInterAppConnectionsForEntryPoint: function(aManifest, aApp, michael@0: aEntryPoint) { michael@0: let root = aManifest; michael@0: if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { michael@0: root = aManifest.entry_points[aEntryPoint]; michael@0: } michael@0: michael@0: let connections = root.connections; michael@0: if (!connections) { michael@0: return; michael@0: } michael@0: michael@0: if ((typeof connections) !== "object") { michael@0: debug("|connections| is not an object. Skipping: " + connections); michael@0: return; michael@0: } michael@0: michael@0: let manifest = new ManifestHelper(aManifest, aApp.origin); michael@0: let launchPathURI = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), michael@0: null, null); michael@0: let manifestURI = Services.io.newURI(aApp.manifestURL, null, null); michael@0: michael@0: for (let keyword in connections) { michael@0: let connection = connections[keyword]; michael@0: michael@0: // Resolve the handler path from origin. If |handler_path| is absent, michael@0: // use |launch_path| as default. michael@0: let fullHandlerPath; michael@0: let handlerPath = connection.handler_path; michael@0: if (handlerPath) { michael@0: try { michael@0: fullHandlerPath = manifest.resolveFromOrigin(handlerPath); michael@0: } catch(e) { michael@0: debug("Connection's handler path is invalid. Skipping: keyword: " + michael@0: keyword + " handler_path: " + handlerPath); michael@0: continue; michael@0: } michael@0: } michael@0: let handlerPageURI = fullHandlerPath michael@0: ? Services.io.newURI(fullHandlerPath, null, null) michael@0: : launchPathURI; michael@0: michael@0: if (SystemMessagePermissionsChecker michael@0: .isSystemMessagePermittedToRegister("connection", michael@0: aApp.origin, michael@0: aManifest)) { michael@0: msgmgr.registerPage("connection", handlerPageURI, manifestURI); michael@0: } michael@0: michael@0: interAppCommService. michael@0: registerConnection(keyword, michael@0: handlerPageURI, michael@0: manifestURI, michael@0: connection.description, michael@0: connection.rules); michael@0: } michael@0: }, michael@0: michael@0: _registerSystemMessages: function(aManifest, aApp) { michael@0: this._registerSystemMessagesForEntryPoint(aManifest, aApp, null); michael@0: michael@0: if (!aManifest.entry_points) { michael@0: return; michael@0: } michael@0: michael@0: for (let entryPoint in aManifest.entry_points) { michael@0: this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint); michael@0: } michael@0: }, michael@0: michael@0: _registerInterAppConnections: function(aManifest, aApp) { michael@0: this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, null); michael@0: michael@0: if (!aManifest.entry_points) { michael@0: return; michael@0: } michael@0: michael@0: for (let entryPoint in aManifest.entry_points) { michael@0: this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, michael@0: entryPoint); michael@0: } michael@0: }, michael@0: michael@0: // |aEntryPoint| is either the entry_point name or the null in which case we michael@0: // use the root of the manifest. michael@0: _createActivitiesToRegister: function(aManifest, aApp, aEntryPoint, aRunUpdate) { michael@0: let activitiesToRegister = []; michael@0: let root = aManifest; michael@0: if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { michael@0: root = aManifest.entry_points[aEntryPoint]; michael@0: } michael@0: michael@0: if (!root.activities) { michael@0: return activitiesToRegister; michael@0: } michael@0: michael@0: let manifest = new ManifestHelper(aManifest, aApp.origin); michael@0: for (let activity in root.activities) { michael@0: let description = root.activities[activity]; michael@0: let href = description.href; michael@0: if (!href) { michael@0: href = manifest.launch_path; michael@0: } michael@0: michael@0: try { michael@0: href = manifest.resolveFromOrigin(href); michael@0: } catch (e) { michael@0: debug("Activity href (" + href + ") is invalid, skipping. " + michael@0: "Error is: " + e); michael@0: continue; michael@0: } michael@0: michael@0: // Make a copy of the description object since we don't want to modify michael@0: // the manifest itself, but need to register with a resolved URI. michael@0: let newDesc = {}; michael@0: for (let prop in description) { michael@0: newDesc[prop] = description[prop]; michael@0: } michael@0: newDesc.href = href; michael@0: michael@0: debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' + michael@0: activity + ', description.href is ' + newDesc.href); michael@0: michael@0: if (aRunUpdate) { michael@0: activitiesToRegister.push({ "manifest": aApp.manifestURL, michael@0: "name": activity, michael@0: "icon": manifest.iconURLForSize(128), michael@0: "description": newDesc }); michael@0: } michael@0: michael@0: let launchPath = Services.io.newURI(href, null, null); michael@0: let manifestURL = Services.io.newURI(aApp.manifestURL, null, null); michael@0: michael@0: if (SystemMessagePermissionsChecker michael@0: .isSystemMessagePermittedToRegister("activity", michael@0: aApp.origin, michael@0: aManifest)) { michael@0: msgmgr.registerPage("activity", launchPath, manifestURL); michael@0: } michael@0: } michael@0: return activitiesToRegister; michael@0: }, michael@0: michael@0: // |aAppsToRegister| contains an array of apps to be registered, where michael@0: // each element is an object in the format of {manifest: foo, app: bar}. michael@0: _registerActivitiesForApps: function(aAppsToRegister, aRunUpdate) { michael@0: // Collect the activities to be registered for root and entry_points. michael@0: let activitiesToRegister = []; michael@0: aAppsToRegister.forEach(function (aApp) { michael@0: let manifest = aApp.manifest; michael@0: let app = aApp.app; michael@0: activitiesToRegister.push.apply(activitiesToRegister, michael@0: this._createActivitiesToRegister(manifest, app, null, aRunUpdate)); michael@0: michael@0: if (!manifest.entry_points) { michael@0: return; michael@0: } michael@0: michael@0: for (let entryPoint in manifest.entry_points) { michael@0: activitiesToRegister.push.apply(activitiesToRegister, michael@0: this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate)); michael@0: } michael@0: }, this); michael@0: michael@0: if (!aRunUpdate || activitiesToRegister.length == 0) { michael@0: this.notifyAppsRegistryReady(); michael@0: return; michael@0: } michael@0: michael@0: // Send the array carrying all the activities to be registered. michael@0: cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister); michael@0: }, michael@0: michael@0: // Better to directly use |_registerActivitiesForApps()| if we have michael@0: // multiple apps to be registered for activities. michael@0: _registerActivities: function(aManifest, aApp, aRunUpdate) { michael@0: this._registerActivitiesForApps([{ manifest: aManifest, app: aApp }], aRunUpdate); michael@0: }, michael@0: michael@0: // |aEntryPoint| is either the entry_point name or the null in which case we michael@0: // use the root of the manifest. michael@0: _createActivitiesToUnregister: function(aManifest, aApp, aEntryPoint) { michael@0: let activitiesToUnregister = []; michael@0: let root = aManifest; michael@0: if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { michael@0: root = aManifest.entry_points[aEntryPoint]; michael@0: } michael@0: michael@0: if (!root.activities) { michael@0: return activitiesToUnregister; michael@0: } michael@0: michael@0: for (let activity in root.activities) { michael@0: let description = root.activities[activity]; michael@0: activitiesToUnregister.push({ "manifest": aApp.manifestURL, michael@0: "name": activity, michael@0: "description": description }); michael@0: } michael@0: return activitiesToUnregister; michael@0: }, michael@0: michael@0: // |aAppsToUnregister| contains an array of apps to be unregistered, where michael@0: // each element is an object in the format of {manifest: foo, app: bar}. michael@0: _unregisterActivitiesForApps: function(aAppsToUnregister) { michael@0: // Collect the activities to be unregistered for root and entry_points. michael@0: let activitiesToUnregister = []; michael@0: aAppsToUnregister.forEach(function (aApp) { michael@0: let manifest = aApp.manifest; michael@0: let app = aApp.app; michael@0: activitiesToUnregister.push.apply(activitiesToUnregister, michael@0: this._createActivitiesToUnregister(manifest, app, null)); michael@0: michael@0: if (!manifest.entry_points) { michael@0: return; michael@0: } michael@0: michael@0: for (let entryPoint in manifest.entry_points) { michael@0: activitiesToUnregister.push.apply(activitiesToUnregister, michael@0: this._createActivitiesToUnregister(manifest, app, entryPoint)); michael@0: } michael@0: }, this); michael@0: michael@0: // Send the array carrying all the activities to be unregistered. michael@0: cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister); michael@0: }, michael@0: michael@0: // Better to directly use |_unregisterActivitiesForApps()| if we have michael@0: // multiple apps to be unregistered for activities. michael@0: _unregisterActivities: function(aManifest, aApp) { michael@0: this._unregisterActivitiesForApps([{ manifest: aManifest, app: aApp }]); michael@0: }, michael@0: michael@0: _processManifestForIds: function(aIds, aRunUpdate) { michael@0: this._readManifests(aIds).then((aResults) => { michael@0: let appsToRegister = []; michael@0: aResults.forEach((aResult) => { michael@0: let app = this.webapps[aResult.id]; michael@0: let manifest = aResult.manifest; michael@0: if (!manifest) { michael@0: // If we can't load the manifest, we probably have a corrupted michael@0: // registry. We delete the app since we can't do anything with it. michael@0: delete this.webapps[aResult.id]; michael@0: return; michael@0: } michael@0: app.name = manifest.name; michael@0: app.csp = manifest.csp || ""; michael@0: app.role = manifest.role || ""; michael@0: if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { michael@0: app.redirects = this.sanitizeRedirects(manifest.redirects); michael@0: } michael@0: this._registerSystemMessages(manifest, app); michael@0: this._registerInterAppConnections(manifest, app); michael@0: appsToRegister.push({ manifest: manifest, app: app }); michael@0: }); michael@0: this._registerActivitiesForApps(appsToRegister, aRunUpdate); michael@0: }); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "xpcom-shutdown") { michael@0: this.messages.forEach((function(msgName) { michael@0: ppmm.removeMessageListener(msgName, this); michael@0: }).bind(this)); michael@0: Services.obs.removeObserver(this, "xpcom-shutdown"); michael@0: cpmm = null; michael@0: ppmm = null; michael@0: } else if (aTopic == "memory-pressure") { michael@0: // Clear the manifest cache on memory pressure. michael@0: this._manifestCache = {}; michael@0: } michael@0: }, michael@0: michael@0: addMessageListener: function(aMsgNames, aApp, aMm) { michael@0: aMsgNames.forEach(function (aMsgName) { michael@0: let man = aApp && aApp.manifestURL; michael@0: if (!(aMsgName in this.children)) { michael@0: this.children[aMsgName] = []; michael@0: } michael@0: michael@0: let mmFound = this.children[aMsgName].some(function(mmRef) { michael@0: if (mmRef.mm === aMm) { michael@0: mmRef.refCount++; michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: michael@0: if (!mmFound) { michael@0: this.children[aMsgName].push({ michael@0: mm: aMm, michael@0: refCount: 1 michael@0: }); michael@0: } michael@0: michael@0: // If the state reported by the registration is outdated, update it now. michael@0: if ((aMsgName === 'Webapps:FireEvent') || michael@0: (aMsgName === 'Webapps:UpdateState')) { michael@0: if (man) { michael@0: let app = this.getAppByManifestURL(aApp.manifestURL); michael@0: if (app && ((aApp.installState !== app.installState) || michael@0: (aApp.downloading !== app.downloading))) { michael@0: debug("Got a registration from an outdated app: " + michael@0: aApp.manifestURL); michael@0: let aEvent ={ michael@0: type: app.installState, michael@0: app: app, michael@0: manifestURL: app.manifestURL, michael@0: manifest: app.manifest michael@0: }; michael@0: aMm.sendAsyncMessage(aMsgName, aEvent); michael@0: } michael@0: } michael@0: } michael@0: }, this); michael@0: }, michael@0: michael@0: removeMessageListener: function(aMsgNames, aMm) { michael@0: if (aMsgNames.length === 1 && michael@0: aMsgNames[0] === "Webapps:Internal:AllMessages") { michael@0: for (let msgName in this.children) { michael@0: let msg = this.children[msgName]; michael@0: michael@0: for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) { michael@0: let mmRef = msg[mmI]; michael@0: if (mmRef.mm === aMm) { michael@0: msg.splice(mmI, 1); michael@0: } michael@0: } michael@0: michael@0: if (msg.length === 0) { michael@0: delete this.children[msgName]; michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: michael@0: aMsgNames.forEach(function(aMsgName) { michael@0: if (!(aMsgName in this.children)) { michael@0: return; michael@0: } michael@0: michael@0: let removeIndex; michael@0: this.children[aMsgName].some(function(mmRef, index) { michael@0: if (mmRef.mm === aMm) { michael@0: mmRef.refCount--; michael@0: if (mmRef.refCount === 0) { michael@0: removeIndex = index; michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: michael@0: if (removeIndex) { michael@0: this.children[aMsgName].splice(removeIndex, 1); michael@0: } michael@0: }, this); michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: // nsIPrefBranch throws if pref does not exist, faster to simply write michael@0: // the pref instead of first checking if it is false. michael@0: Services.prefs.setBoolPref("dom.mozApps.used", true); michael@0: michael@0: // We need to check permissions for calls coming from mozApps.mgmt. michael@0: // These are: getAll(), getNotInstalled(), applyDownload() and uninstall(). michael@0: if (["Webapps:GetAll", michael@0: "Webapps:GetNotInstalled", michael@0: "Webapps:ApplyDownload", michael@0: "Webapps:Uninstall"].indexOf(aMessage.name) != -1) { michael@0: if (!aMessage.target.assertPermission("webapps-manage")) { michael@0: debug("mozApps message " + aMessage.name + michael@0: " from a content process with no 'webapps-manage' privileges."); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: let msg = aMessage.data || {}; michael@0: let mm = aMessage.target; michael@0: msg.mm = mm; michael@0: michael@0: switch (aMessage.name) { michael@0: case "Webapps:Install": { michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg)); michael@0: #else michael@0: this.doInstall(msg, mm); michael@0: #endif michael@0: break; michael@0: } michael@0: case "Webapps:GetSelf": michael@0: this.getSelf(msg, mm); michael@0: break; michael@0: case "Webapps:Uninstall": michael@0: this.doUninstall(msg, mm); michael@0: break; michael@0: case "Webapps:Launch": michael@0: this.doLaunch(msg, mm); michael@0: break; michael@0: case "Webapps:CheckInstalled": michael@0: this.checkInstalled(msg, mm); michael@0: break; michael@0: case "Webapps:GetInstalled": michael@0: this.getInstalled(msg, mm); michael@0: break; michael@0: case "Webapps:GetNotInstalled": michael@0: this.getNotInstalled(msg, mm); michael@0: break; michael@0: case "Webapps:GetAll": michael@0: this.doGetAll(msg, mm); michael@0: break; michael@0: case "Webapps:InstallPackage": { michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg)); michael@0: #else michael@0: this.doInstallPackage(msg, mm); michael@0: #endif michael@0: break; michael@0: } michael@0: case "Webapps:RegisterForMessages": michael@0: this.addMessageListener(msg.messages, msg.app, mm); michael@0: break; michael@0: case "Webapps:UnregisterForMessages": michael@0: this.removeMessageListener(msg, mm); michael@0: break; michael@0: case "child-process-shutdown": michael@0: this.removeMessageListener(["Webapps:Internal:AllMessages"], mm); michael@0: break; michael@0: case "Webapps:GetList": michael@0: this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], null, mm); michael@0: return this.webapps; michael@0: case "Webapps:Download": michael@0: this.startDownload(msg.manifestURL); michael@0: break; michael@0: case "Webapps:CancelDownload": michael@0: this.cancelDownload(msg.manifestURL); michael@0: break; michael@0: case "Webapps:CheckForUpdate": michael@0: this.checkForUpdate(msg, mm); michael@0: break; michael@0: case "Webapps:ApplyDownload": michael@0: this.applyDownload(msg.manifestURL); michael@0: break; michael@0: case "Activities:Register:OK": michael@0: this.notifyAppsRegistryReady(); michael@0: break; michael@0: case "Webapps:Install:Return:Ack": michael@0: this.onInstallSuccessAck(msg.manifestURL); michael@0: break; michael@0: case "Webapps:AddReceipt": michael@0: this.addReceipt(msg, mm); michael@0: break; michael@0: case "Webapps:RemoveReceipt": michael@0: this.removeReceipt(msg, mm); michael@0: break; michael@0: case "Webapps:ReplaceReceipt": michael@0: this.replaceReceipt(msg, mm); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: getAppInfo: function getAppInfo(aAppId) { michael@0: return AppsUtils.getAppInfo(this.webapps, aAppId); michael@0: }, michael@0: michael@0: // Some messages can be listened by several content processes: michael@0: // Webapps:AddApp michael@0: // Webapps:RemoveApp michael@0: // Webapps:Install:Return:OK michael@0: // Webapps:Uninstall:Return:OK michael@0: // Webapps:Uninstall:Broadcast:Return:OK michael@0: // Webapps:FireEvent michael@0: // Webapps:checkForUpdate:Return:OK michael@0: // Webapps:UpdateState michael@0: broadcastMessage: function broadcastMessage(aMsgName, aContent) { michael@0: if (!(aMsgName in this.children)) { michael@0: return; michael@0: } michael@0: this.children[aMsgName].forEach(function(mmRef) { michael@0: mmRef.mm.sendAsyncMessage(aMsgName, aContent); michael@0: }); michael@0: }, michael@0: michael@0: registerUpdateHandler: function(aHandler) { michael@0: this._updateHandlers.push(aHandler); michael@0: }, michael@0: michael@0: unregisterUpdateHandler: function(aHandler) { michael@0: let index = this._updateHandlers.indexOf(aHandler); michael@0: if (index != -1) { michael@0: this._updateHandlers.splice(index, 1); michael@0: } michael@0: }, michael@0: michael@0: notifyUpdateHandlers: function(aApp, aManifest, aZipPath) { michael@0: for (let updateHandler of this._updateHandlers) { michael@0: updateHandler(aApp, aManifest, aZipPath); michael@0: } michael@0: }, michael@0: michael@0: _getAppDir: function(aId) { michael@0: return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); michael@0: }, michael@0: michael@0: _writeFile: function(aPath, aData) { michael@0: debug("Saving " + aPath); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.initWithPath(aPath); michael@0: michael@0: // Initialize the file output stream michael@0: let ostream = FileUtils.openSafeFileOutputStream(file); michael@0: michael@0: // Obtain a converter to convert our data to a UTF-8 encoded input stream. michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: michael@0: // Asynchronously copy the data to the file. michael@0: let istream = converter.convertToInputStream(aData); michael@0: NetUtil.asyncCopy(istream, ostream, function(aResult) { michael@0: if (!Components.isSuccessCode(aResult)) { michael@0: deferred.reject() michael@0: } else { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: doLaunch: function (aData, aMm) { michael@0: this.launch( michael@0: aData.manifestURL, michael@0: aData.startPoint, michael@0: aData.timestamp, michael@0: function onsuccess() { michael@0: aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData); michael@0: }, michael@0: function onfailure(reason) { michael@0: aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: launch: function launch(aManifestURL, aStartPoint, aTimeStamp, aOnSuccess, aOnFailure) { michael@0: let app = this.getAppByManifestURL(aManifestURL); michael@0: if (!app) { michael@0: aOnFailure("NO_SUCH_APP"); michael@0: return; michael@0: } michael@0: michael@0: // Fire an error when trying to launch an app that is not michael@0: // yet fully installed. michael@0: if (app.installState == "pending") { michael@0: aOnFailure("PENDING_APP_NOT_LAUNCHABLE"); michael@0: return; michael@0: } michael@0: michael@0: // We have to clone the app object as nsIDOMApplication objects are michael@0: // stringified as an empty object. (see bug 830376) michael@0: let appClone = AppsUtils.cloneAppObject(app); michael@0: appClone.startPoint = aStartPoint; michael@0: appClone.timestamp = aTimeStamp; michael@0: Services.obs.notifyObservers(null, "webapps-launch", JSON.stringify(appClone)); michael@0: aOnSuccess(); michael@0: }, michael@0: michael@0: close: function close(aApp) { michael@0: debug("close"); michael@0: michael@0: // We have to clone the app object as nsIDOMApplication objects are michael@0: // stringified as an empty object. (see bug 830376) michael@0: let appClone = AppsUtils.cloneAppObject(aApp); michael@0: Services.obs.notifyObservers(null, "webapps-close", JSON.stringify(appClone)); michael@0: }, michael@0: michael@0: cancelDownload: function cancelDownload(aManifestURL, aError) { michael@0: debug("cancelDownload " + aManifestURL); michael@0: let error = aError || "DOWNLOAD_CANCELED"; michael@0: let download = AppDownloadManager.get(aManifestURL); michael@0: if (!download) { michael@0: debug("Could not find a download for " + aManifestURL); michael@0: return; michael@0: } michael@0: michael@0: let app = this.webapps[download.appId]; michael@0: michael@0: if (download.cacheUpdate) { michael@0: try { michael@0: download.cacheUpdate.cancel(); michael@0: } catch (e) { michael@0: debug (e); michael@0: } michael@0: } else if (download.channel) { michael@0: try { michael@0: download.channel.cancel(Cr.NS_BINDING_ABORTED); michael@0: } catch(e) { } michael@0: } else { michael@0: return; michael@0: } michael@0: michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: { michael@0: progress: 0, michael@0: installState: download.previousState, michael@0: downloading: false michael@0: }, michael@0: error: error, michael@0: manifestURL: app.manifestURL, michael@0: }) michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloaderror", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: }); michael@0: AppDownloadManager.remove(aManifestURL); michael@0: }, michael@0: michael@0: startDownload: Task.async(function*(aManifestURL) { michael@0: debug("startDownload for " + aManifestURL); michael@0: michael@0: let id = this._appIdForManifestURL(aManifestURL); michael@0: let app = this.webapps[id]; michael@0: if (!app) { michael@0: debug("startDownload: No app found for " + aManifestURL); michael@0: return; michael@0: } michael@0: michael@0: if (app.downloading) { michael@0: debug("app is already downloading. Ignoring."); michael@0: return; michael@0: } michael@0: michael@0: // If the caller is trying to start a download but we have nothing to michael@0: // download, send an error. michael@0: if (!app.downloadAvailable) { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: error: "NO_DOWNLOAD_AVAILABLE", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloaderror", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: // First of all, we check if the download is supposed to update an michael@0: // already installed application. michael@0: let isUpdate = (app.installState == "installed"); michael@0: michael@0: // An app download would only be triggered for two reasons: an app michael@0: // update or while retrying to download a previously failed or canceled michael@0: // instalation. michael@0: app.retryingDownload = !isUpdate; michael@0: michael@0: // We need to get the update manifest here, not the webapp manifest. michael@0: // If this is an update, the update manifest is staged. michael@0: let file = FileUtils.getFile(DIRECTORY_NAME, michael@0: ["webapps", id, michael@0: isUpdate ? "staged-update.webapp" michael@0: : "update.webapp"], michael@0: true); michael@0: michael@0: if (!file.exists()) { michael@0: // This is a hosted app, let's check if it has an appcache michael@0: // and download it. michael@0: let results = yield this._readManifests([{ id: id }]); michael@0: michael@0: let jsonManifest = results[0].manifest; michael@0: let manifest = new ManifestHelper(jsonManifest, app.origin); michael@0: michael@0: if (manifest.appcache_path) { michael@0: debug("appcache found"); michael@0: this.startOfflineCacheDownload(manifest, app, null, isUpdate); michael@0: } else { michael@0: // Hosted app with no appcache, nothing to do, but we fire a michael@0: // downloaded event. michael@0: debug("No appcache found, sending 'downloaded' for " + aManifestURL); michael@0: app.downloadAvailable = false; michael@0: michael@0: yield this._saveApps(); michael@0: michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifest: jsonManifest, michael@0: manifestURL: aManifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadsuccess", michael@0: manifestURL: aManifestURL michael@0: }); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: let json = yield AppsUtils.loadJSONAsync(file.path); michael@0: if (!json) { michael@0: debug("startDownload: No update manifest found at " + file.path + " " + michael@0: aManifestURL); michael@0: return; michael@0: } michael@0: michael@0: let manifest = new ManifestHelper(json, app.manifestURL); michael@0: let [aId, aManifest] = yield this.downloadPackage(manifest, { michael@0: manifestURL: aManifestURL, michael@0: origin: app.origin, michael@0: installOrigin: app.installOrigin, michael@0: downloadSize: app.downloadSize michael@0: }, isUpdate); michael@0: michael@0: // Success! Keep the zip in of TmpD, we'll move it out when michael@0: // applyDownload() will be called. michael@0: // Save the manifest in TmpD also michael@0: let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", aId, michael@0: "manifest.webapp"); michael@0: yield this._writeFile(manFile, JSON.stringify(aManifest)); michael@0: michael@0: app = this.webapps[aId]; michael@0: // Set state and fire events. michael@0: app.downloading = false; michael@0: app.downloadAvailable = false; michael@0: app.readyToApplyDownload = true; michael@0: app.updateTime = Date.now(); michael@0: michael@0: yield this._saveApps(); michael@0: michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: aManifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadsuccess", michael@0: manifestURL: aManifestURL michael@0: }); michael@0: if (app.installState == "pending") { michael@0: // We restarted a failed download, apply it automatically. michael@0: this.applyDownload(aManifestURL); michael@0: } michael@0: }), michael@0: michael@0: applyDownload: function applyDownload(aManifestURL) { michael@0: debug("applyDownload for " + aManifestURL); michael@0: let id = this._appIdForManifestURL(aManifestURL); michael@0: let app = this.webapps[id]; michael@0: if (!app || (app && !app.readyToApplyDownload)) { michael@0: return; michael@0: } michael@0: michael@0: // We need to get the old manifest to unregister web activities. michael@0: this.getManifestFor(aManifestURL).then((aOldManifest) => { michael@0: // Move the application.zip and manifest.webapp files out of TmpD michael@0: let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true); michael@0: let manFile = tmpDir.clone(); michael@0: manFile.append("manifest.webapp"); michael@0: let appFile = tmpDir.clone(); michael@0: appFile.append("application.zip"); michael@0: michael@0: let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); michael@0: appFile.moveTo(dir, "application.zip"); michael@0: manFile.moveTo(dir, "manifest.webapp"); michael@0: michael@0: // Move the staged update manifest to a non staged one. michael@0: let staged = dir.clone(); michael@0: staged.append("staged-update.webapp"); michael@0: michael@0: // If we are applying after a restarted download, we have no michael@0: // staged update manifest. michael@0: if (staged.exists()) { michael@0: staged.moveTo(dir, "update.webapp"); michael@0: } michael@0: michael@0: try { michael@0: tmpDir.remove(true); michael@0: } catch(e) { } michael@0: michael@0: // Clean up the deprecated manifest cache if needed. michael@0: if (id in this._manifestCache) { michael@0: delete this._manifestCache[id]; michael@0: } michael@0: michael@0: // Flush the zip reader cache to make sure we use the new application.zip michael@0: // when re-launching the application. michael@0: let zipFile = dir.clone(); michael@0: zipFile.append("application.zip"); michael@0: Services.obs.notifyObservers(zipFile, "flush-cache-entry", null); michael@0: michael@0: // Get the manifest, and set properties. michael@0: this.getManifestFor(aManifestURL).then((aData) => { michael@0: app.downloading = false; michael@0: app.downloadAvailable = false; michael@0: app.downloadSize = 0; michael@0: app.installState = "installed"; michael@0: app.readyToApplyDownload = false; michael@0: michael@0: // Update the staged properties. michael@0: if (app.staged) { michael@0: for (let prop in app.staged) { michael@0: app[prop] = app.staged[prop]; michael@0: } michael@0: delete app.staged; michael@0: } michael@0: michael@0: delete app.retryingDownload; michael@0: michael@0: // Update the asm.js scripts we need to compile. michael@0: ScriptPreloader.preload(app, aData) michael@0: .then(() => this._saveApps()).then(() => { michael@0: // Update the handlers and permissions for this app. michael@0: this.updateAppHandlers(aOldManifest, aData, app); michael@0: michael@0: AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => { michael@0: let appObject = AppsUtils.cloneAppObject(app); michael@0: appObject.updateManifest = aUpdateManifest; michael@0: this.notifyUpdateHandlers(appObject, aData, appFile.path); michael@0: }); michael@0: michael@0: if (supportUseCurrentProfile()) { michael@0: PermissionsInstaller.installPermissions( michael@0: { manifest: aData, michael@0: origin: app.origin, michael@0: manifestURL: app.manifestURL }, michael@0: true); michael@0: } michael@0: this.updateDataStore(this.webapps[id].localId, app.origin, michael@0: app.manifestURL, aData, app.appStatus); michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifest: aData, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadapplied", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: }); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) { michael@0: if (!aManifest.appcache_path) { michael@0: return; michael@0: } michael@0: michael@0: // If the manifest has an appcache_path property, use it to populate the michael@0: // appcache. michael@0: let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(), michael@0: null, null); michael@0: let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null); michael@0: michael@0: // We determine the app's 'installState' according to its previous michael@0: // state. Cancelled downloads should remain as 'pending'. Successfully michael@0: // installed apps should morph to 'updating'. michael@0: if (aIsUpdate) { michael@0: aApp.installState = "updating"; michael@0: } michael@0: michael@0: // We set the 'downloading' flag and update the apps registry right before michael@0: // starting the app download/update. michael@0: aApp.downloading = true; michael@0: aApp.progress = 0; michael@0: DOMApplicationRegistry._saveApps().then(() => { michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { michael@0: app: { michael@0: downloading: true, michael@0: installState: aApp.installState, michael@0: progress: 0 michael@0: }, michael@0: manifestURL: aApp.manifestURL michael@0: }); michael@0: let cacheUpdate = updateSvc.scheduleAppUpdate( michael@0: appcacheURI, docURI, aApp.localId, false, aProfileDir); michael@0: michael@0: // We save the download details for potential further usage like michael@0: // cancelling it. michael@0: let download = { michael@0: cacheUpdate: cacheUpdate, michael@0: appId: this._appIdForManifestURL(aApp.manifestURL), michael@0: previousState: aIsUpdate ? "installed" : "pending" michael@0: }; michael@0: AppDownloadManager.add(aApp.manifestURL, download); michael@0: michael@0: cacheUpdate.addObserver(new AppcacheObserver(aApp), false); michael@0: michael@0: }); michael@0: }, michael@0: michael@0: // Returns the MD5 hash of the manifest. michael@0: computeManifestHash: function(aManifest) { michael@0: return AppsUtils.computeHash(JSON.stringify(aManifest)); michael@0: }, michael@0: michael@0: // Updates the redirect mapping, activities and system message handlers. michael@0: // aOldManifest can be null if we don't have any handler to unregister. michael@0: updateAppHandlers: function(aOldManifest, aNewManifest, aApp) { michael@0: debug("updateAppHandlers: old=" + aOldManifest + " new=" + aNewManifest); michael@0: this.notifyAppsRegistryStart(); michael@0: if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { michael@0: aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects); michael@0: } michael@0: michael@0: if (supportSystemMessages()) { michael@0: if (aOldManifest) { michael@0: this._unregisterActivities(aOldManifest, aApp); michael@0: } michael@0: this._registerSystemMessages(aNewManifest, aApp); michael@0: this._registerActivities(aNewManifest, aApp, true); michael@0: this._registerInterAppConnections(aNewManifest, aApp); michael@0: } else { michael@0: // Nothing else to do but notifying we're ready. michael@0: this.notifyAppsRegistryReady(); michael@0: } michael@0: }, michael@0: michael@0: checkForUpdate: function(aData, aMm) { michael@0: debug("checkForUpdate for " + aData.manifestURL); michael@0: michael@0: function sendError(aError) { michael@0: aData.error = aError; michael@0: aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); michael@0: } michael@0: michael@0: let id = this._appIdForManifestURL(aData.manifestURL); michael@0: let app = this.webapps[id]; michael@0: michael@0: // We cannot update an app that does not exists. michael@0: if (!app) { michael@0: sendError("NO_SUCH_APP"); michael@0: return; michael@0: } michael@0: michael@0: // We cannot update an app that is not fully installed. michael@0: if (app.installState !== "installed") { michael@0: sendError("PENDING_APP_NOT_UPDATABLE"); michael@0: return; michael@0: } michael@0: michael@0: // We may be able to remove this when Bug 839071 is fixed. michael@0: if (app.downloading) { michael@0: sendError("APP_IS_DOWNLOADING"); michael@0: return; michael@0: } michael@0: michael@0: // If the app is packaged and its manifestURL has an app:// scheme, michael@0: // then we can't have an update. michael@0: if (app.origin.startsWith("app://") && michael@0: app.manifestURL.startsWith("app://")) { michael@0: aData.error = "NOT_UPDATABLE"; michael@0: aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: // For non-removable hosted apps that lives in the core apps dir we michael@0: // only check the appcache because we can't modify the manifest even michael@0: // if it has changed. michael@0: let onlyCheckAppCache = false; michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); michael@0: onlyCheckAppCache = (app.basePath == appDir.path); michael@0: #endif michael@0: michael@0: if (onlyCheckAppCache) { michael@0: // Bail out for packaged apps. michael@0: if (app.origin.startsWith("app://")) { michael@0: aData.error = "NOT_UPDATABLE"; michael@0: aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: // We need the manifest to check if we have an appcache. michael@0: this._readManifests([{ id: id }]).then((aResult) => { michael@0: let manifest = aResult[0].manifest; michael@0: if (!manifest.appcache_path) { michael@0: aData.error = "NOT_UPDATABLE"; michael@0: aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: debug("Checking only appcache for " + aData.manifestURL); michael@0: // Check if the appcache is updatable, and send "downloadavailable" or michael@0: // "downloadapplied". michael@0: let updateObserver = { michael@0: observe: function(aSubject, aTopic, aObsData) { michael@0: debug("onlyCheckAppCache updateSvc.checkForUpdate return for " + michael@0: app.manifestURL + " - event is " + aTopic); michael@0: if (aTopic == "offline-cache-update-available") { michael@0: app.downloadAvailable = true; michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadavailable", michael@0: manifestURL: app.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: }); michael@0: } else { michael@0: aData.error = "NOT_UPDATABLE"; michael@0: aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); michael@0: } michael@0: } michael@0: }; michael@0: let helper = new ManifestHelper(manifest, aData.manifestURL); michael@0: debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " + michael@0: helper.fullAppcachePath()); michael@0: updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null), michael@0: app.localId, false, updateObserver); michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: // On xhr load request event michael@0: function onload(xhr, oldManifest) { michael@0: debug("Got http status=" + xhr.status + " for " + aData.manifestURL); michael@0: let oldHash = app.manifestHash; michael@0: let isPackage = app.origin.startsWith("app://"); michael@0: michael@0: if (xhr.status == 200) { michael@0: let manifest = xhr.response; michael@0: if (manifest == null) { michael@0: sendError("MANIFEST_PARSE_ERROR"); michael@0: return; michael@0: } michael@0: michael@0: if (!AppsUtils.checkManifest(manifest, app)) { michael@0: sendError("INVALID_MANIFEST"); michael@0: return; michael@0: } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) { michael@0: sendError("INSTALL_FROM_DENIED"); michael@0: return; michael@0: } else { michael@0: AppsUtils.ensureSameAppName(oldManifest, manifest, app); michael@0: michael@0: let hash = this.computeManifestHash(manifest); michael@0: debug("Manifest hash = " + hash); michael@0: if (isPackage) { michael@0: if (!app.staged) { michael@0: app.staged = { }; michael@0: } michael@0: app.staged.manifestHash = hash; michael@0: app.staged.etag = xhr.getResponseHeader("Etag"); michael@0: } else { michael@0: app.manifestHash = hash; michael@0: app.etag = xhr.getResponseHeader("Etag"); michael@0: } michael@0: michael@0: app.lastCheckedUpdate = Date.now(); michael@0: if (isPackage) { michael@0: if (oldHash != hash) { michael@0: this.updatePackagedApp(aData, id, app, manifest); michael@0: } else { michael@0: this._saveApps().then(() => { michael@0: // Like if we got a 304, just send a 'downloadapplied' michael@0: // or downloadavailable event. michael@0: let eventType = app.downloadAvailable ? "downloadavailable" michael@0: : "downloadapplied"; michael@0: aMm.sendAsyncMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: aMm.sendAsyncMessage("Webapps:FireEvent", { michael@0: eventType: eventType, michael@0: manifestURL: app.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: }); michael@0: } michael@0: } else { michael@0: // Update only the appcache if the manifest has not changed michael@0: // based on the hash value. michael@0: this.updateHostedApp(aData, id, app, oldManifest, michael@0: oldHash == hash ? null : manifest); michael@0: } michael@0: } michael@0: } else if (xhr.status == 304) { michael@0: // The manifest has not changed. michael@0: if (isPackage) { michael@0: app.lastCheckedUpdate = Date.now(); michael@0: this._saveApps().then(() => { michael@0: // If the app is a packaged app, we just send a 'downloadapplied' michael@0: // or downloadavailable event. michael@0: let eventType = app.downloadAvailable ? "downloadavailable" michael@0: : "downloadapplied"; michael@0: aMm.sendAsyncMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: aMm.sendAsyncMessage("Webapps:FireEvent", { michael@0: eventType: eventType, michael@0: manifestURL: app.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: }); michael@0: } else { michael@0: // For hosted apps, even if the manifest has not changed, we check michael@0: // for offline cache updates. michael@0: this.updateHostedApp(aData, id, app, oldManifest, null); michael@0: } michael@0: } else { michael@0: sendError("MANIFEST_URL_ERROR"); michael@0: } michael@0: } michael@0: michael@0: // Try to download a new manifest. michael@0: function doRequest(oldManifest, headers) { michael@0: headers = headers || []; michael@0: let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: xhr.open("GET", aData.manifestURL, true); michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: headers.forEach(function(aHeader) { michael@0: debug("Adding header: " + aHeader.name + ": " + aHeader.value); michael@0: xhr.setRequestHeader(aHeader.name, aHeader.value); michael@0: }); michael@0: xhr.responseType = "json"; michael@0: if (app.etag) { michael@0: debug("adding manifest etag:" + app.etag); michael@0: xhr.setRequestHeader("If-None-Match", app.etag); michael@0: } michael@0: xhr.channel.notificationCallbacks = michael@0: this.createLoadContext(app.installerAppId, app.installerIsBrowser); michael@0: michael@0: xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false); michael@0: xhr.addEventListener("error", (function() { michael@0: sendError("NETWORK_ERROR"); michael@0: }).bind(this), false); michael@0: michael@0: debug("Checking manifest at " + aData.manifestURL); michael@0: xhr.send(null); michael@0: } michael@0: michael@0: // Read the current app manifest file michael@0: this._readManifests([{ id: id }]).then((aResult) => { michael@0: let extraHeaders = []; michael@0: #ifdef MOZ_WIDGET_GONK michael@0: let pingManifestURL; michael@0: try { michael@0: pingManifestURL = Services.prefs.getCharPref("ping.manifestURL"); michael@0: } catch(e) { } michael@0: michael@0: if (pingManifestURL && pingManifestURL == aData.manifestURL) { michael@0: // Get the device info. michael@0: let device = libcutils.property_get("ro.product.model"); michael@0: extraHeaders.push({ name: "X-MOZ-B2G-DEVICE", michael@0: value: device || "unknown" }); michael@0: } michael@0: #endif michael@0: doRequest.call(this, aResult[0].manifest, extraHeaders); michael@0: }); michael@0: }, michael@0: michael@0: // Creates a nsILoadContext object with a given appId and isBrowser flag. michael@0: createLoadContext: function createLoadContext(aAppId, aIsBrowser) { michael@0: return { michael@0: associatedWindow: null, michael@0: topWindow : null, michael@0: appId: aAppId, michael@0: isInBrowserElement: aIsBrowser, michael@0: usePrivateBrowsing: false, michael@0: isContent: false, michael@0: michael@0: isAppOfType: function(appType) { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext, michael@0: Ci.nsIInterfaceRequestor, michael@0: Ci.nsISupports]), michael@0: getInterface: function(iid) { michael@0: if (iid.equals(Ci.nsILoadContext)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) { michael@0: debug("updatePackagedApp"); michael@0: michael@0: // Store the new update manifest. michael@0: let dir = this._getAppDir(aId).path; michael@0: let manFile = OS.Path.join(dir, "staged-update.webapp"); michael@0: yield this._writeFile(manFile, JSON.stringify(aNewManifest)); michael@0: michael@0: let manifest = new ManifestHelper(aNewManifest, aApp.manifestURL); michael@0: // A package is available: set downloadAvailable to fire the matching michael@0: // event. michael@0: aApp.downloadAvailable = true; michael@0: aApp.downloadSize = manifest.size; michael@0: aApp.updateManifest = aNewManifest; michael@0: yield this._saveApps(); michael@0: michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: aApp, michael@0: manifestURL: aApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadavailable", michael@0: manifestURL: aApp.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: }), michael@0: michael@0: // A hosted app is updated if the app manifest or the appcache needs michael@0: // updating. Even if the app manifest has not changed, we still check michael@0: // for changes in the app cache. michael@0: // 'aNewManifest' would contain the updated app manifest if michael@0: // it has actually been updated, while 'aOldManifest' contains the michael@0: // stored app manifest. michael@0: updateHostedApp: Task.async(function*(aData, aId, aApp, aOldManifest, aNewManifest) { michael@0: debug("updateHostedApp " + aData.manifestURL); michael@0: michael@0: // Clean up the deprecated manifest cache if needed. michael@0: if (aId in this._manifestCache) { michael@0: delete this._manifestCache[aId]; michael@0: } michael@0: michael@0: aApp.manifest = aNewManifest || aOldManifest; michael@0: michael@0: let manifest; michael@0: if (aNewManifest) { michael@0: this.updateAppHandlers(aOldManifest, aNewManifest, aApp); michael@0: michael@0: this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest); michael@0: michael@0: // Store the new manifest. michael@0: let dir = this._getAppDir(aId).path; michael@0: let manFile = OS.Path.join(dir, "manifest.webapp"); michael@0: yield this._writeFile(manFile, JSON.stringify(aNewManifest)); michael@0: michael@0: manifest = new ManifestHelper(aNewManifest, aApp.origin); michael@0: michael@0: if (supportUseCurrentProfile()) { michael@0: // Update the permissions for this app. michael@0: PermissionsInstaller.installPermissions({ michael@0: manifest: aApp.manifest, michael@0: origin: aApp.origin, michael@0: manifestURL: aData.manifestURL michael@0: }, true); michael@0: } michael@0: michael@0: this.updateDataStore(this.webapps[aId].localId, aApp.origin, michael@0: aApp.manifestURL, aApp.manifest, aApp.appStatus); michael@0: michael@0: aApp.name = manifest.name; michael@0: aApp.csp = manifest.csp || ""; michael@0: aApp.role = manifest.role || ""; michael@0: aApp.updateTime = Date.now(); michael@0: } else { michael@0: manifest = new ManifestHelper(aOldManifest, aApp.origin); michael@0: } michael@0: michael@0: // Update the registry. michael@0: this.webapps[aId] = aApp; michael@0: yield this._saveApps(); michael@0: michael@0: if (!manifest.appcache_path) { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: aApp, michael@0: manifest: aApp.manifest, michael@0: manifestURL: aApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloadapplied", michael@0: manifestURL: aApp.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: } else { michael@0: // Check if the appcache is updatable, and send "downloadavailable" or michael@0: // "downloadapplied". michael@0: debug("updateHostedApp: updateSvc.checkForUpdate for " + michael@0: manifest.fullAppcachePath()); michael@0: michael@0: let updateDeferred = Promise.defer(); michael@0: michael@0: updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null), michael@0: aApp.localId, false, michael@0: (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic)); michael@0: michael@0: let topic = yield updateDeferred.promise; michael@0: michael@0: debug("updateHostedApp: updateSvc.checkForUpdate return for " + michael@0: aApp.manifestURL + " - event is " + topic); michael@0: michael@0: let eventType = michael@0: topic == "offline-cache-update-available" ? "downloadavailable" michael@0: : "downloadapplied"; michael@0: michael@0: aApp.downloadAvailable = (eventType == "downloadavailable"); michael@0: yield this._saveApps(); michael@0: michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: aApp, michael@0: manifest: aApp.manifest, michael@0: manifestURL: aApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: eventType, michael@0: manifestURL: aApp.manifestURL, michael@0: requestID: aData.requestID michael@0: }); michael@0: } michael@0: michael@0: delete aApp.manifest; michael@0: }), michael@0: michael@0: // Downloads the manifest and run checks, then eventually triggers the michael@0: // installation UI. michael@0: doInstall: function doInstall(aData, aMm) { michael@0: let app = aData.app; michael@0: michael@0: let sendError = function sendError(aError) { michael@0: aData.error = aError; michael@0: aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData); michael@0: Cu.reportError("Error installing app from: " + app.installOrigin + michael@0: ": " + aError); michael@0: }.bind(this); michael@0: michael@0: if (app.receipts.length > 0) { michael@0: for (let receipt of app.receipts) { michael@0: let error = this.isReceipt(receipt); michael@0: if (error) { michael@0: sendError(error); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Hosted apps can't be trusted or certified, so just check that the michael@0: // manifest doesn't ask for those. michael@0: function checkAppStatus(aManifest) { michael@0: let manifestStatus = aManifest.type || "web"; michael@0: return manifestStatus === "web"; michael@0: } michael@0: michael@0: let checkManifest = (function() { michael@0: if (!app.manifest) { michael@0: sendError("MANIFEST_PARSE_ERROR"); michael@0: return false; michael@0: } michael@0: michael@0: // Disallow multiple hosted apps installations from the same origin for now. michael@0: // We will remove this code after multiple apps per origin are supported (bug 778277). michael@0: // This will also disallow reinstalls from the same origin for now. michael@0: for (let id in this.webapps) { michael@0: if (this.webapps[id].origin == app.origin && michael@0: !this.webapps[id].packageHash && michael@0: this._isLaunchable(this.webapps[id])) { michael@0: sendError("MULTIPLE_APPS_PER_ORIGIN_FORBIDDEN"); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if (!AppsUtils.checkManifest(app.manifest, app)) { michael@0: sendError("INVALID_MANIFEST"); michael@0: return false; michael@0: } michael@0: michael@0: if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) { michael@0: sendError("INSTALL_FROM_DENIED"); michael@0: return false; michael@0: } michael@0: michael@0: if (!checkAppStatus(app.manifest)) { michael@0: sendError("INVALID_SECURITY_LEVEL"); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }).bind(this); michael@0: michael@0: let installApp = (function() { michael@0: app.manifestHash = this.computeManifestHash(app.manifest); michael@0: // We allow bypassing the install confirmation process to facilitate michael@0: // automation. michael@0: let prefName = "dom.mozApps.auto_confirm_install"; michael@0: if (Services.prefs.prefHasUserValue(prefName) && michael@0: Services.prefs.getBoolPref(prefName)) { michael@0: this.confirmInstall(aData); michael@0: } else { michael@0: Services.obs.notifyObservers(aMm, "webapps-ask-install", michael@0: JSON.stringify(aData)); michael@0: } michael@0: }).bind(this); michael@0: michael@0: // We may already have the manifest (e.g. AutoInstall), michael@0: // in which case we don't need to load it. michael@0: if (app.manifest) { michael@0: if (checkManifest()) { michael@0: installApp(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: xhr.open("GET", app.manifestURL, true); michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, michael@0: aData.isBrowser); michael@0: xhr.responseType = "json"; michael@0: michael@0: xhr.addEventListener("load", (function() { michael@0: if (xhr.status == 200) { michael@0: if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin, michael@0: xhr.getResponseHeader("content-type"))) { michael@0: sendError("INVALID_MANIFEST"); michael@0: return; michael@0: } michael@0: michael@0: app.manifest = xhr.response; michael@0: if (checkManifest()) { michael@0: app.etag = xhr.getResponseHeader("Etag"); michael@0: installApp(); michael@0: } michael@0: } else { michael@0: sendError("MANIFEST_URL_ERROR"); michael@0: } michael@0: }).bind(this), false); michael@0: michael@0: xhr.addEventListener("error", (function() { michael@0: sendError("NETWORK_ERROR"); michael@0: }).bind(this), false); michael@0: michael@0: xhr.send(null); michael@0: }, michael@0: michael@0: doInstallPackage: function doInstallPackage(aData, aMm) { michael@0: let app = aData.app; michael@0: michael@0: let sendError = function sendError(aError) { michael@0: aData.error = aError; michael@0: aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData); michael@0: Cu.reportError("Error installing packaged app from: " + michael@0: app.installOrigin + ": " + aError); michael@0: }.bind(this); michael@0: michael@0: if (app.receipts.length > 0) { michael@0: for (let receipt of app.receipts) { michael@0: let error = this.isReceipt(receipt); michael@0: if (error) { michael@0: sendError(error); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let checkUpdateManifest = (function() { michael@0: let manifest = app.updateManifest; michael@0: michael@0: // Disallow reinstalls from the same manifest URL for now. michael@0: let id = this._appIdForManifestURL(app.manifestURL); michael@0: if (id !== null && this._isLaunchable(this.webapps[id])) { michael@0: sendError("REINSTALL_FORBIDDEN"); michael@0: return false; michael@0: } michael@0: michael@0: if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) { michael@0: sendError("INVALID_MANIFEST"); michael@0: return false; michael@0: } michael@0: michael@0: if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) { michael@0: sendError("INSTALL_FROM_DENIED"); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }).bind(this); michael@0: michael@0: let installApp = (function() { michael@0: app.manifestHash = this.computeManifestHash(app.updateManifest); michael@0: michael@0: // We allow bypassing the install confirmation process to facilitate michael@0: // automation. michael@0: let prefName = "dom.mozApps.auto_confirm_install"; michael@0: if (Services.prefs.prefHasUserValue(prefName) && michael@0: Services.prefs.getBoolPref(prefName)) { michael@0: this.confirmInstall(aData); michael@0: } else { michael@0: Services.obs.notifyObservers(aMm, "webapps-ask-install", michael@0: JSON.stringify(aData)); michael@0: } michael@0: }).bind(this); michael@0: michael@0: // We may already have the manifest (e.g. AutoInstall), michael@0: // in which case we don't need to load it. michael@0: if (app.updateManifest) { michael@0: if (checkUpdateManifest()) { michael@0: installApp(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: xhr.open("GET", app.manifestURL, true); michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, michael@0: aData.isBrowser); michael@0: xhr.responseType = "json"; michael@0: michael@0: xhr.addEventListener("load", (function() { michael@0: if (xhr.status == 200) { michael@0: if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin, michael@0: xhr.getResponseHeader("content-type"))) { michael@0: sendError("INVALID_MANIFEST"); michael@0: return; michael@0: } michael@0: michael@0: app.updateManifest = xhr.response; michael@0: if (!app.updateManifest) { michael@0: sendError("MANIFEST_PARSE_ERROR"); michael@0: return; michael@0: } michael@0: if (checkUpdateManifest()) { michael@0: app.etag = xhr.getResponseHeader("Etag"); michael@0: debug("at install package got app etag=" + app.etag); michael@0: installApp(); michael@0: } michael@0: } michael@0: else { michael@0: sendError("MANIFEST_URL_ERROR"); michael@0: } michael@0: }).bind(this), false); michael@0: michael@0: xhr.addEventListener("error", (function() { michael@0: sendError("NETWORK_ERROR"); michael@0: }).bind(this), false); michael@0: michael@0: xhr.send(null); michael@0: }, michael@0: michael@0: denyInstall: function(aData) { michael@0: let packageId = aData.app.packageId; michael@0: if (packageId) { michael@0: let dir = FileUtils.getDir("TmpD", ["webapps", packageId], michael@0: true, true); michael@0: try { michael@0: dir.remove(true); michael@0: } catch(e) { michael@0: } michael@0: } michael@0: aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData); michael@0: }, michael@0: michael@0: // This function is called after we called the onsuccess callback on the michael@0: // content side. This let the webpage the opportunity to set event handlers michael@0: // on the app before we start firing progress events. michael@0: queuedDownload: {}, michael@0: queuedPackageDownload: {}, michael@0: michael@0: onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, michael@0: aDontNeedNetwork) { michael@0: // If we are offline, register to run when we'll be online. michael@0: if ((Services.io.offline) && !aDontNeedNetwork) { michael@0: let onlineWrapper = { michael@0: observe: function(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(onlineWrapper, michael@0: "network:offline-status-changed"); michael@0: DOMApplicationRegistry.onInstallSuccessAck(aManifestURL); michael@0: } michael@0: }; michael@0: Services.obs.addObserver(onlineWrapper, michael@0: "network:offline-status-changed", false); michael@0: return; michael@0: } michael@0: michael@0: let cacheDownload = this.queuedDownload[aManifestURL]; michael@0: if (cacheDownload) { michael@0: this.startOfflineCacheDownload(cacheDownload.manifest, michael@0: cacheDownload.app, michael@0: cacheDownload.profileDir); michael@0: delete this.queuedDownload[aManifestURL]; michael@0: michael@0: return; michael@0: } michael@0: michael@0: let packageDownload = this.queuedPackageDownload[aManifestURL]; michael@0: if (packageDownload) { michael@0: let manifest = packageDownload.manifest; michael@0: let newApp = packageDownload.app; michael@0: let installSuccessCallback = packageDownload.callback; michael@0: michael@0: delete this.queuedPackageDownload[aManifestURL]; michael@0: michael@0: this.downloadPackage(manifest, newApp, false).then( michael@0: this._onDownloadPackage.bind(this, newApp, installSuccessCallback) michael@0: ); michael@0: } michael@0: }, michael@0: michael@0: _setupApp: function(aData, aId) { michael@0: let app = aData.app; michael@0: michael@0: // app can be uninstalled michael@0: app.removable = true; michael@0: michael@0: if (aData.isPackage) { michael@0: // Override the origin with the correct id. michael@0: app.origin = "app://" + aId; michael@0: } michael@0: michael@0: app.id = aId; michael@0: app.installTime = Date.now(); michael@0: app.lastUpdateCheck = Date.now(); michael@0: michael@0: return app; michael@0: }, michael@0: michael@0: _cloneApp: function(aData, aNewApp, aManifest, aId, aLocalId) { michael@0: let appObject = AppsUtils.cloneAppObject(aNewApp); michael@0: appObject.appStatus = michael@0: aNewApp.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED; michael@0: michael@0: if (aManifest.appcache_path) { michael@0: appObject.installState = "pending"; michael@0: appObject.downloadAvailable = true; michael@0: appObject.downloading = true; michael@0: appObject.downloadSize = 0; michael@0: appObject.readyToApplyDownload = false; michael@0: } else if (aManifest.package_path) { michael@0: appObject.installState = "pending"; michael@0: appObject.downloadAvailable = true; michael@0: appObject.downloading = true; michael@0: appObject.downloadSize = aManifest.size; michael@0: appObject.readyToApplyDownload = false; michael@0: } else { michael@0: appObject.installState = "installed"; michael@0: appObject.downloadAvailable = false; michael@0: appObject.downloading = false; michael@0: appObject.readyToApplyDownload = false; michael@0: } michael@0: michael@0: appObject.localId = aLocalId; michael@0: appObject.basePath = OS.Path.dirname(this.appsFile); michael@0: appObject.name = aManifest.name; michael@0: appObject.csp = aManifest.csp || ""; michael@0: appObject.role = aManifest.role || ""; michael@0: appObject.installerAppId = aData.appId; michael@0: appObject.installerIsBrowser = aData.isBrowser; michael@0: michael@0: return appObject; michael@0: }, michael@0: michael@0: _writeManifestFile: function(aId, aIsPackage, aJsonManifest) { michael@0: debug("_writeManifestFile"); michael@0: michael@0: // For packaged apps, keep the update manifest distinct from the app manifest. michael@0: let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp"; michael@0: michael@0: let dir = this._getAppDir(aId).path; michael@0: let manFile = OS.Path.join(dir, manifestName); michael@0: this._writeFile(manFile, JSON.stringify(aJsonManifest)); michael@0: }, michael@0: michael@0: // Add an app that is already installed to the registry. michael@0: addInstalledApp: Task.async(function*(aApp, aManifest, aUpdateManifest) { michael@0: if (this.getAppLocalIdByManifestURL(aApp.manifestURL) != michael@0: Ci.nsIScriptSecurityManager.NO_APP_ID) { michael@0: return; michael@0: } michael@0: michael@0: let app = AppsUtils.cloneAppObject(aApp); michael@0: michael@0: if (!AppsUtils.checkManifest(aManifest, app) || michael@0: (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) { michael@0: return; michael@0: } michael@0: michael@0: app.name = aManifest.name; michael@0: michael@0: app.csp = aManifest.csp || ""; michael@0: michael@0: app.appStatus = AppsUtils.getAppManifestStatus(aManifest); michael@0: michael@0: app.removable = true; michael@0: michael@0: // Reuse the app ID if the scheme is "app". michael@0: let uri = Services.io.newURI(app.origin, null, null); michael@0: if (uri.scheme == "app") { michael@0: app.id = uri.host; michael@0: } else { michael@0: app.id = this.makeAppId(); michael@0: } michael@0: michael@0: app.localId = this._nextLocalId(); michael@0: michael@0: app.basePath = OS.Path.dirname(this.appsFile); michael@0: michael@0: app.progress = 0.0; michael@0: app.installState = "installed"; michael@0: app.downloadAvailable = false; michael@0: app.downloading = false; michael@0: app.readyToApplyDownload = false; michael@0: michael@0: if (aUpdateManifest && aUpdateManifest.size) { michael@0: app.downloadSize = aUpdateManifest.size; michael@0: } michael@0: michael@0: app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest || michael@0: aManifest)); michael@0: michael@0: let zipFile = WebappOSUtils.getPackagePath(app); michael@0: app.packageHash = yield this._computeFileHash(zipFile); michael@0: michael@0: app.role = aManifest.role || ""; michael@0: michael@0: app.redirects = this.sanitizeRedirects(aManifest.redirects); michael@0: michael@0: this.webapps[app.id] = app; michael@0: michael@0: // Store the manifest in the manifest cache, so we don't need to re-read it michael@0: this._manifestCache[app.id] = app.manifest; michael@0: michael@0: // Store the manifest and the updateManifest. michael@0: this._writeManifestFile(app.id, false, aManifest); michael@0: if (aUpdateManifest) { michael@0: this._writeManifestFile(app.id, true, aUpdateManifest); michael@0: } michael@0: michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app }); michael@0: }); michael@0: }), michael@0: michael@0: confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) { michael@0: debug("confirmInstall"); michael@0: michael@0: let origin = Services.io.newURI(aData.app.origin, null, null); michael@0: let id = this._appIdForManifestURL(aData.app.manifestURL); michael@0: let manifestURL = origin.resolve(aData.app.manifestURL); michael@0: let localId = this.getAppLocalIdByManifestURL(manifestURL); michael@0: michael@0: let isReinstall = false; michael@0: michael@0: // Installing an application again is considered as an update. michael@0: if (id) { michael@0: isReinstall = true; michael@0: let dir = this._getAppDir(id); michael@0: try { michael@0: dir.remove(true); michael@0: } catch(e) { } michael@0: } else { michael@0: id = this.makeAppId(); michael@0: localId = this._nextLocalId(); michael@0: } michael@0: michael@0: let app = this._setupApp(aData, id); michael@0: michael@0: let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest; michael@0: this._writeManifestFile(id, aData.isPackage, jsonManifest); michael@0: michael@0: debug("app.origin: " + app.origin); michael@0: let manifest = new ManifestHelper(jsonManifest, app.origin); michael@0: michael@0: let appObject = this._cloneApp(aData, app, manifest, id, localId); michael@0: michael@0: this.webapps[id] = appObject; michael@0: michael@0: // For package apps, the permissions are not in the mini-manifest, so michael@0: // don't update the permissions yet. michael@0: if (!aData.isPackage) { michael@0: if (supportUseCurrentProfile()) { michael@0: PermissionsInstaller.installPermissions( michael@0: { michael@0: origin: appObject.origin, michael@0: manifestURL: appObject.manifestURL, michael@0: manifest: jsonManifest michael@0: }, michael@0: isReinstall, michael@0: this.uninstall.bind(this, aData, aData.mm) michael@0: ); michael@0: } michael@0: michael@0: this.updateDataStore(this.webapps[id].localId, this.webapps[id].origin, michael@0: this.webapps[id].manifestURL, jsonManifest, michael@0: this.webapps[id].appStatus); michael@0: } michael@0: michael@0: for each (let prop in ["installState", "downloadAvailable", "downloading", michael@0: "downloadSize", "readyToApplyDownload"]) { michael@0: aData.app[prop] = appObject[prop]; michael@0: } michael@0: michael@0: if (manifest.appcache_path) { michael@0: this.queuedDownload[app.manifestURL] = { michael@0: manifest: manifest, michael@0: app: appObject, michael@0: profileDir: aProfileDir michael@0: } michael@0: } michael@0: michael@0: // We notify about the successful installation via mgmt.oninstall and the michael@0: // corresponging DOMRequest.onsuccess event as soon as the app is properly michael@0: // saved in the registry. michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject }); michael@0: if (aData.isPackage && aData.apkInstall && !aData.requestID) { michael@0: // Skip directly to onInstallSuccessAck, since there isn't michael@0: // a WebappsRegistry to receive Webapps:Install:Return:OK and respond michael@0: // Webapps:Install:Return:Ack when an app is being auto-installed. michael@0: this.onInstallSuccessAck(app.manifestURL); michael@0: } else { michael@0: // Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify michael@0: // the installing page about the successful install, after which it'll michael@0: // respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck. michael@0: this.broadcastMessage("Webapps:Install:Return:OK", aData); michael@0: } michael@0: if (!aData.isPackage) { michael@0: this.updateAppHandlers(null, app.manifest, app); michael@0: if (aInstallSuccessCallback) { michael@0: aInstallSuccessCallback(app.manifest); michael@0: } michael@0: } michael@0: Services.obs.notifyObservers(null, "webapps-installed", michael@0: JSON.stringify({ manifestURL: app.manifestURL })); michael@0: }); michael@0: michael@0: let dontNeedNetwork = false; michael@0: if (manifest.package_path) { michael@0: // If it is a local app then it must been installed from a local file michael@0: // instead of web. michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: // In that case, we would already have the manifest, not just the update michael@0: // manifest. michael@0: dontNeedNetwork = !!aData.app.manifest; michael@0: #else michael@0: if (aData.app.localInstallPath) { michael@0: dontNeedNetwork = true; michael@0: jsonManifest.package_path = "file://" + aData.app.localInstallPath; michael@0: } michael@0: #endif michael@0: michael@0: // origin for install apps is meaningless here, since it's app:// and this michael@0: // can't be used to resolve package paths. michael@0: manifest = new ManifestHelper(jsonManifest, app.manifestURL); michael@0: michael@0: this.queuedPackageDownload[app.manifestURL] = { michael@0: manifest: manifest, michael@0: app: appObject, michael@0: callback: aInstallSuccessCallback michael@0: }; michael@0: } michael@0: michael@0: if (aData.forceSuccessAck) { michael@0: // If it's a local install, there's no content process so just michael@0: // ack the install. michael@0: this.onInstallSuccessAck(app.manifestURL, dontNeedNetwork); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Install the package after successfully downloading it michael@0: * michael@0: * Bound params: michael@0: * michael@0: * @param aNewApp {Object} the new app data michael@0: * @param aInstallSuccessCallback {Function} michael@0: * the callback to call on install success michael@0: * michael@0: * Passed params: michael@0: * michael@0: * @param aId {Integer} the unique ID of the application michael@0: * @param aManifest {Object} The manifest of the application michael@0: */ michael@0: _onDownloadPackage: Task.async(function*(aNewApp, aInstallSuccessCallback, michael@0: [aId, aManifest]) { michael@0: debug("_onDownloadPackage"); michael@0: // Success! Move the zip out of TmpD. michael@0: let app = this.webapps[aId]; michael@0: let zipFile = michael@0: FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true); michael@0: let dir = this._getAppDir(aId); michael@0: zipFile.moveTo(dir, "application.zip"); michael@0: let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true); michael@0: try { michael@0: tmpDir.remove(true); michael@0: } catch(e) { } michael@0: michael@0: // Save the manifest michael@0: let manFile = OS.Path.join(dir.path, "manifest.webapp"); michael@0: yield this._writeFile(manFile, JSON.stringify(aManifest)); michael@0: // Set state and fire events. michael@0: app.installState = "installed"; michael@0: app.downloading = false; michael@0: app.downloadAvailable = false; michael@0: michael@0: yield this._saveApps(); michael@0: michael@0: this.updateAppHandlers(null, aManifest, aNewApp); michael@0: // Clear the manifest cache in case it holds the update manifest. michael@0: if (aId in this._manifestCache) { michael@0: delete this._manifestCache[aId]; michael@0: } michael@0: michael@0: this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp }); michael@0: Services.obs.notifyObservers(null, "webapps-installed", michael@0: JSON.stringify({ manifestURL: aNewApp.manifestURL })); michael@0: michael@0: if (supportUseCurrentProfile()) { michael@0: // Update the permissions for this app. michael@0: PermissionsInstaller.installPermissions({ michael@0: manifest: aManifest, michael@0: origin: aNewApp.origin, michael@0: manifestURL: aNewApp.manifestURL michael@0: }, true); michael@0: } michael@0: michael@0: this.updateDataStore(this.webapps[aId].localId, aNewApp.origin, michael@0: aNewApp.manifestURL, aManifest, aNewApp.appStatus); michael@0: michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifest: aManifest, michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: michael@0: // Check if we have asm.js code to preload for this application. michael@0: yield ScriptPreloader.preload(aNewApp, aManifest); michael@0: michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: ["downloadsuccess", "downloadapplied"], michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: michael@0: if (aInstallSuccessCallback) { michael@0: aInstallSuccessCallback(aManifest, zipFile.path); michael@0: } michael@0: }), michael@0: michael@0: _nextLocalId: function() { michael@0: let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1; michael@0: michael@0: while (this.getManifestURLByLocalId(id)) { michael@0: id++; michael@0: } michael@0: michael@0: Services.prefs.setIntPref("dom.mozApps.maxLocalId", id); michael@0: Services.prefs.savePrefFile(null); michael@0: return id; michael@0: }, michael@0: michael@0: _appIdForManifestURL: function(aURI) { michael@0: for (let id in this.webapps) { michael@0: if (this.webapps[id].manifestURL == aURI) michael@0: return id; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: makeAppId: function() { michael@0: let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); michael@0: return uuidGenerator.generateUUID().toString(); michael@0: }, michael@0: michael@0: _saveApps: function() { michael@0: return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2)); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously reads a list of manifests michael@0: */ michael@0: michael@0: _manifestCache: {}, michael@0: michael@0: _readManifests: function(aData) { michael@0: return Task.spawn(function*() { michael@0: for (let elem of aData) { michael@0: let id = elem.id; michael@0: michael@0: if (!this._manifestCache[id]) { michael@0: // the manifest file used to be named manifest.json, so fallback on this. michael@0: let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath() michael@0: ? "coreAppsDir" : DIRECTORY_NAME; michael@0: michael@0: let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true); michael@0: michael@0: let fileNames = ["manifest.webapp", "update.webapp", "manifest.json"]; michael@0: for (let fileName of fileNames) { michael@0: this._manifestCache[id] = yield AppsUtils.loadJSONAsync(OS.Path.join(dir.path, fileName)); michael@0: if (this._manifestCache[id]) { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: elem.manifest = this._manifestCache[id]; michael@0: } michael@0: michael@0: return aData; michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: downloadPackage: function(aManifest, aNewApp, aIsUpdate, aOnSuccess) { michael@0: // Here are the steps when installing a package: michael@0: // - create a temp directory where to store the app. michael@0: // - download the zip in this directory. michael@0: // - check the signature on the zip. michael@0: // - extract the manifest from the zip and check it. michael@0: // - ask confirmation to the user. michael@0: // - add the new app to the registry. michael@0: // If we fail at any step, we revert the previous ones and return an error. michael@0: michael@0: // We define these outside the task to use them in its reject handler. michael@0: let id = this._appIdForManifestURL(aNewApp.manifestURL); michael@0: let oldApp = this.webapps[id]; michael@0: michael@0: return Task.spawn((function*() { michael@0: yield this._ensureSufficientStorage(aNewApp); michael@0: michael@0: let fullPackagePath = aManifest.fullPackagePath(); michael@0: michael@0: // Check if it's a local file install (we've downloaded/sideloaded the michael@0: // package already, it existed on the build, or it came with an APK). michael@0: // Note that this variable also controls whether files signed with expired michael@0: // certificates are accepted or not. If isLocalFileInstall is true and the michael@0: // device date is earlier than the build generation date, then the signature michael@0: // will be accepted even if the certificate is expired. michael@0: let isLocalFileInstall = michael@0: Services.io.extractScheme(fullPackagePath) === 'file'; michael@0: michael@0: debug("About to download " + fullPackagePath); michael@0: michael@0: let requestChannel = this._getRequestChannel(fullPackagePath, michael@0: isLocalFileInstall, michael@0: oldApp, michael@0: aNewApp); michael@0: michael@0: AppDownloadManager.add( michael@0: aNewApp.manifestURL, michael@0: { michael@0: channel: requestChannel, michael@0: appId: id, michael@0: previousState: aIsUpdate ? "installed" : "pending" michael@0: } michael@0: ); michael@0: michael@0: // We set the 'downloading' flag to true right before starting the fetch. michael@0: oldApp.downloading = true; michael@0: michael@0: // We determine the app's 'installState' according to its previous michael@0: // state. Cancelled download should remain as 'pending'. Successfully michael@0: // installed apps should morph to 'updating'. michael@0: oldApp.installState = aIsUpdate ? "updating" : "pending"; michael@0: michael@0: // initialize the progress to 0 right now michael@0: oldApp.progress = 0; michael@0: michael@0: let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp); michael@0: let hash = yield this._computeFileHash(zipFile.path); michael@0: michael@0: let responseStatus = requestChannel.responseStatus; michael@0: let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash); michael@0: michael@0: if (oldPackage) { michael@0: debug("package's etag or hash unchanged; sending 'applied' event"); michael@0: // The package's Etag or hash has not changed. michael@0: // We send a "applied" event right away. michael@0: this._sendAppliedEvent(aNewApp, oldApp, id); michael@0: return; michael@0: } michael@0: michael@0: let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp, michael@0: isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash); michael@0: michael@0: AppDownloadManager.remove(aNewApp.manifestURL); michael@0: michael@0: return [oldApp.id, newManifest]; michael@0: michael@0: }).bind(this)).then( michael@0: aOnSuccess, michael@0: this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate) michael@0: ); michael@0: }, michael@0: michael@0: _ensureSufficientStorage: function(aNewApp) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let navigator = Services.wm.getMostRecentWindow(chromeWindowType) michael@0: .navigator; michael@0: let deviceStorage = null; michael@0: michael@0: if (navigator.getDeviceStorage) { michael@0: deviceStorage = navigator.getDeviceStorage("apps"); michael@0: } michael@0: michael@0: if (deviceStorage) { michael@0: let req = deviceStorage.freeSpace(); michael@0: req.onsuccess = req.onerror = e => { michael@0: let freeBytes = e.target.result; michael@0: let sufficientStorage = this._checkDownloadSize(freeBytes, aNewApp); michael@0: if (sufficientStorage) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject("INSUFFICIENT_STORAGE"); michael@0: } michael@0: } michael@0: } else { michael@0: debug("No deviceStorage"); michael@0: // deviceStorage isn't available, so use FileUtils to find the size of michael@0: // available storage. michael@0: let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true); michael@0: try { michael@0: let sufficientStorage = this._checkDownloadSize(dir.diskSpaceAvailable, michael@0: aNewApp); michael@0: if (sufficientStorage) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject("INSUFFICIENT_STORAGE"); michael@0: } michael@0: } catch(ex) { michael@0: // If disk space information isn't available, we'll end up here. michael@0: // We should proceed anyway, otherwise devices that support neither michael@0: // deviceStorage nor diskSpaceAvailable will never be able to install michael@0: // packaged apps. michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _checkDownloadSize: function(aFreeBytes, aNewApp) { michael@0: if (aFreeBytes) { michael@0: debug("Free storage: " + aFreeBytes + ". Download size: " + michael@0: aNewApp.downloadSize); michael@0: if (aFreeBytes <= michael@0: aNewApp.downloadSize + AppDownloadManager.MIN_REMAINING_FREESPACE) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: _getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp, michael@0: aNewApp) { michael@0: let requestChannel; michael@0: michael@0: if (aIsLocalFileInstall) { michael@0: requestChannel = NetUtil.newChannel(aFullPackagePath) michael@0: .QueryInterface(Ci.nsIFileChannel); michael@0: } else { michael@0: requestChannel = NetUtil.newChannel(aFullPackagePath) michael@0: .QueryInterface(Ci.nsIHttpChannel); michael@0: requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: } michael@0: michael@0: if (aOldApp.packageEtag && !aIsLocalFileInstall) { michael@0: debug("Add If-None-Match header: " + aOldApp.packageEtag); michael@0: requestChannel.setRequestHeader("If-None-Match", aOldApp.packageEtag, michael@0: false); michael@0: } michael@0: michael@0: let lastProgressTime = 0; michael@0: michael@0: requestChannel.notificationCallbacks = { michael@0: QueryInterface: function(aIID) { michael@0: if (aIID.equals(Ci.nsISupports) || michael@0: aIID.equals(Ci.nsIProgressEventSink) || michael@0: aIID.equals(Ci.nsILoadContext)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: getInterface: function(aIID) { michael@0: return this.QueryInterface(aIID); michael@0: }, michael@0: onProgress: (function(aRequest, aContext, aProgress, aProgressMax) { michael@0: aOldApp.progress = aProgress; michael@0: let now = Date.now(); michael@0: if (now - lastProgressTime > MIN_PROGRESS_EVENT_DELAY) { michael@0: debug("onProgress: " + aProgress + "/" + aProgressMax); michael@0: this._sendDownloadProgressEvent(aNewApp, aProgress); michael@0: lastProgressTime = now; michael@0: this._saveApps(); michael@0: } michael@0: }).bind(this), michael@0: onStatus: function(aRequest, aContext, aStatus, aStatusArg) { }, michael@0: michael@0: // nsILoadContext michael@0: appId: aOldApp.installerAppId, michael@0: isInBrowserElement: aOldApp.installerIsBrowser, michael@0: usePrivateBrowsing: false, michael@0: isContent: false, michael@0: associatedWindow: null, michael@0: topWindow : null, michael@0: isAppOfType: function(appType) { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: }; michael@0: michael@0: return requestChannel; michael@0: }, michael@0: michael@0: _sendDownloadProgressEvent: function(aNewApp, aProgress) { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: { michael@0: progress: aProgress michael@0: }, michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "progress", michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: }, michael@0: michael@0: _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: // Staging the zip in TmpD until all the checks are done. michael@0: let zipFile = michael@0: FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true); michael@0: michael@0: // We need an output stream to write the channel content to the zip file. michael@0: let outputStream = Cc["@mozilla.org/network/file-output-stream;1"] michael@0: .createInstance(Ci.nsIFileOutputStream); michael@0: // write, create, truncate michael@0: outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0); michael@0: let bufferedOutputStream = michael@0: Cc['@mozilla.org/network/buffered-output-stream;1'] michael@0: .createInstance(Ci.nsIBufferedOutputStream); michael@0: bufferedOutputStream.init(outputStream, 1024); michael@0: michael@0: // Create a listener that will give data to the file output stream. michael@0: let listener = Cc["@mozilla.org/network/simple-stream-listener;1"] michael@0: .createInstance(Ci.nsISimpleStreamListener); michael@0: michael@0: listener.init(bufferedOutputStream, { michael@0: onStartRequest: function(aRequest, aContext) { michael@0: // Nothing to do there anymore. michael@0: }, michael@0: michael@0: onStopRequest: function(aRequest, aContext, aStatusCode) { michael@0: bufferedOutputStream.close(); michael@0: outputStream.close(); michael@0: michael@0: if (!Components.isSuccessCode(aStatusCode)) { michael@0: deferred.reject("NETWORK_ERROR"); michael@0: return; michael@0: } michael@0: michael@0: // If we get a 4XX or a 5XX http status, bail out like if we had a michael@0: // network error. michael@0: let responseStatus = aRequestChannel.responseStatus; michael@0: if (responseStatus >= 400 && responseStatus <= 599) { michael@0: // unrecoverable error, don't bug the user michael@0: aOldApp.downloadAvailable = false; michael@0: deferred.reject("NETWORK_ERROR"); michael@0: return; michael@0: } michael@0: michael@0: deferred.resolve(zipFile); michael@0: } michael@0: }); michael@0: aRequestChannel.asyncOpen(listener, null); michael@0: michael@0: // send a first progress event to correctly set the DOM object's properties michael@0: this._sendDownloadProgressEvent(aNewApp, 0); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Compute the MD5 hash of a file, doing async IO off the main thread. michael@0: * michael@0: * @param {String} aFilePath michael@0: * the path of the file to hash michael@0: * @returns {String} the MD5 hash of the file michael@0: */ michael@0: _computeFileHash: function(aFilePath) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.initWithPath(aFilePath); michael@0: michael@0: NetUtil.asyncFetch(file, function(inputStream, status) { michael@0: if (!Components.isSuccessCode(status)) { michael@0: debug("Error reading " + aFilePath + ": " + e); michael@0: deferred.reject(); michael@0: return; michael@0: } michael@0: michael@0: let hasher = Cc["@mozilla.org/security/hash;1"] michael@0: .createInstance(Ci.nsICryptoHash); michael@0: // We want to use the MD5 algorithm. michael@0: hasher.init(hasher.MD5); michael@0: michael@0: const PR_UINT32_MAX = 0xffffffff; michael@0: hasher.updateFromStream(inputStream, PR_UINT32_MAX); michael@0: michael@0: // Return the two-digit hexadecimal code for a byte. michael@0: function toHexString(charCode) { michael@0: return ("0" + charCode.toString(16)).slice(-2); michael@0: } michael@0: michael@0: // We're passing false to get the binary hash and not base64. michael@0: let data = hasher.finish(false); michael@0: // Convert the binary hash data to a hex string. michael@0: let hash = [toHexString(data.charCodeAt(i)) for (i in data)].join(""); michael@0: debug("File hash computed: " + hash); michael@0: michael@0: deferred.resolve(hash); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Send an "applied" event right away for the package being installed. michael@0: * michael@0: * XXX We use this to exit the app update process early when the downloaded michael@0: * package is identical to the last one we installed. Presumably we do michael@0: * something similar after updating the app, and we could refactor both cases michael@0: * to use the same code to send the "applied" event. michael@0: * michael@0: * @param aNewApp {Object} the new app data michael@0: * @param aOldApp {Object} the currently stored app data michael@0: * @param aId {String} the unique id of the app michael@0: */ michael@0: _sendAppliedEvent: function(aNewApp, aOldApp, aId) { michael@0: aOldApp.downloading = false; michael@0: aOldApp.downloadAvailable = false; michael@0: aOldApp.downloadSize = 0; michael@0: aOldApp.installState = "installed"; michael@0: aOldApp.readyToApplyDownload = false; michael@0: if (aOldApp.staged && aOldApp.staged.manifestHash) { michael@0: // If we're here then the manifest has changed but the package michael@0: // hasn't. Let's clear this, so we don't keep offering michael@0: // a bogus update to the user michael@0: aOldApp.manifestHash = aOldApp.staged.manifestHash; michael@0: aOldApp.etag = aOldApp.staged.etag || aOldApp.etag; michael@0: aOldApp.staged = {}; michael@0: michael@0: // Move the staged update manifest to a non staged one. michael@0: try { michael@0: let staged = this._getAppDir(aId); michael@0: staged.append("staged-update.webapp"); michael@0: staged.moveTo(staged.parent, "update.webapp"); michael@0: } catch (ex) { michael@0: // We don't really mind much if this fails. michael@0: } michael@0: } michael@0: michael@0: // Save the updated registry, and cleanup the tmp directory. michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: aOldApp, michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: manifestURL: aNewApp.manifestURL, michael@0: eventType: ["downloadsuccess", "downloadapplied"] michael@0: }); michael@0: }); michael@0: let file = FileUtils.getFile("TmpD", ["webapps", aId], false); michael@0: if (file && file.exists()) { michael@0: file.remove(true); michael@0: } michael@0: }, michael@0: michael@0: _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall, michael@0: aIsUpdate, aManifest, aRequestChannel, aHash) { michael@0: return Task.spawn((function*() { michael@0: let zipReader, isSigned, newManifest; michael@0: michael@0: try { michael@0: [zipReader, isSigned] = yield this._openPackage(aZipFile, aOldApp, michael@0: aIsLocalFileInstall); michael@0: newManifest = yield this._readPackage(aOldApp, aNewApp, michael@0: aIsLocalFileInstall, aIsUpdate, aManifest, aRequestChannel, michael@0: aHash, zipReader, isSigned); michael@0: } catch (e) { michael@0: debug("package open/read error: " + e); michael@0: // Something bad happened when opening/reading the package. michael@0: // Unrecoverable error, don't bug the user. michael@0: // Apps with installState 'pending' does not produce any michael@0: // notification, so we are safe with its current michael@0: // downloadAvailable state. michael@0: if (aOldApp.installState !== "pending") { michael@0: aOldApp.downloadAvailable = false; michael@0: } michael@0: if (typeof e == 'object') { michael@0: Cu.reportError("Error while reading package:" + e); michael@0: throw "INVALID_PACKAGE"; michael@0: } else { michael@0: throw e; michael@0: } michael@0: } finally { michael@0: if (zipReader) { michael@0: zipReader.close(); michael@0: } michael@0: } michael@0: michael@0: return newManifest; michael@0: michael@0: }).bind(this)); michael@0: }, michael@0: michael@0: _openPackage: function(aZipFile, aApp, aIsLocalFileInstall) { michael@0: return Task.spawn((function*() { michael@0: let certDb; michael@0: try { michael@0: certDb = Cc["@mozilla.org/security/x509certdb;1"] michael@0: .getService(Ci.nsIX509CertDB); michael@0: } catch (e) { michael@0: debug("nsIX509CertDB error: " + e); michael@0: // unrecoverable error, don't bug the user michael@0: aApp.downloadAvailable = false; michael@0: throw "CERTDB_ERROR"; michael@0: } michael@0: michael@0: let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin, michael@0: aApp.manifestURL, michael@0: aZipFile, michael@0: certDb); michael@0: michael@0: // We cannot really know if the system date is correct or michael@0: // not. What we can know is if it's after the build date or not, michael@0: // and assume the build date is correct (which we cannot michael@0: // really know either). michael@0: let isLaterThanBuildTime = Date.now() > PLATFORM_BUILD_ID_TIME; michael@0: michael@0: let isSigned; michael@0: michael@0: if (Components.isSuccessCode(result)) { michael@0: isSigned = true; michael@0: } else if (result == Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY || michael@0: result == Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY || michael@0: result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING) { michael@0: throw "APP_PACKAGE_CORRUPTED"; michael@0: } else if (result == Cr.NS_ERROR_FILE_CORRUPTED || michael@0: result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE || michael@0: result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID || michael@0: result == Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID) { michael@0: throw "APP_PACKAGE_INVALID"; michael@0: } else if ((!aIsLocalFileInstall || isLaterThanBuildTime) && michael@0: (result != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)) { michael@0: throw "INVALID_SIGNATURE"; michael@0: } else { michael@0: // If it's a localFileInstall and the validation failed michael@0: // because of a expired certificate, just assume it was valid michael@0: // and that the error occurred because the system time has not michael@0: // been set yet. michael@0: isSigned = (aIsLocalFileInstall && michael@0: (getNSPRErrorCode(result) == michael@0: SEC_ERROR_EXPIRED_CERTIFICATE)); michael@0: michael@0: zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] michael@0: .createInstance(Ci.nsIZipReader); michael@0: zipReader.open(aZipFile); michael@0: } michael@0: michael@0: return [zipReader, isSigned]; michael@0: michael@0: }).bind(this)); michael@0: }, michael@0: michael@0: _openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let root = TrustedRootCertificate.index; michael@0: michael@0: let useReviewerCerts = false; michael@0: try { michael@0: useReviewerCerts = Services.prefs. michael@0: getBoolPref("dom.mozApps.use_reviewer_certs"); michael@0: } catch (ex) { } michael@0: michael@0: // We'll use the reviewer and dev certificates only if the pref is set to michael@0: // true. michael@0: if (useReviewerCerts) { michael@0: let manifestPath = Services.io.newURI(aManifestURL, null, null).path; michael@0: michael@0: switch (aInstallOrigin) { michael@0: case "https://marketplace.firefox.com": michael@0: root = manifestPath.startsWith("/reviewers/") michael@0: ? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot michael@0: : Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot; michael@0: break; michael@0: michael@0: case "https://marketplace-dev.allizom.org": michael@0: root = manifestPath.startsWith("/reviewers/") michael@0: ? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot michael@0: : Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: aCertDb.openSignedAppFileAsync( michael@0: root, aZipFile, michael@0: function(aRv, aZipReader) { michael@0: deferred.resolve([aRv, aZipReader]); michael@0: } michael@0: ); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate, michael@0: aManifest, aRequestChannel, aHash, aZipReader, michael@0: aIsSigned) { michael@0: this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall); michael@0: michael@0: if (!aZipReader.hasEntry("manifest.webapp")) { michael@0: throw "MISSING_MANIFEST"; michael@0: } michael@0: michael@0: let istream = aZipReader.getInputStream("manifest.webapp"); michael@0: michael@0: // Obtain a converter to read from a UTF-8 encoded input stream. michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: michael@0: let newManifest = JSON.parse(converter.ConvertToUnicode( michael@0: NetUtil.readInputStreamToString(istream, istream.available()) || "")); michael@0: michael@0: if (!AppsUtils.checkManifest(newManifest, aOldApp)) { michael@0: throw "INVALID_MANIFEST"; michael@0: } michael@0: michael@0: // For app updates we don't forbid apps to rename themselves but michael@0: // we still retain the old name of the app. In the future we michael@0: // will use UI to allow updates to rename an app after we check michael@0: // with the user that the rename is ok. michael@0: if (aIsUpdate) { michael@0: // Call ensureSameAppName before compareManifests as `manifest` michael@0: // has been normalized to avoid app rename. michael@0: AppsUtils.ensureSameAppName(aManifest._manifest, newManifest, aOldApp); michael@0: } michael@0: michael@0: if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) { michael@0: throw "MANIFEST_MISMATCH"; michael@0: } michael@0: michael@0: if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) { michael@0: throw "INSTALL_FROM_DENIED"; michael@0: } michael@0: michael@0: // Local file installs can be privileged even without the signature. michael@0: let maxStatus = aIsSigned || aIsLocalFileInstall michael@0: ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED michael@0: : Ci.nsIPrincipal.APP_STATUS_INSTALLED; michael@0: michael@0: if (AppsUtils.getAppManifestStatus(newManifest) > maxStatus) { michael@0: throw "INVALID_SECURITY_LEVEL"; michael@0: } michael@0: michael@0: aOldApp.appStatus = AppsUtils.getAppManifestStatus(newManifest); michael@0: michael@0: this._saveEtag(aIsUpdate, aOldApp, aRequestChannel, aHash, newManifest); michael@0: this._checkOrigin(aIsSigned || aIsLocalFileInstall, aOldApp, newManifest, michael@0: aIsUpdate); michael@0: this._getIds(aIsSigned, aZipReader, converter, aNewApp, aOldApp, aIsUpdate); michael@0: michael@0: return newManifest; michael@0: }, michael@0: michael@0: _checkSignature: function(aApp, aIsSigned, aIsLocalFileInstall) { michael@0: // XXX Security: You CANNOT safely add a new app store for michael@0: // installing privileged apps just by modifying this pref and michael@0: // adding the signing cert for that store to the cert trust michael@0: // database. *Any* origin listed can install apps signed with michael@0: // *any* certificate trusted; we don't try to maintain a strong michael@0: // association between certificate with installOrign. The michael@0: // expectation here is that in production builds the pref will michael@0: // contain exactly one origin. However, in custom development michael@0: // builds it may contain more than one origin so we can test michael@0: // different stages (dev, staging, prod) of the same app store. michael@0: // michael@0: // Only allow signed apps to be installed from a whitelist of michael@0: // domains, and require all packages installed from any of the michael@0: // domains on the whitelist to be signed. This is a stopgap until michael@0: // we have a real story for handling multiple app stores signing michael@0: // apps. michael@0: let signedAppOriginsStr = michael@0: Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from"); michael@0: // If it's a local install and it's signed then we assume michael@0: // the app origin is a valid signer. michael@0: let isSignedAppOrigin = (aIsSigned && aIsLocalFileInstall) || michael@0: signedAppOriginsStr.split(","). michael@0: indexOf(aApp.installOrigin) > -1; michael@0: if (!aIsSigned && isSignedAppOrigin) { michael@0: // Packaged apps installed from these origins must be signed; michael@0: // if not, assume somebody stripped the signature. michael@0: throw "INVALID_SIGNATURE"; michael@0: } else if (aIsSigned && !isSignedAppOrigin) { michael@0: // Other origins are *prohibited* from installing signed apps. michael@0: // One reason is that our app revocation mechanism requires michael@0: // strong cooperation from the host of the mini-manifest, which michael@0: // we assume to be under the control of the install origin, michael@0: // even if it has a different origin. michael@0: throw "INSTALL_FROM_DENIED"; michael@0: } michael@0: }, michael@0: michael@0: _saveEtag: function(aIsUpdate, aOldApp, aRequestChannel, aHash, aManifest) { michael@0: // Save the new Etag for the package. michael@0: if (aIsUpdate) { michael@0: if (!aOldApp.staged) { michael@0: aOldApp.staged = { }; michael@0: } michael@0: try { michael@0: aOldApp.staged.packageEtag = aRequestChannel.getResponseHeader("Etag"); michael@0: } catch(e) { } michael@0: aOldApp.staged.packageHash = aHash; michael@0: aOldApp.staged.appStatus = AppsUtils.getAppManifestStatus(aManifest); michael@0: } else { michael@0: try { michael@0: aOldApp.packageEtag = aRequestChannel.getResponseHeader("Etag"); michael@0: } catch(e) { } michael@0: aOldApp.packageHash = aHash; michael@0: aOldApp.appStatus = AppsUtils.getAppManifestStatus(aManifest); michael@0: } michael@0: }, michael@0: michael@0: _checkOrigin: function(aIsSigned, aOldApp, aManifest, aIsUpdate) { michael@0: // Check if the app declares which origin it will use. michael@0: if (aIsSigned && michael@0: aOldApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED && michael@0: aManifest.origin !== undefined) { michael@0: let uri; michael@0: try { michael@0: uri = Services.io.newURI(aManifest.origin, null, null); michael@0: } catch(e) { michael@0: throw "INVALID_ORIGIN"; michael@0: } michael@0: if (uri.scheme != "app") { michael@0: throw "INVALID_ORIGIN"; michael@0: } michael@0: michael@0: if (aIsUpdate) { michael@0: // Changing the origin during an update is not allowed. michael@0: if (uri.prePath != aOldApp.origin) { michael@0: throw "INVALID_ORIGIN_CHANGE"; michael@0: } michael@0: // Nothing else to do for an update... since the michael@0: // origin can't change we don't need to move the michael@0: // app nor can we have a duplicated origin michael@0: } else { michael@0: debug("Setting origin to " + uri.prePath + michael@0: " for " + aOldApp.manifestURL); michael@0: let newId = uri.prePath.substring(6); // "app://".length michael@0: if (newId in this.webapps) { michael@0: throw "DUPLICATE_ORIGIN"; michael@0: } michael@0: aOldApp.origin = uri.prePath; michael@0: // Update the registry. michael@0: let oldId = aOldApp.id; michael@0: aOldApp.id = newId; michael@0: this.webapps[newId] = aOldApp; michael@0: delete this.webapps[oldId]; michael@0: // Rename the directories where the files are installed. michael@0: [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) { michael@0: let parent = FileUtils.getDir(aDir, ["webapps"], true, true); michael@0: let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true); michael@0: dir.moveTo(parent, newId); michael@0: }); michael@0: // Signals that we need to swap the old id with the new app. michael@0: this.broadcastMessage("Webapps:RemoveApp", { id: oldId }); michael@0: this.broadcastMessage("Webapps:AddApp", { id: newId, michael@0: app: aOldApp }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp, michael@0: aIsUpdate) { michael@0: // Get ids.json if the file is signed michael@0: if (aIsSigned) { michael@0: let idsStream; michael@0: try { michael@0: idsStream = aZipReader.getInputStream("META-INF/ids.json"); michael@0: } catch (e) { michael@0: throw aZipReader.hasEntry("META-INF/ids.json") michael@0: ? e michael@0: : "MISSING_IDS_JSON"; michael@0: } michael@0: michael@0: let ids = JSON.parse(aConverter.ConvertToUnicode(NetUtil. michael@0: readInputStreamToString( idsStream, idsStream.available()) || "")); michael@0: if ((!ids.id) || !Number.isInteger(ids.version) || michael@0: (ids.version <= 0)) { michael@0: throw "INVALID_IDS_JSON"; michael@0: } michael@0: let storeId = aNewApp.installOrigin + "#" + ids.id; michael@0: this._checkForStoreIdMatch(aIsUpdate, aOldApp, storeId, ids.version); michael@0: aOldApp.storeId = storeId; michael@0: aOldApp.storeVersion = ids.version; michael@0: } michael@0: }, michael@0: michael@0: // aStoreId must be a string of the form michael@0: // # michael@0: // aStoreVersion must be a positive integer. michael@0: _checkForStoreIdMatch: function(aIsUpdate, aNewApp, aStoreId, aStoreVersion) { michael@0: // Things to check: michael@0: // 1. if it's a update: michael@0: // a. We should already have this storeId, or the original storeId must michael@0: // start with STORE_ID_PENDING_PREFIX michael@0: // b. The manifestURL for the stored app should be the same one we're michael@0: // updating michael@0: // c. And finally the version of the update should be higher than the one michael@0: // on the already installed package michael@0: // 2. else michael@0: // a. We should not have this storeId on the list michael@0: // We're currently launching WRONG_APP_STORE_ID for all the mismatch kind of michael@0: // errors, and APP_STORE_VERSION_ROLLBACK for the version error. michael@0: michael@0: // Does an app with this storeID exist already? michael@0: let appId = this.getAppLocalIdByStoreId(aStoreId); michael@0: let isInstalled = appId != Ci.nsIScriptSecurityManager.NO_APP_ID; michael@0: if (aIsUpdate) { michael@0: let isDifferent = aNewApp.localId !== appId; michael@0: let isPending = aNewApp.storeId.indexOf(STORE_ID_PENDING_PREFIX) == 0; michael@0: michael@0: if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) { michael@0: throw "WRONG_APP_STORE_ID"; michael@0: } michael@0: michael@0: if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) { michael@0: throw "APP_STORE_VERSION_ROLLBACK"; michael@0: } michael@0: michael@0: } else if (isInstalled) { michael@0: throw "WRONG_APP_STORE_ID"; michael@0: } michael@0: }, michael@0: michael@0: // Removes the directory we created, and sends an error to the DOM side. michael@0: _revertDownloadPackage: function(aId, aOldApp, aNewApp, aIsUpdate, aError) { michael@0: debug("Cleanup: " + aError + "\n" + aError.stack); michael@0: let dir = FileUtils.getDir("TmpD", ["webapps", aId], true, true); michael@0: try { michael@0: dir.remove(true); michael@0: } catch (e) { } michael@0: michael@0: // We avoid notifying the error to the DOM side if the app download michael@0: // was cancelled via cancelDownload, which already sends its own michael@0: // notification. michael@0: if (aOldApp.isCanceling) { michael@0: delete aOldApp.isCanceling; michael@0: return; michael@0: } michael@0: michael@0: let download = AppDownloadManager.get(aNewApp.manifestURL); michael@0: aOldApp.downloading = false; michael@0: michael@0: // If there were not enough storage to download the package we michael@0: // won't have a record of the download details, so we just set the michael@0: // installState to 'pending' at first download and to 'installed' when michael@0: // updating. michael@0: aOldApp.installState = download ? download.previousState michael@0: : aIsUpdate ? "installed" michael@0: : "pending"; michael@0: michael@0: if (aOldApp.staged) { michael@0: delete aOldApp.staged; michael@0: } michael@0: michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:UpdateState", { michael@0: app: aOldApp, michael@0: error: aError, michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: this.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "downloaderror", michael@0: manifestURL: aNewApp.manifestURL michael@0: }); michael@0: }); michael@0: AppDownloadManager.remove(aNewApp.manifestURL); michael@0: }, michael@0: michael@0: doUninstall: function(aData, aMm) { michael@0: this.uninstall(aData.manifestURL, michael@0: function onsuccess() { michael@0: aMm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData); michael@0: }, michael@0: function onfailure() { michael@0: // Fall-through, fails to uninstall the desired app because: michael@0: // - we cannot find the app to be uninstalled. michael@0: // - the app to be uninstalled is not removable. michael@0: aMm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: uninstall: function(aManifestURL, aOnSuccess, aOnFailure) { michael@0: debug("uninstall " + aManifestURL); michael@0: michael@0: let app = this.getAppByManifestURL(aManifestURL); michael@0: if (!app) { michael@0: aOnFailure("NO_SUCH_APP"); michael@0: return; michael@0: } michael@0: let id = app.id; michael@0: michael@0: if (!app.removable) { michael@0: debug("Error: cannot uninstall a non-removable app."); michael@0: aOnFailure("NON_REMOVABLE_APP"); michael@0: return; michael@0: } michael@0: michael@0: // Check if we are downloading something for this app, and cancel the michael@0: // download if needed. michael@0: this.cancelDownload(app.manifestURL); michael@0: michael@0: // Clean up the deprecated manifest cache if needed. michael@0: if (id in this._manifestCache) { michael@0: delete this._manifestCache[id]; michael@0: } michael@0: michael@0: // Clear private data first. michael@0: this._clearPrivateData(app.localId, false); michael@0: michael@0: // Then notify observers. michael@0: // We have to clone the app object as nsIDOMApplication objects are michael@0: // stringified as an empty object. (see bug 830376) michael@0: let appClone = AppsUtils.cloneAppObject(app); michael@0: Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(appClone)); michael@0: michael@0: if (supportSystemMessages()) { michael@0: this._readManifests([{ id: id }]).then((aResult) => { michael@0: this._unregisterActivities(aResult[0].manifest, app); michael@0: }); michael@0: } michael@0: michael@0: let dir = this._getAppDir(id); michael@0: try { michael@0: dir.remove(true); michael@0: } catch (e) {} michael@0: michael@0: delete this.webapps[id]; michael@0: michael@0: this._saveApps().then(() => { michael@0: this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", appClone); michael@0: // Catch exception on callback call to ensure notifying observers after michael@0: try { michael@0: if (aOnSuccess) { michael@0: aOnSuccess(); michael@0: } michael@0: } catch(ex) { michael@0: Cu.reportError("DOMApplicationRegistry: Exception on app uninstall: " + michael@0: ex + "\n" + ex.stack); michael@0: } michael@0: this.broadcastMessage("Webapps:RemoveApp", { id: id }); michael@0: }); michael@0: }, michael@0: michael@0: getSelf: function(aData, aMm) { michael@0: aData.apps = []; michael@0: michael@0: if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || michael@0: aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { michael@0: aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); michael@0: return; michael@0: } michael@0: michael@0: let tmp = []; michael@0: michael@0: for (let id in this.webapps) { michael@0: if (this.webapps[id].origin == aData.origin && michael@0: this.webapps[id].localId == aData.appId && michael@0: this._isLaunchable(this.webapps[id])) { michael@0: let app = AppsUtils.cloneAppObject(this.webapps[id]); michael@0: aData.apps.push(app); michael@0: tmp.push({ id: id }); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!aData.apps.length) { michael@0: aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); michael@0: return; michael@0: } michael@0: michael@0: this._readManifests(tmp).then((aResult) => { michael@0: for (let i = 0; i < aResult.length; i++) michael@0: aData.apps[i].manifest = aResult[i].manifest; michael@0: aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: checkInstalled: function(aData, aMm) { michael@0: aData.app = null; michael@0: let tmp = []; michael@0: michael@0: for (let appId in this.webapps) { michael@0: if (this.webapps[appId].manifestURL == aData.manifestURL && michael@0: this._isLaunchable(this.webapps[appId])) { michael@0: aData.app = AppsUtils.cloneAppObject(this.webapps[appId]); michael@0: tmp.push({ id: appId }); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: this._readManifests(tmp).then((aResult) => { michael@0: for (let i = 0; i < aResult.length; i++) { michael@0: aData.app.manifest = aResult[i].manifest; michael@0: break; michael@0: } michael@0: aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: getInstalled: function(aData, aMm) { michael@0: aData.apps = []; michael@0: let tmp = []; michael@0: michael@0: for (let id in this.webapps) { michael@0: if (this.webapps[id].installOrigin == aData.origin && michael@0: this._isLaunchable(this.webapps[id])) { michael@0: aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id])); michael@0: tmp.push({ id: id }); michael@0: } michael@0: } michael@0: michael@0: this._readManifests(tmp).then((aResult) => { michael@0: for (let i = 0; i < aResult.length; i++) michael@0: aData.apps[i].manifest = aResult[i].manifest; michael@0: aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: getNotInstalled: function(aData, aMm) { michael@0: aData.apps = []; michael@0: let tmp = []; michael@0: michael@0: for (let id in this.webapps) { michael@0: if (!this._isLaunchable(this.webapps[id])) { michael@0: aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id])); michael@0: tmp.push({ id: id }); michael@0: } michael@0: } michael@0: michael@0: this._readManifests(tmp).then((aResult) => { michael@0: for (let i = 0; i < aResult.length; i++) michael@0: aData.apps[i].manifest = aResult[i].manifest; michael@0: aMm.sendAsyncMessage("Webapps:GetNotInstalled:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: doGetAll: function(aData, aMm) { michael@0: this.getAll(function (apps) { michael@0: aData.apps = apps; michael@0: aMm.sendAsyncMessage("Webapps:GetAll:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: getAll: function(aCallback) { michael@0: debug("getAll"); michael@0: let apps = []; michael@0: let tmp = []; michael@0: michael@0: for (let id in this.webapps) { michael@0: let app = AppsUtils.cloneAppObject(this.webapps[id]); michael@0: if (!this._isLaunchable(app)) michael@0: continue; michael@0: michael@0: apps.push(app); michael@0: tmp.push({ id: id }); michael@0: } michael@0: michael@0: this._readManifests(tmp).then((aResult) => { michael@0: for (let i = 0; i < aResult.length; i++) michael@0: apps[i].manifest = aResult[i].manifest; michael@0: aCallback(apps); michael@0: }); michael@0: }, michael@0: michael@0: /* Check if |data| is actually a receipt */ michael@0: isReceipt: function(data) { michael@0: try { michael@0: // The receipt data shouldn't be too big (allow up to 1 MiB of data) michael@0: const MAX_RECEIPT_SIZE = 1048576; michael@0: michael@0: if (data.length > MAX_RECEIPT_SIZE) { michael@0: return "RECEIPT_TOO_BIG"; michael@0: } michael@0: michael@0: // Marketplace receipts are JWK + "~" + JWT michael@0: // Other receipts may contain only the JWT michael@0: let receiptParts = data.split('~'); michael@0: let jwtData = null; michael@0: if (receiptParts.length == 2) { michael@0: jwtData = receiptParts[1]; michael@0: } else { michael@0: jwtData = receiptParts[0]; michael@0: } michael@0: michael@0: let segments = jwtData.split('.'); michael@0: if (segments.length != 3) { michael@0: return "INVALID_SEGMENTS_NUMBER"; michael@0: } michael@0: michael@0: // We need to translate the base64 alphabet used in JWT to our base64 alphabet michael@0: // before calling atob. michael@0: let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+') michael@0: .replace(/_/g, '/'))); michael@0: if (!decodedReceipt) { michael@0: return "INVALID_RECEIPT_ENCODING"; michael@0: } michael@0: michael@0: // Required values for a receipt michael@0: if (!decodedReceipt.typ) { michael@0: return "RECEIPT_TYPE_REQUIRED"; michael@0: } michael@0: if (!decodedReceipt.product) { michael@0: return "RECEIPT_PRODUCT_REQUIRED"; michael@0: } michael@0: if (!decodedReceipt.user) { michael@0: return "RECEIPT_USER_REQUIRED"; michael@0: } michael@0: if (!decodedReceipt.iss) { michael@0: return "RECEIPT_ISS_REQUIRED"; michael@0: } michael@0: if (!decodedReceipt.nbf) { michael@0: return "RECEIPT_NBF_REQUIRED"; michael@0: } michael@0: if (!decodedReceipt.iat) { michael@0: return "RECEIPT_IAT_REQUIRED"; michael@0: } michael@0: michael@0: let allowedTypes = [ "purchase-receipt", "developer-receipt", michael@0: "reviewer-receipt", "test-receipt" ]; michael@0: if (allowedTypes.indexOf(decodedReceipt.typ) < 0) { michael@0: return "RECEIPT_TYPE_UNSUPPORTED"; michael@0: } michael@0: } catch (e) { michael@0: return "RECEIPT_ERROR"; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: addReceipt: function(aData, aMm) { michael@0: debug("addReceipt " + aData.manifestURL); michael@0: michael@0: let receipt = aData.receipt; michael@0: michael@0: if (!receipt) { michael@0: aData.error = "INVALID_PARAMETERS"; michael@0: aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let error = this.isReceipt(receipt); michael@0: if (error) { michael@0: aData.error = error; michael@0: aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let id = this._appIdForManifestURL(aData.manifestURL); michael@0: let app = this.webapps[id]; michael@0: michael@0: if (!app.receipts) { michael@0: app.receipts = []; michael@0: } else if (app.receipts.length > 500) { michael@0: aData.error = "TOO_MANY_RECEIPTS"; michael@0: aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let index = app.receipts.indexOf(receipt); michael@0: if (index >= 0) { michael@0: aData.error = "RECEIPT_ALREADY_EXISTS"; michael@0: aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: app.receipts.push(receipt); michael@0: michael@0: this._saveApps().then(() => { michael@0: aData.receipts = app.receipts; michael@0: aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: removeReceipt: function(aData, aMm) { michael@0: debug("removeReceipt " + aData.manifestURL); michael@0: michael@0: let receipt = aData.receipt; michael@0: michael@0: if (!receipt) { michael@0: aData.error = "INVALID_PARAMETERS"; michael@0: aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let id = this._appIdForManifestURL(aData.manifestURL); michael@0: let app = this.webapps[id]; michael@0: michael@0: if (!app.receipts) { michael@0: aData.error = "NO_SUCH_RECEIPT"; michael@0: aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let index = app.receipts.indexOf(receipt); michael@0: if (index == -1) { michael@0: aData.error = "NO_SUCH_RECEIPT"; michael@0: aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: app.receipts.splice(index, 1); michael@0: michael@0: this._saveApps().then(() => { michael@0: aData.receipts = app.receipts; michael@0: aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: replaceReceipt: function(aData, aMm) { michael@0: debug("replaceReceipt " + aData.manifestURL); michael@0: michael@0: let oldReceipt = aData.oldReceipt; michael@0: let newReceipt = aData.newReceipt; michael@0: michael@0: if (!oldReceipt || !newReceipt) { michael@0: aData.error = "INVALID_PARAMETERS"; michael@0: aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let error = this.isReceipt(newReceipt); michael@0: if (error) { michael@0: aData.error = error; michael@0: aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let id = this._appIdForManifestURL(aData.manifestURL); michael@0: let app = this.webapps[id]; michael@0: michael@0: if (!app.receipts) { michael@0: aData.error = "NO_SUCH_RECEIPT"; michael@0: aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: let oldIndex = app.receipts.indexOf(oldReceipt); michael@0: if (oldIndex == -1) { michael@0: aData.error = "NO_SUCH_RECEIPT"; michael@0: aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); michael@0: return; michael@0: } michael@0: michael@0: app.receipts[oldIndex] = newReceipt; michael@0: michael@0: this._saveApps().then(() => { michael@0: aData.receipts = app.receipts; michael@0: aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData); michael@0: }); michael@0: }, michael@0: michael@0: getManifestFor: function(aManifestURL) { michael@0: let id = this._appIdForManifestURL(aManifestURL); michael@0: let app = this.webapps[id]; michael@0: if (!id || (app.installState == "pending" && !app.retryingDownload)) { michael@0: return Promise.resolve(null); michael@0: } michael@0: michael@0: return this._readManifests([{ id: id }]).then((aResult) => { michael@0: return aResult[0].manifest; michael@0: }); michael@0: }, michael@0: michael@0: getAppByManifestURL: function(aManifestURL) { michael@0: return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL); michael@0: }, michael@0: michael@0: getCSPByLocalId: function(aLocalId) { michael@0: debug("getCSPByLocalId:" + aLocalId); michael@0: return AppsUtils.getCSPByLocalId(this.webapps, aLocalId); michael@0: }, michael@0: michael@0: getAppLocalIdByStoreId: function(aStoreId) { michael@0: debug("getAppLocalIdByStoreId:" + aStoreId); michael@0: return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId); michael@0: }, michael@0: michael@0: getAppByLocalId: function(aLocalId) { michael@0: return AppsUtils.getAppByLocalId(this.webapps, aLocalId); michael@0: }, michael@0: michael@0: getManifestURLByLocalId: function(aLocalId) { michael@0: return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId); michael@0: }, michael@0: michael@0: getAppLocalIdByManifestURL: function(aManifestURL) { michael@0: return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL); michael@0: }, michael@0: michael@0: getCoreAppsBasePath: function() { michael@0: return AppsUtils.getCoreAppsBasePath(); michael@0: }, michael@0: michael@0: getWebAppsBasePath: function() { michael@0: return OS.Path.dirname(this.appsFile); michael@0: }, michael@0: michael@0: _isLaunchable: function(aApp) { michael@0: if (this.allAppsLaunchable) michael@0: return true; michael@0: michael@0: return WebappOSUtils.isLaunchable(aApp); michael@0: }, michael@0: michael@0: _notifyCategoryAndObservers: function(subject, topic, data, msg) { michael@0: const serviceMarker = "service,"; michael@0: michael@0: // First create observers from the category manager. michael@0: let cm = michael@0: Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); michael@0: let enumerator = cm.enumerateCategory(topic); michael@0: michael@0: let observers = []; michael@0: michael@0: while (enumerator.hasMoreElements()) { michael@0: let entry = michael@0: enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data; michael@0: let contractID = cm.getCategoryEntry(topic, entry); michael@0: michael@0: let factoryFunction; michael@0: if (contractID.substring(0, serviceMarker.length) == serviceMarker) { michael@0: contractID = contractID.substring(serviceMarker.length); michael@0: factoryFunction = "getService"; michael@0: } michael@0: else { michael@0: factoryFunction = "createInstance"; michael@0: } michael@0: michael@0: try { michael@0: let handler = Cc[contractID][factoryFunction](); michael@0: if (handler) { michael@0: let observer = handler.QueryInterface(Ci.nsIObserver); michael@0: observers.push(observer); michael@0: } michael@0: } catch(e) { } michael@0: } michael@0: michael@0: // Next enumerate the registered observers. michael@0: enumerator = Services.obs.enumerateObservers(topic); michael@0: while (enumerator.hasMoreElements()) { michael@0: try { michael@0: let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver); michael@0: if (observers.indexOf(observer) == -1) { michael@0: observers.push(observer); michael@0: } michael@0: } catch (e) { } michael@0: } michael@0: michael@0: observers.forEach(function (observer) { michael@0: try { michael@0: observer.observe(subject, topic, data); michael@0: } catch(e) { } michael@0: }); michael@0: // Send back an answer to the child. michael@0: if (msg) { michael@0: ppmm.broadcastAsyncMessage("Webapps:ClearBrowserData:Return", msg); michael@0: } michael@0: }, michael@0: michael@0: registerBrowserElementParentForApp: function(bep, appId) { michael@0: let mm = bep._mm; michael@0: michael@0: // Make a listener function that holds on to this appId. michael@0: let listener = this.receiveAppMessage.bind(this, appId); michael@0: michael@0: this.frameMessages.forEach(function(msgName) { michael@0: mm.addMessageListener(msgName, listener); michael@0: }); michael@0: }, michael@0: michael@0: receiveAppMessage: function(appId, message) { michael@0: switch (message.name) { michael@0: case "Webapps:ClearBrowserData": michael@0: this._clearPrivateData(appId, true, message.data); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _clearPrivateData: function(appId, browserOnly, msg) { michael@0: let subject = { michael@0: appId: appId, michael@0: browserOnly: browserOnly, michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams]) michael@0: }; michael@0: this._notifyCategoryAndObservers(subject, "webapps-clear-data", null, msg); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Appcache download observer michael@0: */ michael@0: let AppcacheObserver = function(aApp) { michael@0: debug("Creating AppcacheObserver for " + aApp.origin + michael@0: " - " + aApp.installState); michael@0: this.app = aApp; michael@0: this.startStatus = aApp.installState; michael@0: this.lastProgressTime = 0; michael@0: // Send a first progress event to correctly set the DOM object's properties. michael@0: this._sendProgressEvent(); michael@0: }; michael@0: michael@0: AppcacheObserver.prototype = { michael@0: // nsIOfflineCacheUpdateObserver implementation michael@0: _sendProgressEvent: function() { michael@0: let app = this.app; michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: "progress", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: }, michael@0: michael@0: updateStateChanged: function appObs_Update(aUpdate, aState) { michael@0: let mustSave = false; michael@0: let app = this.app; michael@0: michael@0: debug("Offline cache state change for " + app.origin + " : " + aState); michael@0: michael@0: var self = this; michael@0: let setStatus = function appObs_setStatus(aStatus, aProgress) { michael@0: debug("Offlinecache setStatus to " + aStatus + " with progress " + michael@0: aProgress + " for " + app.origin); michael@0: mustSave = (app.installState != aStatus); michael@0: michael@0: app.installState = aStatus; michael@0: app.progress = aProgress; michael@0: if (aStatus != "installed") { michael@0: self._sendProgressEvent(); michael@0: return; michael@0: } michael@0: michael@0: app.updateTime = Date.now(); michael@0: app.downloading = false; michael@0: app.downloadAvailable = false; michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { michael@0: eventType: ["downloadsuccess", "downloadapplied"], michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: } michael@0: michael@0: let setError = function appObs_setError(aError) { michael@0: debug("Offlinecache setError to " + aError); michael@0: app.downloading = false; michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { michael@0: app: app, michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { michael@0: error: aError, michael@0: eventType: "downloaderror", michael@0: manifestURL: app.manifestURL michael@0: }); michael@0: mustSave = true; michael@0: } michael@0: michael@0: switch (aState) { michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR: michael@0: aUpdate.removeObserver(this); michael@0: AppDownloadManager.remove(app.manifestURL); michael@0: setError("APP_CACHE_DOWNLOAD_ERROR"); michael@0: break; michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE: michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED: michael@0: aUpdate.removeObserver(this); michael@0: AppDownloadManager.remove(app.manifestURL); michael@0: setStatus("installed", aUpdate.byteProgress); michael@0: break; michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING: michael@0: setStatus(this.startStatus, aUpdate.byteProgress); michael@0: break; michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED: michael@0: case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS: michael@0: let now = Date.now(); michael@0: if (now - this.lastProgressTime > MIN_PROGRESS_EVENT_DELAY) { michael@0: setStatus(this.startStatus, aUpdate.byteProgress); michael@0: this.lastProgressTime = now; michael@0: } michael@0: break; michael@0: } michael@0: michael@0: // Status changed, update the stored version. michael@0: if (mustSave) { michael@0: DOMApplicationRegistry._saveApps(); michael@0: } michael@0: }, michael@0: michael@0: applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) { michael@0: // Nothing to do. michael@0: } michael@0: }; michael@0: michael@0: DOMApplicationRegistry.init();