gecko-dev/dom/apps/Webapps.jsm

4819 строки
159 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
// Possible errors thrown by the signature verifier.
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
const SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11);
// We need this to decide if we should accept or not files signed with expired
// certificates.
function buildIDToTime() {
let platformBuildID =
Cc["@mozilla.org/xre/app-info;1"]
.getService(Ci.nsIXULAppInfo).platformBuildID;
let platformBuildIDDate = new Date();
platformBuildIDDate.setUTCFullYear(platformBuildID.substr(0,4),
platformBuildID.substr(4,2) - 1,
platformBuildID.substr(6,2));
platformBuildIDDate.setUTCHours(platformBuildID.substr(8,2),
platformBuildID.substr(10,2),
platformBuildID.substr(12,2));
return platformBuildIDDate.getTime();
}
const PLATFORM_BUILD_ID_TIME = buildIDToTime();
this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import('resource://gre/modules/ActivitiesService.jsm');
Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/AppDownloadManager.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/MessageBroadcaster.jsm");
XPCOMUtils.defineLazyGetter(this, "UserCustomizations", function() {
let enabled = false;
try {
enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
} catch(e) {}
if (enabled) {
return Cu.import("resource://gre/modules/UserCustomizations.jsm", {})
.UserCustomizations;
} else {
return {
register: function() {},
unregister: function() {}
};
}
});
XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate",
"resource://gre/modules/StoreTrustAnchor.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
"resource://gre/modules/PermissionsInstaller.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller",
"resource://gre/modules/OfflineCacheInstaller.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker",
"resource://gre/modules/SystemMessagePermissionsChecker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
"resource://gre/modules/ScriptPreloader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Langpacks",
"resource://gre/modules/Langpacks.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ImportExport",
"resource://gre/modules/ImportExport.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
"resource://gre/modules/Messaging.jsm");
#ifdef MOZ_WIDGET_GONK
XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
Cu.import("resource://gre/modules/systemlibs.js");
return libcutils;
});
#endif
#ifdef MOZ_WIDGET_ANDROID
// On Android, define the "debug" function as a binding of the "d" function
// from the AndroidLog module so it gets the "debug" priority and a log tag.
// We always report debug messages on Android because it's unnecessary
// to restrict reporting, per bug 1003469.
var debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {})
.AndroidLog.d.bind(null, "Webapps");
#else
// Elsewhere, report debug messages only if dom.mozApps.debug is set to true.
var debug;
function debugPrefObserver() {
debug = Services.prefs.getBoolPref("dom.mozApps.debug")
? (aMsg) => dump("-*- Webapps.jsm : " + aMsg + "\n")
: (aMsg) => {};
}
debugPrefObserver();
Services.prefs.addObserver("dom.mozApps.debug", debugPrefObserver, false);
#endif
function getNSPRErrorCode(err) {
return -1 * ((err) & 0xffff);
}
function supportUseCurrentProfile() {
return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile");
}
function supportSystemMessages() {
return Services.prefs.getBoolPref("dom.sysmsg.enabled");
}
// Minimum delay between two progress events while downloading, in ms.
const MIN_PROGRESS_EVENT_DELAY = 1500;
const chromeWindowType = "navigator:browser";
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageBroadcaster");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIMessageSender");
XPCOMUtils.defineLazyServiceGetter(this, "appsService",
"@mozilla.org/AppsService;1",
"nsIAppsService");
XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
return Cc["@mozilla.org/system-message-internal;1"]
.getService(Ci.nsISystemMessagesInternal);
});
XPCOMUtils.defineLazyGetter(this, "updateSvc", function() {
return Cc["@mozilla.org/offlinecacheupdate-service;1"]
.getService(Ci.nsIOfflineCacheUpdateService);
});
XPCOMUtils.defineLazyGetter(this, "permMgr", function() {
return Cc["@mozilla.org/permissionmanager;1"]
.getService(Ci.nsIPermissionManager);
});
#ifdef MOZ_WIDGET_GONK
const DIRECTORY_NAME = "webappsDir";
#elifdef ANDROID
const DIRECTORY_NAME = "webappsDir";
#else
// Mulet, B2G Desktop, etc.
const DIRECTORY_NAME = "ProfD";
#endif
// We'll use this to identify privileged apps that have been preinstalled
// For those apps we'll set
// STORE_ID_PENDING_PREFIX + installOrigin
// as the storeID. This ensures it's unique and can't be set from a legit
// store even by error.
const STORE_ID_PENDING_PREFIX = "#unknownID#";
this.DOMApplicationRegistry = {
// pseudo-constants for the different application kinds.
get kPackaged() { return "packaged"; },
get kHosted() { return "hosted"; },
get kHostedAppcache() { return "hosted-appcache"; },
get kAndroid() { return "android-native"; },
// Path to the webapps.json file where we store the registry data.
appsFile: null,
webapps: { },
_updateHandlers: [ ],
_pendingUninstalls: {},
_contentActions: new Map(),
dirKey: DIRECTORY_NAME,
init: function() {
// Keep the messages in sync with the lazy-loading in browser.js (bug 1171013).
this.messages = ["Webapps:Install",
"Webapps:Uninstall",
"Webapps:GetSelf",
"Webapps:CheckInstalled",
"Webapps:GetInstalled",
"Webapps:Launch",
"Webapps:LocationChange",
"Webapps:InstallPackage",
"Webapps:GetList",
"Webapps:RegisterForMessages",
"Webapps:UnregisterForMessages",
"Webapps:CancelDownload",
"Webapps:CheckForUpdate",
"Webapps:Download",
"Webapps:ApplyDownload",
"Webapps:Install:Return:Ack",
"Webapps:AddReceipt",
"Webapps:RemoveReceipt",
"Webapps:ReplaceReceipt",
"Webapps:RegisterBEP",
"Webapps:Export",
"Webapps:Import",
"Webapps:GetIcon",
"Webapps:ExtractManifest",
"Webapps:SetEnabled",
"child-process-shutdown"];
this.frameMessages = ["Webapps:ClearBrowserData"];
this.messages.forEach((function(msgName) {
ppmm.addMessageListener(msgName, this);
}).bind(this));
cpmm.addMessageListener("Activities:Register:OK", this);
cpmm.addMessageListener("Activities:Register:KO", this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
Services.obs.addObserver(this, "memory-pressure", false);
AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
["webapps", "webapps.json"], true).path;
this.loadAndUpdateApps();
Langpacks.registerRegistryFunctions(MessageBroadcaster.broadcastMessage.bind(MessageBroadcaster),
this._appIdForManifestURL.bind(this),
this.getFullAppByManifestURL.bind(this));
MessageBroadcaster.init(this.getAppByManifestURL);
},
// loads the current registry, that could be empty on first run.
loadCurrentRegistry: function() {
return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => {
if (!aData) {
return;
}
this.webapps = aData;
let appDir = OS.Path.dirname(this.appsFile);
for (let id in this.webapps) {
let app = this.webapps[id];
if (!app) {
delete this.webapps[id];
continue;
}
app.id = id;
// Make sure we have a localId
if (app.localId === undefined) {
app.localId = this._nextLocalId();
}
if (app.basePath === undefined) {
app.basePath = appDir;
}
// Default to removable apps.
if (app.removable === undefined) {
app.removable = true;
}
// Default to a non privileged status.
if (app.appStatus === undefined) {
app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
}
// Default to NO_APP_ID and not in browser.
if (app.installerAppId === undefined) {
app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
}
if (app.installerIsBrowser === undefined) {
app.installerIsBrowser = false;
}
// Default installState to "installed", and reset if we shutdown
// during an update.
if (app.installState === undefined ||
app.installState === "updating") {
app.installState = "installed";
}
// Default storeId to "" and storeVersion to 0
if (app.storeId === undefined) {
app.storeId = "";
}
if (app.storeVersion === undefined) {
app.storeVersion = 0;
}
// Default role to "".
if (app.role === undefined) {
app.role = "";
}
if (app.widgetPages === undefined) {
app.widgetPages = [];
}
if (!AppsUtils.checkAppRole(app.role, app.appStatus)) {
delete this.webapps[id];
continue;
}
if (app.enabled === undefined) {
app.enabled = true;
}
if (app.blockedStatus === undefined) {
app.blockedStatus = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
}
// At startup we can't be downloading, and the $TMP directory
// will be empty so we can't just apply a staged update.
app.downloading = false;
app.readyToApplyDownload = false;
}
});
},
// Notify we are starting with registering apps.
_registryStarted: Promise.defer(),
notifyAppsRegistryStart: function notifyAppsRegistryStart() {
Services.obs.notifyObservers(this, "webapps-registry-start", null);
this._registryStarted.resolve();
},
get registryStarted() {
return this._registryStarted.promise;
},
// The registry will be safe to clone when this promise is resolved.
_safeToClone: Promise.defer(),
// Notify we are done with registering apps and save a copy of the registry.
_registryReady: Promise.defer(),
notifyAppsRegistryReady: function notifyAppsRegistryReady() {
// Usually this promise will be resolved earlier, but just in case,
// resolve it here also.
this._safeToClone.resolve();
this._registryReady.resolve();
Services.obs.notifyObservers(this, "webapps-registry-ready", null);
this._saveApps();
},
get registryReady() {
return this._registryReady.promise;
},
get safeToClone() {
return this._safeToClone.promise;
},
// Ensure that the .to property in redirects is a relative URL.
sanitizeRedirects: function sanitizeRedirects(aSource) {
if (!aSource) {
return null;
}
let res = [];
for (let i = 0; i < aSource.length; i++) {
let redirect = aSource[i];
if (redirect.from && redirect.to &&
isAbsoluteURI(redirect.from) &&
!isAbsoluteURI(redirect.to)) {
res.push(redirect);
}
}
return res.length > 0 ? res : null;
},
_saveWidgetsFullPath: function(aManifest, aDestApp) {
if (aManifest.widgetPages) {
let resolve = (aPage)=>{
let filepath = AppsUtils.getFilePath(aPage);
return aManifest.resolveURL(filepath);
};
aDestApp.widgetPages = aManifest.widgetPages.map(resolve);
} else {
aDestApp.widgetPages = [];
}
},
// Registers all the activities and system messages.
registerAppsHandlers: Task.async(function*(aRunUpdate) {
this.notifyAppsRegistryStart();
let ids = [];
for (let id in this.webapps) {
ids.push({ id: id });
}
if (supportSystemMessages()) {
this._processManifestForIds(ids, aRunUpdate);
} else {
// Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on
// _processManifestForIds so as to not reading the manifests
// twice
let results = yield this._readManifests(ids);
results.forEach((aResult) => {
if (!aResult.manifest) {
// If we can't load the manifest, we probably have a corrupted
// registry. We delete the app since we can't do anything with it.
delete this.webapps[aResult.id];
return;
}
let app = this.webapps[aResult.id];
app.csp = aResult.manifest.csp || "";
app.role = aResult.manifest.role || "";
let localeManifest = new ManifestHelper(aResult.manifest, app.origin, app.manifestURL);
this._saveWidgetsFullPath(localeManifest, app);
if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
app.redirects = this.sanitizeRedirects(aResult.redirects);
}
app.kind = this.appKind(app, aResult.manifest);
UserCustomizations.register(app);
Langpacks.register(app, aResult.manifest);
});
// Nothing else to do but notifying we're ready.
this.notifyAppsRegistryReady();
}
}),
appKind: function(aApp, aManifest) {
if (aApp.origin.startsWith("android://")) {
return this.kAndroid;
} if (aApp.origin.startsWith("app://")) {
return this.kPackaged;
} else {
// Hosted apps, can be appcached or not.
let kind = this.kHosted;
if (aManifest.appcache_path) {
kind = this.kHostedAppcache;
}
return kind;
}
},
updatePermissionsForApp: function(aId, aIsPreinstalled) {
if (!this.webapps[aId]) {
return;
}
// Install the permissions for this app, as if we were updating
// to cleanup the old ones if needed.
// TODO It's not clear what this should do when there are multiple profiles.
if (supportUseCurrentProfile()) {
this._readManifests([{ id: aId }]).then((aResult) => {
let data = aResult[0];
this.webapps[aId].kind = this.webapps[aId].kind ||
this.appKind(this.webapps[aId], aResult[0].manifest);
PermissionsInstaller.installPermissions({
manifest: data.manifest,
manifestURL: this.webapps[aId].manifestURL,
origin: this.webapps[aId].origin,
isPreinstalled: aIsPreinstalled,
kind: this.webapps[aId].kind
}, true, function() {
debug("Error installing permissions for " + aId);
});
});
}
},
updateOfflineCacheForApp: function(aId) {
let app = this.webapps[aId];
this._readManifests([{ id: aId }]).then((aResult) => {
let manifest =
new ManifestHelper(aResult[0].manifest, app.origin, app.manifestURL);
let fullAppcachePath = manifest.fullAppcachePath();
if (fullAppcachePath) {
OfflineCacheInstaller.installCache({
cachePath: app.cachePath || app.basePath,
appId: aId,
origin: Services.io.newURI(app.origin, null, null),
localId: app.localId,
appcache_path: fullAppcachePath
});
}
});
},
// Installs a 3rd party app.
installPreinstalledApp: function installPreinstalledApp(aId) {
if (AppConstants.platform !== "gonk") {
return false;
}
// In some cases, the app might be already installed under a different ID but
// with the same manifestURL. In that case, the only content of the webapp will
// be the id of the old version, which is the one we'll keep.
let destId = this.webapps[aId].oldId || aId;
// We don't need the oldId anymore
if (destId !== aId) {
delete this.webapps[aId];
}
let app = this.webapps[destId];
let baseDir, isPreinstalled = false;
try {
baseDir = FileUtils.getDir("coreAppsDir", ["webapps", aId], false);
if (!baseDir.exists()) {
return isPreinstalled;
} else if (!baseDir.directoryEntries.hasMoreElements()) {
debug("Error: Core app in " + baseDir.path + " is empty");
return isPreinstalled;
}
} catch(e) {
// In ENG builds, we don't have apps in coreAppsDir.
return isPreinstalled;
}
// Beyond this point we know it's really a preinstalled app.
isPreinstalled = true;
let filesToMove;
let isPackage;
let updateFile = baseDir.clone();
updateFile.append("update.webapp");
if (!updateFile.exists()) {
// The update manifest is missing, this is a hosted app only if there is
// no application.zip
let appFile = baseDir.clone();
appFile.append("application.zip");
if (appFile.exists()) {
return isPreinstalled;
}
isPackage = false;
filesToMove = ["manifest.webapp"];
} else {
isPackage = true;
filesToMove = ["application.zip", "update.webapp"];
}
debug("Installing 3rd party app : " + aId +
" from " + baseDir.path + " to " + destId);
// We copy this app to DIRECTORY_NAME/$destId, and set the base path as needed.
let destDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", destId], true, true);
filesToMove.forEach(function(aFile) {
let file = baseDir.clone();
file.append(aFile);
try {
file.copyTo(destDir, aFile);
} catch(e) {
debug("Error: Failed to copy " + file.path + " to " + destDir.path);
}
});
app.installState = "installed";
app.cachePath = app.basePath;
app.basePath = OS.Path.dirname(this.appsFile);
if (!isPackage) {
return isPreinstalled;
}
app.origin = "app://" + destId;
// Do this for all preinstalled apps... we can't know at this
// point if the updates will be signed or not and it doesn't
// hurt to have it always.
app.storeId = STORE_ID_PENDING_PREFIX + app.installOrigin;
// Extract the manifest.webapp file from application.zip.
let zipFile = baseDir.clone();
zipFile.append("application.zip");
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
.createInstance(Ci.nsIZipReader);
try {
debug("Opening " + zipFile.path);
zipReader.open(zipFile);
if (!zipReader.hasEntry("manifest.webapp")) {
throw "MISSING_MANIFEST";
}
let manifestFile = destDir.clone();
manifestFile.append("manifest.webapp");
zipReader.extract("manifest.webapp", manifestFile);
} catch(e) {
// If we are unable to extract the manifest, cleanup and remove this app.
debug("Cleaning up: " + e);
destDir.remove(true);
delete this.webapps[destId];
} finally {
zipReader.close();
}
return isPreinstalled;
},
// For hosted apps, uninstall an app served from http:// if we have
// one installed from the same url with an https:// scheme.
removeIfHttpsDuplicate: function(aId) {
#ifdef MOZ_WIDGET_GONK
let app = this.webapps[aId];
if (!app || !app.origin.startsWith("http://")) {
return;
}
let httpsManifestURL =
"https://" + app.manifestURL.substring("http://".length);
// This will uninstall the http apps and remove any data hold by this
// app. Bug 948105 tracks data migration from http to https apps.
for (let id in this.webapps) {
if (this.webapps[id].manifestURL === httpsManifestURL) {
debug("Found a http/https match: " + app.manifestURL + " / " +
this.webapps[id].manifestURL);
this.uninstall(app.manifestURL);
return;
}
}
#endif
},
// Implements the core of bug 787439
// if at first run, go through these steps:
// a. load the core apps registry.
// b. uninstall any core app from the current registry but not in the
// new core apps registry.
// c. for all apps in the new core registry, install them if they are not
// yet in the current registry, and run installPermissions()
installSystemApps: function() {
return Task.spawn(function*() {
let file;
try {
file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false);
} catch(e) { }
if (!file || !file.exists()) {
return;
}
// a
let data = yield AppsUtils.loadJSONAsync(file.path);
if (!data) {
return;
}
// b : core apps are not removable.
for (let id in this.webapps) {
if (id in data || this.webapps[id].removable)
continue;
// Remove the permissions, cookies and private data for this app.
// Both permission and cookie managers observe the "clear-origin-data"
// event.
let localId = this.webapps[id].localId;
this._clearPrivateData(localId, false);
delete this.webapps[id];
}
let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
// c
for (let id in data) {
// Core apps have ids matching their domain name (eg: dialer.gaiamobile.org)
// Use that property to check if they are new or not.
// Note that in some cases, the id might change, but the
// manifest URL wont. So consider that the app is old if
// the id does not exist already and if there's no other id
// for the manifestURL.
var oldId = (id in this.webapps) ? id :
this._appIdForManifestURL(data[id].manifestURL);
if (!oldId) {
this.webapps[id] = data[id];
this.webapps[id].basePath = appDir.path;
this.webapps[id].id = id;
// Create a new localId.
this.webapps[id].localId = this._nextLocalId();
// Core apps are not removable.
if (this.webapps[id].removable === undefined) {
this.webapps[id].removable = false;
}
} else {
// Fields that we must not update. Confere bug 993011 comment 10.
let fieldsBlacklist = ["basePath", "id", "installerAppId",
"installerIsBrowser", "localId", "receipts", "storeId",
"storeVersion"];
// we fall into this case if the app is present in /system/b2g/webapps/webapps.json
// and in /data/local/webapps/webapps.json: this happens when updating gaia apps
// Confere bug 989876
// We also should fall in this case when the app is a preinstalled third party app.
for (let field in data[id]) {
if (fieldsBlacklist.indexOf(field) === -1) {
this.webapps[oldId][field] = data[id][field];
}
}
// If the id for the app has changed on the update, keep a pointer to the old one
// since we'll need this to update the app files.
if (id !== oldId) {
this.webapps[id] = {oldId: oldId};
}
}
}
}.bind(this)).then(null, Cu.reportError);
},
loadAndUpdateApps: function() {
return Task.spawn(function*() {
let runUpdate = false;
try {
runUpdate = AppsUtils.isFirstRun(Services.prefs);
} catch(e) {}
let loadAppPermission = Services.prefs.getBoolPref("dom.apps.reset-permissions");
yield this.loadCurrentRegistry();
// Sanity check and roll back previous incomplete app updates.
for (let id in this.webapps) {
let oldDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id + ".old"], false, true);
if (oldDir.exists()) {
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], false, true);
if (dir.exists()) {
dir.remove(true);
}
oldDir.moveTo(null, id);
}
}
try {
let systemManifestURL =
Services.prefs.getCharPref("b2g.system_manifest_url");
let systemAppFound =
this.webapps.some(v => v.manifestURL == systemManifestURL);
// We configured a system app but can't find it. That prevents us
// from starting so we clear our registry to start again from scratch.
if (!systemAppFound) {
runUpdate = true;
}
} catch(e) {} // getCharPref will throw on non-b2g platforms. That's ok.
if (runUpdate || !loadAppPermission) {
// Run migration before uninstall of core apps happens.
let appMigrator = Components.classes["@mozilla.org/app-migrator;1"];
if (appMigrator) {
try {
appMigrator = appMigrator.createInstance(Components.interfaces.nsIObserver);
appMigrator.observe(null, "webapps-before-update-merge", null);
} catch(e) {
debug("Exception running app migration: ");
debug(e.name + " " + e.message);
debug("Skipping app migration.");
}
}
if (AppConstants.MOZ_B2G) {
yield this.installSystemApps();
}
// At first run, install preloaded apps and set up their permissions.
for (let id in this.webapps) {
let isPreinstalled = this.installPreinstalledApp(id);
this.removeIfHttpsDuplicate(id);
if (!this.webapps[id]) {
continue;
}
this.updateOfflineCacheForApp(id);
this.updatePermissionsForApp(id, isPreinstalled);
}
// Need to update the persisted list of apps since
// installPreinstalledApp() removes the ones failing to install.
yield this._saveApps();
Services.prefs.setBoolPref("dom.apps.reset-permissions", true);
}
yield this.registerAppsHandlers(runUpdate);
}.bind(this)).then(null, Cu.reportError);
},
// |aEntryPoint| is either the entry_point name or the null in which case we
// use the root of the manifest.
//
// TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...).
_registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) {
let root = aManifest;
if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
root = aManifest.entry_points[aEntryPoint];
}
if (!root.messages) {
// This application just doesn't use system messages.
return;
}
if (!Array.isArray(root.messages) || root.messages.length == 0) {
dump("Could not register invalid system message entry for " + aApp.manifestURL + "\n");
try {
dump(JSON.stringify(root.messages) + "\n");
} catch(e) {}
return;
}
let manifest = new ManifestHelper(aManifest, aApp.origin, aApp.manifestURL);
let launchPathURI = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), null, null);
let manifestURI = Services.io.newURI(aApp.manifestURL, null, null);
root.messages.forEach(function registerPages(aMessage) {
let handlerPageURI = launchPathURI;
let messageName;
if (typeof(aMessage) !== "object" || Object.keys(aMessage).length !== 1) {
dump("Could not register invalid system message entry for " + aApp.manifestURL + "\n");
try {
dump(JSON.stringify(aMessage) + "\n");
} catch(e) {}
return;
}
messageName = Object.keys(aMessage)[0];
let handlerPath = aMessage[messageName];
// Resolve the handler path from origin. If |handler_path| is absent,
// simply skip.
let fullHandlerPath;
try {
if (handlerPath && handlerPath.trim()) {
fullHandlerPath = manifest.resolveURL(handlerPath);
} else {
throw new Error("Empty or blank handler path.");
}
} catch(e) {
debug("system message handler path (" + handlerPath + ") is " +
"invalid, skipping. Error is: " + e);
return;
}
handlerPageURI = Services.io.newURI(fullHandlerPath, null, null);
if (SystemMessagePermissionsChecker
.isSystemMessagePermittedToRegister(messageName,
aApp.manifestURL,
aApp.origin,
aManifest)) {
msgmgr.registerPage(messageName, handlerPageURI, manifestURI);
}
});
},
_registerSystemMessages: function(aManifest, aApp) {
this._registerSystemMessagesForEntryPoint(aManifest, aApp, null);
if (!aManifest.entry_points) {
return;
}
for (let entryPoint in aManifest.entry_points) {
this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint);
}
},
// |aEntryPoint| is either the entry_point name or the null in which case we
// use the root of the manifest.
_createActivitiesToRegister: function(aManifest, aApp, aEntryPoint,
aRunUpdate, aUninstall) {
let activitiesToRegister = [];
let root = aManifest;
if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
root = aManifest.entry_points[aEntryPoint];
}
if (!root || !root.activities) {
return activitiesToRegister;
}
let manifest = new ManifestHelper(aManifest, aApp.origin, aApp.manifestURL);
for (let activity in root.activities) {
let entry = root.activities[activity];
if (!Array.isArray(entry)) {
entry = [entry];
}
for (let i = 0; i < entry.length; i++) {
let description = entry[i];
let href = description.href;
if (!href) {
href = manifest.launch_path;
}
try {
href = manifest.resolveURL(href);
} catch (e) {
debug("Activity href (" + href + ") is invalid, skipping. " +
"Error is: " + e);
continue;
}
// Make a copy of the description object since we don't want to modify
// the manifest itself, but need to register with a resolved URI.
let newDesc = {};
for (let prop in description) {
newDesc[prop] = description[prop];
}
newDesc.href = href;
debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' +
activity + ', description.href is ' + newDesc.href);
if (aRunUpdate || aUninstall) {
activitiesToRegister.push({ "manifest": aApp.manifestURL,
"name": activity,
"icon": manifest.iconURLForSize(128),
"description": newDesc });
}
if (aUninstall) {
continue;
}
let launchPathURI = Services.io.newURI(href, null, null);
let manifestURI = Services.io.newURI(aApp.manifestURL, null, null);
if (SystemMessagePermissionsChecker
.isSystemMessagePermittedToRegister("activity",
aApp.manifestURL,
aApp.origin,
aManifest)) {
msgmgr.registerPage("activity", launchPathURI, manifestURI);
}
}
}
return activitiesToRegister;
},
// |aAppsToRegister| contains an array of apps to be registered, where
// each element is an object in the format of {manifest: foo, app: bar}.
_registerActivitiesForApps: function(aAppsToRegister, aRunUpdate) {
// Collect the activities to be registered for root and entry_points.
let activitiesToRegister = [];
aAppsToRegister.forEach(function (aApp) {
let manifest = aApp.manifest;
let app = aApp.app;
activitiesToRegister.push.apply(activitiesToRegister,
this._createActivitiesToRegister(manifest, app, null, aRunUpdate));
if (aRunUpdate) {
cpmm.sendAsyncMessage("Activities:UnregisterAll", app.manifestURL);
}
if (!manifest.entry_points) {
return;
}
for (let entryPoint in manifest.entry_points) {
activitiesToRegister.push.apply(activitiesToRegister,
this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate));
}
}, this);
if (!aRunUpdate || activitiesToRegister.length == 0) {
this.notifyAppsRegistryReady();
return;
}
// Send the array carrying all the activities to be registered.
cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister);
},
// Better to directly use |_registerActivitiesForApps()| if we have
// multiple apps to be registered for activities.
_registerActivities: function(aManifest, aApp, aRunUpdate) {
this._registerActivitiesForApps([{ manifest: aManifest, app: aApp }], aRunUpdate);
},
// |aAppsToUnregister| contains an array of apps to be unregistered, where
// each element is an object in the format of {manifest: foo, app: bar}.
_unregisterActivitiesForApps: function(aAppsToUnregister) {
// Collect the activities to be unregistered for root and entry_points.
let activitiesToUnregister = [];
aAppsToUnregister.forEach(function (aApp) {
let manifest = aApp.manifest;
let app = aApp.app;
activitiesToUnregister.push.apply(activitiesToUnregister,
this._createActivitiesToRegister(manifest, app, null, false, true));
if (!manifest.entry_points) {
return;
}
for (let entryPoint in manifest.entry_points) {
activitiesToUnregister.push.apply(activitiesToUnregister,
this._createActivitiesToRegister(manifest, app, entryPoint, false, true));
}
}, this);
// Send the array carrying all the activities to be unregistered.
cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister);
},
// Better to directly use |_unregisterActivitiesForApps()| if we have
// multiple apps to be unregistered for activities.
_unregisterActivities: function(aManifest, aApp) {
this._unregisterActivitiesForApps([{ manifest: aManifest, app: aApp }]);
},
_processManifestForIds: function(aIds, aRunUpdate) {
this._readManifests(aIds).then((aResults) => {
let appsToRegister = [];
aResults.forEach((aResult) => {
let app = this.webapps[aResult.id];
let manifest = aResult.manifest;
if (!manifest) {
// If we can't load the manifest, we probably have a corrupted
// registry. We delete the app since we can't do anything with it.
delete this.webapps[aResult.id];
return;
}
let localeManifest =
new ManifestHelper(manifest, app.origin, app.manifestURL);
app.name = manifest.name;
app.csp = manifest.csp || "";
app.role = localeManifest.role;
this._saveWidgetsFullPath(localeManifest, app);
if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
app.redirects = this.sanitizeRedirects(manifest.redirects);
}
app.kind = this.appKind(app, aResult.manifest);
this._registerSystemMessages(manifest, app);
appsToRegister.push({ manifest: manifest, app: app });
UserCustomizations.register(app);
Langpacks.register(app, manifest);
});
this._safeToClone.resolve();
this._registerActivitiesForApps(appsToRegister, aRunUpdate);
});
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "xpcom-shutdown") {
this.messages.forEach((function(msgName) {
ppmm.removeMessageListener(msgName, this);
}).bind(this));
Services.obs.removeObserver(this, "xpcom-shutdown");
Services.obs.removeObserver(this, "memory-pressure");
cpmm = null;
ppmm = null;
} else if (aTopic == "memory-pressure") {
// Clear the manifest cache on memory pressure.
this._manifestCache = {};
}
},
// Check extensions to be blocked.
blockExtensions: function(aExtensions) {
debug("blockExtensions");
let app;
let runtime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime);
aExtensions.filter(extension => {
// Filter out id-less items and those who don't have a matching installed
// extension.
if (!extension.attributes.has("id")) {
return false;
}
// Check that we have an app with this extension id.
let extId = extension.attributes.get("id");
for (let id in this.webapps) {
if (this.webapps[id].blocklistId == extId) {
app = this.webapps[id];
return true;
}
}
// No webapp found for this extension id.
return false;
}).forEach(extension => {
// `extension` is a object such as:
// {"versions":[{"minVersion":"0.1",
// "maxVersion":"1.3.328.4",
// "severity":"1",
// "vulnerabilityStatus":0,
// "targetApps":{
// "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}":[{"minVersion":"3.7a1pre","maxVersion":"*"}]
// }
// }],
// "prefs":[],
// "blockID":"i24",
// "attributes": Map()
// }
//
// `versions` is array of BlocklistItemData (see nsBlocklistService.js)
let severity = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
for (let item of extension.versions) {
if (item.includesItem(app.extensionVersion, runtime.version, runtime.platformVersion)) {
severity = item.severity;
break;
}
}
this.setBlockedStatus(app.manifestURL, severity);
});
},
formatMessage: function(aData) {
let msg = aData;
delete msg["mm"];
return msg;
},
receiveMessage: function(aMessage) {
let msg = aMessage.data || {};
let mm = aMessage.target;
msg.mm = mm;
let principal = aMessage.principal;
let checkPermission = function(aPermission) {
if (!permMgr.testPermissionFromPrincipal(principal, aPermission)) {
debug("mozApps message " + aMessage.name +
" from a content process with no " + aPermission + " privileges.");
return false;
}
return true;
};
// We need to check permissions for calls coming from mozApps.mgmt.
let allowed = true;
switch (aMessage.name) {
case "Webapps:Uninstall":
let isCurrentHomescreen =
Services.prefs.prefHasUserValue("dom.mozApps.homescreenURL") &&
Services.prefs.getCharPref("dom.mozApps.homescreenURL") ==
appsService.getManifestURLByLocalId(principal.appId);
allowed = checkPermission("webapps-manage") ||
(checkPermission("homescreen-webapps-manage") && isCurrentHomescreen);
break;
case "Webapps:ApplyDownload":
case "Webapps:Import":
case "Webapps:ExtractManifest":
case "Webapps:SetEnabled":
allowed = checkPermission("webapps-manage");
break;
case "Webapps:RegisterBEP":
allowed = checkPermission("browser");
break;
default:
break;
}
let processedImmediately = true;
if (!allowed) {
mm.killChild();
return null;
}
// There are two kind of messages: the messages that only make sense once the
// registry is ready, and those that can (or have to) be treated as soon as
// they're received.
switch (aMessage.name) {
case "Activities:Register:KO":
dump("Activities didn't register correctly!");
case "Activities:Register:OK":
// Activities:Register:OK is special because it's one way the registryReady
// promise can be resolved.
// XXX: What to do when the activities registration failed? At this point
// just act as if nothing happened.
this.notifyAppsRegistryReady();
break;
case "Webapps:GetList":
// GetList is special because it's synchronous. So far so well, it's the
// only synchronous message, if we get more at some point they should get
// this treatment also.
return this.doGetList();
case "child-process-shutdown":
MessageBroadcaster.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
break;
case "Webapps:RegisterForMessages":
MessageBroadcaster.addMessageListener(msg.messages, msg.app, mm);
break;
case "Webapps:UnregisterForMessages":
MessageBroadcaster.removeMessageListener(msg, mm);
break;
default:
processedImmediately = false;
}
if (processedImmediately) {
return;
}
// For all the rest (asynchronous), we wait till the registry is ready
// before processing the message.
this.registryReady.then( () => {
switch (aMessage.name) {
case "Webapps:Install": {
if (AppConstants.platform == "android") {
Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
} else {
this.doInstall(msg, mm);
}
break;
}
case "Webapps:GetSelf":
this.getSelf(msg, mm);
break;
case "Webapps:Uninstall":
if (AppConstants.platform == "android") {
Services.obs.notifyObservers(mm, "webapps-runtime-uninstall", JSON.stringify(msg));
} else {
this.doUninstall(msg, mm);
}
break;
case "Webapps:Launch":
this.doLaunch(msg, mm);
break;
case "Webapps:LocationChange":
this.onLocationChange(msg.oid);
break;
case "Webapps:CheckInstalled":
this.checkInstalled(msg, mm);
break;
case "Webapps:GetInstalled":
this.getInstalled(msg, mm);
break;
case "Webapps:InstallPackage": {
if (AppConstants.platform == "android") {
Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
} else {
this.doInstallPackage(msg, mm);
}
break;
}
case "Webapps:Download":
this.startDownload(msg.manifestURL);
break;
case "Webapps:CancelDownload":
this.cancelDownload(msg.manifestURL);
break;
case "Webapps:CheckForUpdate":
this.checkForUpdate(msg, mm);
break;
case "Webapps:ApplyDownload":
this.applyDownload(msg.manifestURL);
break;
case "Webapps:Install:Return:Ack":
this.onInstallSuccessAck(msg.manifestURL);
break;
case "Webapps:AddReceipt":
this.addReceipt(msg, mm);
break;
case "Webapps:RemoveReceipt":
this.removeReceipt(msg, mm);
break;
case "Webapps:ReplaceReceipt":
this.replaceReceipt(msg, mm);
break;
case "Webapps:RegisterBEP":
this.registerBrowserElementParentForApp(msg, mm);
break;
case "Webapps:GetIcon":
this.getIcon(msg, mm);
break;
case "Webapps:Export":
this.doExport(msg, mm);
break;
case "Webapps:Import":
this.doImport(msg, mm);
break;
case "Webapps:ExtractManifest":
this.doExtractManifest(msg, mm);
break;
case "Webapps:SetEnabled":
this.setEnabled(msg);
break;
}
});
},
getAppInfo: function getAppInfo(aAppId) {
return AppsUtils.getAppInfo(this.webapps, aAppId);
},
registerUpdateHandler: function(aHandler) {
this._updateHandlers.push(aHandler);
},
unregisterUpdateHandler: function(aHandler) {
let index = this._updateHandlers.indexOf(aHandler);
if (index != -1) {
this._updateHandlers.splice(index, 1);
}
},
notifyUpdateHandlers: function(aApp, aManifest, aZipPath) {
for (let updateHandler of this._updateHandlers) {
updateHandler(aApp, aManifest, aZipPath);
}
},
_getAppDir: function(aId) {
return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
},
_writeFile: function(aPath, aData) {
debug("Saving " + aPath);
let deferred = Promise.defer();
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(aPath);
// Initialize the file output stream
let ostream = FileUtils.openSafeFileOutputStream(file);
// Obtain a converter to convert our data to a UTF-8 encoded input stream.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Asynchronously copy the data to the file.
let istream = converter.convertToInputStream(aData);
NetUtil.asyncCopy(istream, ostream, function(aResult) {
if (!Components.isSuccessCode(aResult)) {
debug("Error saving " + aPath + " : " + aResult);
deferred.reject(aResult)
} else {
debug("Success saving " + aPath);
deferred.resolve();
}
});
return deferred.promise;
},
/**
* Returns the full list of apps and manifests.
*/
doGetList: function() {
let tmp = [];
let res = {};
let done = false;
// We allow cloning the registry when the local processing has been done.
this.safeToClone.then( () => {
for (let id in this.webapps) {
tmp.push({ id: id });
this.webapps[id].additionalLanguages =
Langpacks.getAdditionalLanguages(this.webapps[id].manifestURL).langs;
}
this._readManifests(tmp).then(
function(manifests) {
manifests.forEach((item) => {
res[item.id] = item.manifest;
});
done = true;
}
);
});
let thread = Services.tm.currentThread;
while (!done) {
thread.processNextEvent(/* mayWait */ true);
}
return { webapps: this.webapps, manifests: res };
},
doExport: function(aMsg, aMm) {
let sendError = (aError) => {
aMm.sendAsyncMessage("Webapps:Export:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
error: aError,
success: false
});
};
let app = this.getAppByManifestURL(aMsg.manifestURL);
if (!app) {
sendError("NoSuchApp");
return;
}
ImportExport.export(app).then(
aBlob => {
debug("exporting " + aBlob);
aMm.sendAsyncMessage("Webapps:Export:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
blob: aBlob,
success: true
});
},
aError => sendError(aError));
},
doImport: function(aMsg, aMm) {
let sendError = (aError) => {
aMm.sendAsyncMessage("Webapps:Import:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
error: aError,
success: false
});
};
if (!aMsg.blob || !aMsg.blob instanceof Ci.nsIDOMBlob) {
sendError("NoBlobFound");
return;
}
ImportExport.import(aMsg.blob).then(
([aManifestURL, aManifest]) => {
let app = this.getAppByManifestURL(aManifestURL);
app.manifest = aManifest;
aMm.sendAsyncMessage("Webapps:Import:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
app: app,
success: true
});
},
aError => sendError(aError));
},
doExtractManifest: function(aMsg, aMm) {
let sendError = (aError) => {
aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
error: aError,
success: false
});
};
if (!aMsg.blob || !aMsg.blob instanceof Ci.nsIDOMBlob) {
sendError("NoBlobFound");
return;
}
ImportExport.extractManifest(aMsg.blob).then(
aManifest => {
aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
manifest: aManifest,
success: true
});
},
aError => {
aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
{ requestID: aMsg.requestID,
oid: aMsg.oid,
error: aError,
success: false
});
}
);
},
doLaunch: function (aData, aMm) {
this.launch(
aData.manifestURL,
aData.startPoint,
aData.timestamp,
() => {
aMm.sendAsyncMessage("Webapps:Launch:Return:OK", this.formatMessage(aData));
},
(reason) => {
aData.error = reason;
aMm.sendAsyncMessage("Webapps:Launch:Return:KO", this.formatMessage(aData));
}
);
},
launch: function launch(aManifestURL, aStartPoint, aTimeStamp, aOnSuccess, aOnFailure) {
let app = this.getAppByManifestURL(aManifestURL);
if (!app) {
aOnFailure("NO_SUCH_APP");
return;
}
// Fire an error when trying to launch an app that is not
// yet fully installed.
if (app.installState == "pending") {
aOnFailure("PENDING_APP_NOT_LAUNCHABLE");
return;
}
// Delegate native android apps launch.
if (this.kAndroid == app.kind) {
debug("Launching android app " + app.origin);
let [packageName, className] =
AndroidUtils.getPackageAndClassFromManifestURL(aManifestURL);
debug(" " + packageName + " " + className);
Messaging.sendRequest({ type: "Apps:Launch",
packagename: packageName,
classname: className });
aOnSuccess();
return;
}
// We have to clone the app object as nsIDOMApplication objects are
// stringified as an empty object. (see bug 830376)
let appClone = AppsUtils.cloneAppObject(app);
appClone.startPoint = aStartPoint;
appClone.timestamp = aTimeStamp;
Services.obs.notifyObservers(null, "webapps-launch", JSON.stringify(appClone));
aOnSuccess();
},
close: function close(aApp) {
debug("close");
// We have to clone the app object as nsIDOMApplication objects are
// stringified as an empty object. (see bug 830376)
let appClone = AppsUtils.cloneAppObject(aApp);
Services.obs.notifyObservers(null, "webapps-close", JSON.stringify(appClone));
},
cancelDownload: function cancelDownload(aManifestURL, aError) {
debug("cancelDownload " + aManifestURL);
let error = aError || "DOWNLOAD_CANCELED";
let download = AppDownloadManager.get(aManifestURL);
if (!download) {
debug("Could not find a download for " + aManifestURL);
return;
}
let app = this.webapps[download.appId];
if (download.cacheUpdate) {
try {
download.cacheUpdate.cancel();
} catch (e) {
debug (e);
}
} else if (download.channel) {
try {
download.channel.cancel(Cr.NS_BINDING_ABORTED);
} catch(e) { }
} else {
return;
}
// Ensure we don't send additional errors for this download
app.isCanceling = true;
// Ensure this app can be downloaded again after canceling
app.downloading = false;
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: {
progress: 0,
installState: download.previousState,
downloading: false
},
error: error,
id: app.id
})
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloaderror",
manifestURL: app.manifestURL
});
});
AppDownloadManager.remove(aManifestURL);
},
startDownload: Task.async(function*(aManifestURL) {
debug("startDownload for " + aManifestURL);
let id = this._appIdForManifestURL(aManifestURL);
let app = this.webapps[id];
if (!app) {
debug("startDownload: No app found for " + aManifestURL);
throw new Error("NO_SUCH_APP");
}
if (app.downloading) {
debug("app is already downloading. Ignoring.");
throw new Error("APP_IS_DOWNLOADING");
}
// If the caller is trying to start a download but we have nothing to
// download, send an error.
if (!app.downloadAvailable) {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
error: "NO_DOWNLOAD_AVAILABLE",
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloaderror",
manifestURL: app.manifestURL
});
throw new Error("NO_DOWNLOAD_AVAILABLE");
}
// First of all, we check if the download is supposed to update an
// already installed application.
let isUpdate = (app.installState == "installed");
// An app download would only be triggered for two reasons: an app
// update or while retrying to download a previously failed or canceled
// instalation.
app.retryingDownload = !isUpdate;
// We need to get the update manifest here, not the webapp manifest.
// If this is an update, the update manifest is staged.
let file = FileUtils.getFile(DIRECTORY_NAME,
["webapps", id,
isUpdate ? "staged-update.webapp"
: "update.webapp"],
true);
if (!file.exists()) {
// This is a hosted app, let's check if it has an appcache
// and download it.
let results = yield this._readManifests([{ id: id }]);
let jsonManifest = results[0].manifest;
let manifest =
new ManifestHelper(jsonManifest, app.origin, app.manifestURL);
if (manifest.appcache_path) {
debug("appcache found");
this.startOfflineCacheDownload(manifest, app, null, isUpdate);
} else {
// Hosted app with no appcache, nothing to do, but we fire a
// downloaded event.
debug("No appcache found, sending 'downloaded' for " + aManifestURL);
app.downloadAvailable = false;
yield this._saveApps();
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
manifest: jsonManifest,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadsuccess",
manifestURL: aManifestURL
});
}
return;
}
let json = yield AppsUtils.loadJSONAsync(file.path);
if (!json) {
debug("startDownload: No update manifest found at " + file.path + " " +
aManifestURL);
throw new Error("MISSING_UPDATE_MANIFEST");
}
let manifest = new ManifestHelper(json, app.origin, app.manifestURL);
let newApp = {
manifestURL: aManifestURL,
origin: app.origin,
installOrigin: app.installOrigin,
downloadSize: app.downloadSize
};
let newManifest, newId;
try {
[newId, newManifest] = yield this.downloadPackage(id, app, manifest, newApp, isUpdate);
} catch (ex) {
this.revertDownloadPackage(id, app, newApp, isUpdate, ex);
throw ex;
}
// Success! Keep the zip in of TmpD, we'll move it out when
// applyDownload() will be called.
// Save the manifest in TmpD also
let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", newId,
"manifest.webapp");
yield this._writeFile(manFile, JSON.stringify(newManifest));
app = this.webapps[id];
// Set state and fire events.
app.downloading = false;
app.downloadAvailable = false;
app.readyToApplyDownload = true;
app.updateTime = Date.now();
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadsuccess",
manifestURL: aManifestURL
});
if (app.installState == "pending") {
// We restarted a failed download, apply it automatically.
this.applyDownload(aManifestURL);
}
}),
applyDownload: Task.async(function*(aManifestURL) {
debug("applyDownload for " + aManifestURL);
let id = this._appIdForManifestURL(aManifestURL);
let app = this.webapps[id];
if (!app) {
throw new Error("NO_SUCH_APP");
}
if (!app.readyToApplyDownload) {
throw new Error("NOT_READY_TO_APPLY_DOWNLOAD");
}
// We need to get the old manifest to unregister web activities.
let oldManifest = yield this.getManifestFor(aManifestURL);
// Move the application.zip and manifest.webapp files out of TmpD
let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
let manFile = tmpDir.clone();
manFile.append("manifest.webapp");
let appFile = tmpDir.clone();
appFile.append("application.zip");
// In order to better control the potential inconsistency due to unexpected
// shutdown during the update process, a separate folder is used to accommodate
// the updated files and to replace the current one. Some sanity check and
// correspondent rollback logic may be necessary during the initialization
// of this component to recover it at next system boot-up.
let oldDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id + ".new"], true, true);
appFile.moveTo(dir, "application.zip");
manFile.moveTo(dir, "manifest.webapp");
// Copy the staged update manifest to a non staged one.
let staged = oldDir.clone();
staged.append("staged-update.webapp");
// If we are applying after a restarted download, we have no
// staged update manifest.
if (staged.exists()) {
staged.copyTo(dir, "update.webapp");
}
oldDir.moveTo(null, id + ".old");
dir.moveTo(null, id);
try {
oldDir.remove(true);
} catch(e) {
oldDir.moveTo(tmpDir, "old." + app.updateTime);
}
try {
tmpDir.remove(true);
} catch(e) { }
// Clean up the deprecated manifest cache if needed.
if (id in this._manifestCache) {
delete this._manifestCache[id];
}
// Flush the zip reader cache to make sure we use the new application.zip
// when re-launching the application.
let zipFile = dir.clone();
zipFile.append("application.zip");
Services.obs.notifyObservers(zipFile, "flush-cache-entry", null);
// Get the manifest, and set properties.
let newManifest = yield this.getManifestFor(aManifestURL);
app.downloading = false;
app.downloadAvailable = false;
app.downloadSize = 0;
app.installState = "installed";
app.readyToApplyDownload = false;
// Update the staged properties.
if (app.staged) {
for (let prop in app.staged) {
app[prop] = app.staged[prop];
}
delete app.staged;
}
delete app.retryingDownload;
// Once updated we are not in the blocklist anymore.
app.blockedStatus = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
// Update the asm.js scripts we need to compile.
yield ScriptPreloader.preload(app, newManifest);
// Update langpack information.
Langpacks.register(app, newManifest);
yield this._saveApps();
// Update the handlers and permissions for this app.
this.updateAppHandlers(oldManifest, newManifest, app);
let updateManifest = yield AppsUtils.loadJSONAsync(staged.path);
let appObject = AppsUtils.cloneAppObject(app);
appObject.updateManifest = updateManifest;
this.notifyUpdateHandlers(appObject, newManifest, appFile.path);
if (supportUseCurrentProfile()) {
PermissionsInstaller.installPermissions(
{ manifest: newManifest,
origin: app.origin,
manifestURL: app.manifestURL },
true);
}
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
manifest: newManifest,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadapplied",
manifestURL: app.manifestURL
});
}),
startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
debug("startOfflineCacheDownload " + aApp.id + " " + aApp.kind);
if (aApp.kind !== this.kHostedAppcache || !aManifest.appcache_path) {
return;
}
debug("startOfflineCacheDownload " + aManifest.appcache_path);
// If the manifest has an appcache_path property, use it to populate the
// appcache.
let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(),
null, null);
let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null);
// We determine the app's 'installState' according to its previous
// state. Cancelled downloads should remain as 'pending'. Successfully
// installed apps should morph to 'updating'.
if (aIsUpdate) {
aApp.installState = "updating";
}
// We set the 'downloading' flag and update the apps registry right before
// starting the app download/update.
aApp.downloading = true;
aApp.progress = 0;
DOMApplicationRegistry._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
// Clear any previous errors.
error: null,
app: {
downloading: true,
installState: aApp.installState,
progress: 0
},
id: aApp.id
});
let appURI = NetUtil.newURI(aApp.origin, null, null);
let principal =
Services.scriptSecurityManager.createCodebasePrincipal(appURI,
{appId: aApp.localId});
let cacheUpdate = updateSvc.scheduleAppUpdate(
appcacheURI, docURI, principal, aProfileDir);
// We save the download details for potential further usage like
// cancelling it.
let download = {
cacheUpdate: cacheUpdate,
appId: this._appIdForManifestURL(aApp.manifestURL),
previousState: aIsUpdate ? "installed" : "pending"
};
AppDownloadManager.add(aApp.manifestURL, download);
cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
});
},
// Returns the MD5 hash of the manifest.
computeManifestHash: function(aManifest) {
return AppsUtils.computeHash(JSON.stringify(aManifest));
},
// Updates the redirect mapping, activities and system message handlers.
// aOldManifest can be null if we don't have any handler to unregister.
updateAppHandlers: function(aOldManifest, aNewManifest, aApp) {
debug("updateAppHandlers: old=" + uneval(aOldManifest) +
" new=" + uneval(aNewManifest));
this.notifyAppsRegistryStart();
if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects);
}
let manifest =
new ManifestHelper(aNewManifest, aApp.origin, aApp.manifestURL);
this._saveWidgetsFullPath(manifest, aApp);
aApp.role = manifest.role ? manifest.role : "";
if (supportSystemMessages()) {
if (aOldManifest) {
this._unregisterActivities(aOldManifest, aApp);
}
this._registerSystemMessages(aNewManifest, aApp);
this._registerActivities(aNewManifest, aApp, true);
} else {
// Nothing else to do but notifying we're ready.
this.notifyAppsRegistryReady();
}
// Update user customizations and langpacks.
if (aOldManifest) {
UserCustomizations.unregister(aApp);
Langpacks.unregister(aApp, aOldManifest);
}
UserCustomizations.register(aApp);
Langpacks.register(aApp, aNewManifest);
},
checkForUpdate: function(aData, aMm) {
debug("checkForUpdate for " + aData.manifestURL);
let sendError = (aError) => {
debug("checkForUpdate error " + aError);
aData.error = aError;
aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", this.formatMessage(aData));
};
let id = this._appIdForManifestURL(aData.manifestURL);
let app = this.webapps[id];
// We cannot update an app that does not exists.
if (!app) {
sendError("NO_SUCH_APP");
return;
}
// We cannot update an app that is not fully installed.
if (app.installState !== "installed") {
sendError("PENDING_APP_NOT_UPDATABLE");
return;
}
// We may be able to remove this when Bug 839071 is fixed.
if (app.downloading) {
sendError("APP_IS_DOWNLOADING");
return;
}
// If the app is packaged and its manifestURL has an app:// scheme,
// or if it's a native Android app then we can't have an update.
if (app.kind == this.kAndroid ||
(app.kind == this.kPackaged && app.manifestURL.startsWith("app://"))) {
sendError("NOT_UPDATABLE");
return;
}
// For non-removable hosted apps that lives in the core apps dir we
// only check the appcache because we can't modify the manifest even
// if it has changed.
let onlyCheckAppCache = false;
#ifdef MOZ_WIDGET_GONK
let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
onlyCheckAppCache = (app.basePath == appDir.path);
#endif
if (onlyCheckAppCache) {
// Bail out for packaged apps & hosted apps without appcache.
if (aApp.kind !== this.kHostedAppcache) {
sendError("NOT_UPDATABLE");
return;
}
// We need the manifest to get the appcache path.
this._readManifests([{ id: id }]).then((aResult) => {
debug("Checking only appcache for " + aData.manifestURL);
let manifest = aResult[0].manifest;
if (!manifest.appcache_path) {
sendError("NOT_UPDATABLE");
return;
}
// Check if the appcache is updatable, and send "downloadavailable" or
// "downloadapplied".
let updateObserver = {
observe: function(aSubject, aTopic, aObsData) {
debug("onlyCheckAppCache updateSvc.checkForUpdate return for " +
app.manifestURL + " - event is " + aTopic);
if (aTopic == "offline-cache-update-available") {
app.downloadAvailable = true;
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadavailable",
manifestURL: app.manifestURL,
requestID: aData.requestID
});
});
} else {
sendError("NOT_UPDATABLE");
}
}
};
let helper =
new ManifestHelper(manifest, aData.origin, aData.manifestURL);
debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " +
helper.fullAppcachePath());
let appURI = NetUtil.newURI(aApp.origin, null, null);
let principal =
Services.scriptSecurityManager.createCodebasePrincipal(appURI,
{appId: aApp.localId});
updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null),
principal, updateObserver);
});
return;
}
// On xhr load request event
function onload(xhr, oldManifest) {
debug("Got http status=" + xhr.status + " for " + aData.manifestURL);
let oldHash = app.manifestHash;
let isPackage = app.kind == DOMApplicationRegistry.kPackaged;
if (xhr.status == 200) {
let manifest = xhr.response;
if (manifest == null) {
sendError("MANIFEST_PARSE_ERROR");
return;
}
if (!AppsUtils.checkManifest(manifest, app)) {
sendError("INVALID_MANIFEST");
return;
} else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
sendError("INSTALL_FROM_DENIED");
return;
} else {
AppsUtils.ensureSameAppName(oldManifest, manifest, app);
let hash = this.computeManifestHash(manifest);
debug("Manifest hash = " + hash);
if (isPackage) {
if (!app.staged) {
app.staged = { };
}
app.staged.manifestHash = hash;
app.staged.etag = xhr.getResponseHeader("Etag");
} else {
app.manifestHash = hash;
app.etag = xhr.getResponseHeader("Etag");
}
app.lastCheckedUpdate = Date.now();
if (isPackage) {
if (oldHash != hash) {
this.updatePackagedApp(aData, id, app, manifest);
} else {
this._saveApps().then(() => {
// Like if we got a 304, just send a 'downloadapplied'
// or downloadavailable event.
let eventType = app.downloadAvailable ? "downloadavailable"
: "downloadapplied";
aMm.sendAsyncMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
aMm.sendAsyncMessage("Webapps:FireEvent", {
eventType: eventType,
manifestURL: app.manifestURL,
requestID: aData.requestID
});
});
}
} else {
// Update only the appcache if the manifest has not changed
// based on the hash value.
if (oldHash == hash) {
debug("Update - oldhash");
this.updateHostedApp(aData, id, app, oldManifest, null);
return;
}
// For hosted apps and hosted apps with appcache, use the
// manifest "as is".
this.updateHostedApp(aData, id, app, oldManifest, manifest);
}
}
} else if (xhr.status == 304) {
// The manifest has not changed.
if (isPackage) {
app.lastCheckedUpdate = Date.now();
this._saveApps().then(() => {
// If the app is a packaged app, we just send a 'downloadapplied'
// or downloadavailable event.
let eventType = app.downloadAvailable ? "downloadavailable"
: "downloadapplied";
aMm.sendAsyncMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
aMm.sendAsyncMessage("Webapps:FireEvent", {
eventType: eventType,
manifestURL: app.manifestURL,
requestID: aData.requestID
});
});
} else {
// For hosted apps, even if the manifest has not changed, we check
// for offline cache updates.
this.updateHostedApp(aData, id, app, oldManifest, null);
}
} else {
sendError("MANIFEST_URL_ERROR");
}
}
// Try to download a new manifest.
function doRequest(oldManifest, headers) {
headers = headers || [];
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", aData.manifestURL, true);
xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
if (xhr.channel.loadInfo) {
xhr.channel.loadInfo.originAttributes = { appId: app.installerAppId,
inIsolatedMozBrowser: app.installerIsBrowser
};
}
headers.forEach(function(aHeader) {
debug("Adding header: " + aHeader.name + ": " + aHeader.value);
xhr.setRequestHeader(aHeader.name, aHeader.value);
});
xhr.responseType = "json";
if (app.etag) {
debug("adding manifest etag:" + app.etag);
xhr.setRequestHeader("If-None-Match", app.etag);
}
xhr.channel.notificationCallbacks =
AppsUtils.createLoadContext(app.installerAppId, app.installerIsBrowser);
xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
xhr.addEventListener("error", (function() {
sendError("NETWORK_ERROR");
}).bind(this), false);
debug("Checking manifest at " + aData.manifestURL);
xhr.send(null);
}
// Read the current app manifest file
this._readManifests([{ id: id }]).then((aResult) => {
let extraHeaders = [];
#ifdef MOZ_WIDGET_GONK
let pingManifestURL;
try {
pingManifestURL = Services.prefs.getCharPref("ping.manifestURL");
} catch(e) { }
if (pingManifestURL && pingManifestURL == aData.manifestURL) {
// Get the device info.
let device = libcutils.property_get("ro.product.model");
extraHeaders.push({ name: "X-MOZ-B2G-DEVICE",
value: device || "unknown" });
}
#endif
doRequest.call(this, aResult[0].manifest, extraHeaders);
});
},
updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
debug("updatePackagedApp");
// Store the new update manifest.
let dir = this._getAppDir(aId).path;
let manFile = OS.Path.join(dir, "staged-update.webapp");
yield this._writeFile(manFile, JSON.stringify(aNewManifest));
let manifest =
new ManifestHelper(aNewManifest, aApp.origin, aApp.manifestURL);
// A package is available: set downloadAvailable to fire the matching
// event.
aApp.downloadAvailable = true;
aApp.downloadSize = manifest.size;
aApp.updateManifest = aNewManifest;
this._saveWidgetsFullPath(manifest, aApp);
yield this._saveApps();
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aApp,
id: aApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadavailable",
manifestURL: aApp.manifestURL,
requestID: aData.requestID
});
}),
// A hosted app is updated if the app manifest or the appcache needs
// updating. Even if the app manifest has not changed, we still check
// for changes in the app cache.
// 'aNewManifest' would contain the updated app manifest if
// it has actually been updated, while 'aOldManifest' contains the
// stored app manifest.
updateHostedApp: Task.async(function*(aData, aId, aApp, aOldManifest, aNewManifest) {
debug("updateHostedApp " + aData.manifestURL);
// Clean up the deprecated manifest cache if needed.
if (aId in this._manifestCache) {
delete this._manifestCache[aId];
}
aApp.manifest = aNewManifest || aOldManifest;
let manifest =
new ManifestHelper(aApp.manifest, aApp.origin, aApp.manifestURL);
aApp.role = manifest.role || "";
if (!AppsUtils.checkAppRole(aApp.role, aApp.appStatus)) {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aApp,
manifest: aApp.manifest,
id: aApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadapplied",
manifestURL: aApp.manifestURL,
requestID: aData.requestID
});
delete aApp.manifest;
return;
}
if (aNewManifest) {
this.updateAppHandlers(aOldManifest, aNewManifest, aApp);
this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest);
// Store the new manifest.
let dir = this._getAppDir(aId).path;
let manFile = OS.Path.join(dir, "manifest.webapp");
yield this._writeFile(manFile, JSON.stringify(aNewManifest));
manifest =
new ManifestHelper(aNewManifest, aApp.origin, aApp.manifestURL);
if (supportUseCurrentProfile()) {
// Update the permissions for this app.
PermissionsInstaller.installPermissions({
manifest: aApp.manifest,
origin: aApp.origin,
manifestURL: aData.manifestURL
}, true);
}
aApp.name = aNewManifest.name;
aApp.csp = manifest.csp || "";
aApp.updateTime = Date.now();
}
// Update the registry.
this.webapps[aId] = aApp;
yield this._saveApps();
if (aApp.kind !== this.kHostedAppcache || !aApp.manifest.appcache_path) {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aApp,
manifest: aApp.manifest,
id: aApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloadapplied",
manifestURL: aApp.manifestURL,
requestID: aData.requestID
});
} else {
// Check if the appcache is updatable, and send "downloadavailable" or
// "downloadapplied".
debug("updateHostedApp: updateSvc.checkForUpdate for " +
manifest.fullAppcachePath());
let updateDeferred = Promise.defer();
let appURI = NetUtil.newURI(aApp.origin, null, null);
let principal =
Services.scriptSecurityManager.createCodebasePrincipal(appURI,
{appId: aApp.localId});
updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null),
principal, (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic));
let topic = yield updateDeferred.promise;
debug("updateHostedApp: updateSvc.checkForUpdate return for " +
aApp.manifestURL + " - event is " + topic);
let eventType =
topic == "offline-cache-update-available" ? "downloadavailable"
: "downloadapplied";
aApp.downloadAvailable = (eventType == "downloadavailable");
yield this._saveApps();
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aApp,
manifest: aApp.manifest,
id: aApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: eventType,
manifestURL: aApp.manifestURL,
requestID: aData.requestID
});
}
delete aApp.manifest;
}),
// Downloads the manifest and run checks, then eventually triggers the
// installation UI.
doInstall: function doInstall(aData, aMm) {
let app = aData.app;
let sendError = (aError) => {
aData.error = aError;
aMm.sendAsyncMessage("Webapps:Install:Return:KO", this.formatMessage(aData));
Cu.reportError("Error installing app from: " + app.installOrigin +
": " + aError);
this.popContentAction(aData.oid);
};
if (app.receipts.length > 0) {
for (let receipt of app.receipts) {
let error = this.isReceipt(receipt);
if (error) {
sendError(error);
return;
}
}
}
// Hosted apps can't be trusted or certified, so just check that the
// manifest doesn't ask for those.
function checkAppStatus(aManifest) {
try {
// Everything is authorized in developer mode.
if (Services.prefs.getBoolPref("dom.apps.developer_mode")) {
return true;
}
} catch(e) {}
let manifestStatus = aManifest.type || "web";
return manifestStatus === "web" ||
manifestStatus === "trusted";
}
let checkManifest = (function() {
if (!app.manifest) {
sendError("MANIFEST_PARSE_ERROR");
return false;
}
// Disallow reinstalls from the same manifest url for now.
for (let id in this.webapps) {
if (this.webapps[id].manifestURL == app.manifestURL) {
sendError("REINSTALL_FORBIDDEN");
return false;
}
}
if (!AppsUtils.checkManifest(app.manifest, app)) {
sendError("INVALID_MANIFEST");
return false;
}
if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) {
sendError("INSTALL_FROM_DENIED");
return false;
}
if (!checkAppStatus(app.manifest)) {
sendError("INVALID_SECURITY_LEVEL");
return false;
}
app.role = app.manifest.role || "";
if (!AppsUtils.checkAppRole(app.role, app.appStatus)) {
sendError("INVALID_ROLE");
return false;
}
return true;
}).bind(this);
let installApp = (function() {
app.manifestHash = this.computeManifestHash(app.manifest);
// Check to see if the action has been cancelled in the interim.
let cancelled = this.actionCancelled(aData.oid);
this.popContentAction(aData.oid);
if (!cancelled) {
// We allow bypassing the install confirmation process to facilitate
// automation.
let prefName = "dom.mozApps.auto_confirm_install";
if (Services.prefs.prefHasUserValue(prefName) &&
Services.prefs.getBoolPref(prefName)) {
this.confirmInstall(aData);
} else {
Services.obs.notifyObservers(aMm, "webapps-ask-install",
JSON.stringify(aData));
}
}
}).bind(this);
// This action will be popped on success in installApp, or on
// failure in sendError.
this.pushContentAction(aData.oid);
// We may already have the manifest (e.g. AutoInstall),
// in which case we don't need to load it.
if (app.manifest) {
if (checkManifest()) {
debug("Installed manifest check OK");
installApp();
} else {
debug("Installed manifest check failed");
// checkManifest() sends error before return
}
return;
}
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", app.manifestURL, true);
xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
if (xhr.channel.loadInfo) {
xhr.channel.loadInfo.originAttributes = { appId: aData.appId,
inIsolatedMozBrowser: aData.isBrowser};
}
xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
aData.isBrowser);
xhr.responseType = "json";
xhr.addEventListener("load", (function() {
if (xhr.status == 200) {
if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
xhr.getResponseHeader("content-type"))) {
sendError("INVALID_MANIFEST_CONTENT_TYPE");
return;
}
app.manifest = xhr.response;
if (checkManifest()) {
debug("Downloaded manifest check OK");
app.etag = xhr.getResponseHeader("Etag");
installApp();
return;
} else {
debug("Downloaded manifest check failed");
// checkManifest() sends error before return
}
} else {
sendError("MANIFEST_URL_ERROR");
}
}).bind(this), false);
xhr.addEventListener("error", (function() {
sendError("NETWORK_ERROR");
}).bind(this), false);
xhr.send(null);
},
doInstallPackage: function doInstallPackage(aData, aMm) {
let app = aData.app;
let sendError = (aError) => {
aData.error = aError;
aMm.sendAsyncMessage("Webapps:Install:Return:KO", this.formatMessage(aData));
Cu.reportError("Error installing packaged app from: " +
app.installOrigin + ": " + aError);
this.popContentAction(aData.oid);
};
if (app.receipts.length > 0) {
for (let receipt of app.receipts) {
let error = this.isReceipt(receipt);
if (error) {
sendError(error);
return;
}
}
}
let checkUpdateManifest = (function() {
let manifest = app.updateManifest;
// Disallow reinstalls from the same manifest URL for now.
let id = this._appIdForManifestURL(app.manifestURL);
if (id !== null) {
sendError("REINSTALL_FORBIDDEN");
return false;
}
if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) {
sendError("INVALID_MANIFEST");
return false;
}
if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
sendError("INSTALL_FROM_DENIED");
return false;
}
return true;
}).bind(this);
let installApp = (function() {
app.manifestHash = this.computeManifestHash(app.updateManifest);
// Check to see if the action has been cancelled in the interim.
let cancelled = this.actionCancelled(aData.oid);
this.popContentAction(aData.oid);
if (!cancelled) {
// We allow bypassing the install confirmation process to facilitate
// automation.
let prefName = "dom.mozApps.auto_confirm_install";
if (Services.prefs.prefHasUserValue(prefName) &&
Services.prefs.getBoolPref(prefName)) {
this.confirmInstall(aData);
} else {
Services.obs.notifyObservers(aMm, "webapps-ask-install",
JSON.stringify(aData));
}
}
}).bind(this);
// This action will be popped on success in installApp, or on
// failure in sendError.
this.pushContentAction(aData.oid);
// We may already have the manifest (e.g. AutoInstall),
// in which case we don't need to load it.
if (app.updateManifest) {
if (checkUpdateManifest()) {
installApp();
}
return;
}
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", app.manifestURL, true);
xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
if (xhr.channel.loadInfo) {
xhr.channel.loadInfo.originAttributes = { appId: aData.appId,
inIsolatedMozBrowser: aData.isBrowser};
}
xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
aData.isBrowser);
xhr.responseType = "json";
xhr.addEventListener("load", (function() {
if (xhr.status == 200) {
if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
xhr.getResponseHeader("content-type"))) {
sendError("INVALID_MANIFEST_CONTENT_TYPE");
return;
}
app.updateManifest = xhr.response;
if (!app.updateManifest) {
sendError("MANIFEST_PARSE_ERROR");
return;
}
if (checkUpdateManifest()) {
app.etag = xhr.getResponseHeader("Etag");
debug("at install package got app etag=" + app.etag);
installApp();
}
}
else {
sendError("MANIFEST_URL_ERROR");
}
}).bind(this), false);
xhr.addEventListener("error", (function() {
sendError("NETWORK_ERROR");
}).bind(this), false);
xhr.send(null);
},
onLocationChange(oid) {
let action = this._contentActions.get(oid);
if (action) {
action.cancelled = true;
}
},
pushContentAction: function(windowID) {
let actions = this._contentActions.get(windowID);
if (!actions) {
actions = {
count: 0,
cancelled: false,
};
this._contentActions.set(windowID, actions);
}
actions.count++;
},
popContentAction: function(windowID) {
let actions = this._contentActions.get(windowID);
if (!actions) {
Cu.reportError(`Failed to pop content action for window with ID ${windowID}`);
return;
}
actions.count--;
if (!actions.count) {
this._contentActions.delete(windowID);
}
},
actionCancelled: function(windowID) {
return this._contentActions.has(windowID) &&
this._contentActions.get(windowID).cancelled;
},
denyInstall: function(aData) {
let packageId = aData.app.packageId;
if (packageId) {
let dir = FileUtils.getDir("TmpD", ["webapps", packageId],
true, true);
try {
dir.remove(true);
} catch(e) {
}
}
aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", this.formatMessage(aData));
},
// This function is called after we called the onsuccess callback on the
// content side. This let the webpage the opportunity to set event handlers
// on the app before we start firing progress events.
queuedDownload: {},
queuedPackageDownload: {},
onInstallSuccessAck: Task.async(function*(aManifestURL, aDontNeedNetwork) {
// If we are offline, register to run when we'll be online.
if ((Services.io.offline) && !aDontNeedNetwork) {
let onlineWrapper = {
observe: function(aSubject, aTopic, aData) {
Services.obs.removeObserver(onlineWrapper,
"network:offline-status-changed");
DOMApplicationRegistry.onInstallSuccessAck(aManifestURL);
}
};
Services.obs.addObserver(onlineWrapper,
"network:offline-status-changed", false);
return;
}
let cacheDownload = this.queuedDownload[aManifestURL];
if (cacheDownload) {
this.startOfflineCacheDownload(cacheDownload.manifest,
cacheDownload.app,
cacheDownload.profileDir);
delete this.queuedDownload[aManifestURL];
return;
}
let packageDownload = this.queuedPackageDownload[aManifestURL];
if (packageDownload) {
let manifest = packageDownload.manifest;
let newApp = packageDownload.app;
let installSuccessCallback = packageDownload.callback;
delete this.queuedPackageDownload[aManifestURL];
let id = this._appIdForManifestURL(newApp.manifestURL);
let oldApp = this.webapps[id];
let newManifest, newId;
try {
[newId, newManifest] = yield this.downloadPackage(id, oldApp, manifest, newApp, false);
yield this._onDownloadPackage(newApp, installSuccessCallback, newId, newManifest);
} catch (ex) {
this.revertDownloadPackage(id, oldApp, newApp, false, ex);
}
}
}),
_setupApp: function(aData, aId) {
let app = aData.app;
// app can be uninstalled by default.
if (app.removable === undefined) {
app.removable = true;
}
if (aData.isPackage) {
// Override the origin with the correct id.
app.origin = "app://" + aId;
}
app.id = aId;
app.installTime = Date.now();
app.lastUpdateCheck = Date.now();
return app;
},
_cloneApp: function(aData, aNewApp, aLocaleManifest, aManifest, aId, aLocalId) {
let appObject = AppsUtils.cloneAppObject(aNewApp);
appObject.appStatus =
aNewApp.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
let usesAppcache = appObject.kind == this.kHostedAppcache;
if (usesAppcache) {
appObject.installState = "pending";
appObject.downloadAvailable = true;
appObject.downloading = true;
appObject.downloadSize = 0;
appObject.readyToApplyDownload = false;
} else if (appObject.kind == this.kPackaged) {
appObject.installState = "pending";
appObject.downloadAvailable = true;
appObject.downloading = true;
appObject.downloadSize = aLocaleManifest.size;
appObject.readyToApplyDownload = false;
} else if (appObject.kind == this.kHosted ||
appObject.kind == this.kAndroid) {
appObject.installState = "installed";
appObject.downloadAvailable = false;
appObject.downloading = false;
appObject.readyToApplyDownload = false;
} else {
debug("Unknown app kind: " + appObject.kind);
throw Error("Unknown app kind: " + appObject.kind);
}
appObject.localId = aLocalId;
appObject.basePath = OS.Path.dirname(this.appsFile);
appObject.name = aManifest.name;
appObject.csp = aLocaleManifest.csp || "";
appObject.role = aLocaleManifest.role;
this._saveWidgetsFullPath(aLocaleManifest, appObject);
appObject.installerAppId = aData.appId;
appObject.installerIsBrowser = aData.isBrowser;
return appObject;
},
_writeManifestFile: function(aId, aIsPackage, aJsonManifest) {
debug("_writeManifestFile");
// For packaged apps, keep the update manifest distinct from the app manifest.
let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp";
let dir = this._getAppDir(aId).path;
let manFile = OS.Path.join(dir, manifestName);
return this._writeFile(manFile, JSON.stringify(aJsonManifest));
},
// Add an app that is already installed to the registry.
addInstalledApp: Task.async(function*(aApp, aManifest, aUpdateManifest) {
if (this.getAppLocalIdByManifestURL(aApp.manifestURL) !=
Ci.nsIScriptSecurityManager.NO_APP_ID) {
return;
}
let app = AppsUtils.cloneAppObject(aApp);
if (!AppsUtils.checkManifest(aManifest, app) ||
(aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) {
return;
}
app.name = aManifest.name;
app.csp = aManifest.csp || "";
let aLocaleManifest = new ManifestHelper(aManifest, app.origin, app.manifestURL);
this._saveWidgetsFullPath(aLocaleManifest, app);
app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
// Reuse the app ID if the scheme is "app".
let uri = Services.io.newURI(app.origin, null, null);
if (uri.scheme == "app") {
app.id = uri.host;
} else {
app.id = this.makeAppId();
}
app.localId = this._nextLocalId();
app.basePath = OS.Path.dirname(this.appsFile);
app.progress = 0.0;
app.installState = "installed";
app.downloadAvailable = false;
app.downloading = false;
app.readyToApplyDownload = false;
if (aUpdateManifest && aUpdateManifest.size) {
app.downloadSize = aUpdateManifest.size;
}
app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest ||
aManifest));
let zipFile = app.basePath + "/" + app.id;
app.packageHash = yield this._computeFileHash(zipFile);
app.role = aManifest.role || "";
if (!AppsUtils.checkAppRole(app.role, app.appStatus)) {
return;
}
app.redirects = this.sanitizeRedirects(aManifest.redirects);
this.webapps[app.id] = app;
// Store the manifest in the manifest cache, so we don't need to re-read it
this._manifestCache[app.id] = app.manifest;
// Store the manifest and the updateManifest.
this._writeManifestFile(app.id, false, aManifest);
if (aUpdateManifest) {
this._writeManifestFile(app.id, true, aUpdateManifest);
// If there is an id in the mini-manifest, use it for blocklisting purposes.
if (aData.isPackage && ("id" in aUpdateManifest)) {
this.webapps[app.id].blocklistId = aUpdateManifest["id"];
}
}
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:AddApp",
{ id: app.id, app: app, manifest: aManifest });
});
}),
confirmInstall: Task.async(function*(aData, aProfileDir, aInstallSuccessCallback) {
debug("confirmInstall");
let origin = Services.io.newURI(aData.app.origin, null, null);
let id = this._appIdForManifestURL(aData.app.manifestURL);
let manifestURL = origin.resolve(aData.app.manifestURL);
let localId = this.getAppLocalIdByManifestURL(manifestURL);
let isReinstall = false;
// Installing an application again is considered as an update.
if (id) {
isReinstall = true;
let dir = this._getAppDir(id);
try {
dir.remove(true);
} catch(e) { }
} else {
id = this.makeAppId();
localId = this._nextLocalId();
}
let app = this._setupApp(aData, id);
let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
yield this._writeManifestFile(id, aData.isPackage, jsonManifest);
// If there is an id in the mini-manifest, use it for blocklisting purposes.
if (aData.isPackage && ("id" in jsonManifest)) {
app.blocklistId = jsonManifest["id"];
}
debug("app.origin: " + app.origin);
let manifest =
new ManifestHelper(jsonManifest, app.origin, app.manifestURL);
// Set the application kind.
app.kind = this.appKind(app, manifest);
let appObject = this._cloneApp(aData, app, manifest, jsonManifest, id, localId);
this.webapps[id] = appObject;
this._manifestCache[id] = jsonManifest;
// For package apps, the permissions are not in the mini-manifest, so
// don't update the permissions yet.
if (!aData.isPackage) {
if (supportUseCurrentProfile()) {
try {
if (Services.prefs.getBoolPref("dom.apps.developer_mode")) {
this.webapps[id].appStatus =
AppsUtils.getAppManifestStatus(app.manifest);
}
} catch(e) {};
PermissionsInstaller.installPermissions(
{
origin: appObject.origin,
manifestURL: appObject.manifestURL,
manifest: jsonManifest,
kind: appObject.kind
},
isReinstall,
this.doUninstall.bind(this, aData, aData.mm)
);
}
}
for (let prop of ["installState", "downloadAvailable", "downloading",
"downloadSize", "readyToApplyDownload"]) {
aData.app[prop] = appObject[prop];
}
let dontNeedNetwork = false;
if (appObject.kind == this.kHostedAppcache && manifest.appcache_path) {
this.queuedDownload[app.manifestURL] = {
manifest: manifest,
app: appObject,
profileDir: aProfileDir
}
} else if (appObject.kind == this.kPackaged) {
// If it is a local app then it must been installed from a local file
// instead of web.
// In that case, we would already have the manifest, not just the update
// manifest.
#ifdef MOZ_WIDGET_ANDROID
dontNeedNetwork = !!aData.app.manifest;
#else
if (aData.app.localInstallPath) {
dontNeedNetwork = true;
jsonManifest.package_path = "file://" + aData.app.localInstallPath;
}
#endif
// origin for install apps is meaningless here, since it's app:// and this
// can't be used to resolve package paths.
manifest = new ManifestHelper(jsonManifest, app.origin, app.manifestURL);
this.queuedPackageDownload[app.manifestURL] = {
manifest: manifest,
app: appObject,
callback: aInstallSuccessCallback
};
}
// We notify about the successful installation via mgmt.oninstall and the
// corresponding DOMRequest.onsuccess event as soon as the app is properly
// saved in the registry.
yield this._saveApps();
aData.isPackage ? appObject.updateManifest = jsonManifest :
appObject.manifest = jsonManifest;
MessageBroadcaster.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
if (!aData.isPackage) {
this.updateAppHandlers(null, app.manifest, app);
if (aInstallSuccessCallback) {
yield aInstallSuccessCallback(app, app.manifest);
}
}
// The presence of a requestID means that we have a page to update.
if (aData.isPackage && aData.apkInstall && !aData.requestID) {
// Skip directly to onInstallSuccessAck, since there isn't
// a WebappsRegistry to receive Webapps:Install:Return:OK and respond
// Webapps:Install:Return:Ack when an app is being auto-installed.
this.onInstallSuccessAck(app.manifestURL);
} else {
// Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify
// the installing page about the successful install, after which it'll
// respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck.
MessageBroadcaster.broadcastMessage("Webapps:Install:Return:OK", aData);
}
Services.obs.notifyObservers(null, "webapps-installed",
JSON.stringify({ manifestURL: app.manifestURL }));
if (aData.forceSuccessAck) {
// If it's a local install, there's no content process so just
// ack the install.
this.onInstallSuccessAck(app.manifestURL, dontNeedNetwork);
}
}),
/**
* Install the package after successfully downloading it
*
* Bound params:
*
* @param aNewApp {Object} the new app data
* @param aInstallSuccessCallback {Function}
* the callback to call on install success
*
* Passed params:
*
* @param aId {Integer} the unique ID of the application
* @param aManifest {Object} The manifest of the application
*/
_onDownloadPackage: Task.async(function*(aNewApp, aInstallSuccessCallback,
aId, aManifest) {
debug("_onDownloadPackage");
// Success! Move the zip out of TmpD.
let app = this.webapps[aId];
let zipFile =
FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
let dir = this._getAppDir(aId);
zipFile.moveTo(dir, "application.zip");
let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
try {
tmpDir.remove(true);
} catch(e) { }
// Save the manifest
let manFile = OS.Path.join(dir.path, "manifest.webapp");
yield this._writeFile(manFile, JSON.stringify(aManifest));
// Set state and fire events.
app.installState = "installed";
app.downloading = false;
app.downloadAvailable = false;
yield this._saveApps();
this.updateAppHandlers(null, aManifest, aNewApp);
// Clear the manifest cache in case it holds the update manifest.
if (aId in this._manifestCache) {
delete this._manifestCache[aId];
}
MessageBroadcaster.broadcastMessage("Webapps:AddApp",
{ id: aId, app: aNewApp, manifest: aManifest });
Services.obs.notifyObservers(null, "webapps-installed",
JSON.stringify({ manifestURL: aNewApp.manifestURL }));
if (supportUseCurrentProfile()) {
// Update the permissions for this app.
PermissionsInstaller.installPermissions({
manifest: aManifest,
origin: aNewApp.origin,
manifestURL: aNewApp.manifestURL,
kind: this.webapps[aId].kind
}, true);
}
if (aInstallSuccessCallback) {
yield aInstallSuccessCallback(aNewApp, aManifest, zipFile.path);
}
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
manifest: aManifest,
manifestURL: aNewApp.manifestURL
});
// Check if we have asm.js code to preload for this application.
yield ScriptPreloader.preload(aNewApp, aManifest);
// Update langpack information.
yield Langpacks.register(aNewApp, aManifest);
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: ["downloadsuccess", "downloadapplied"],
manifestURL: aNewApp.manifestURL
});
}),
_nextLocalId: function() {
let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
while (this.getManifestURLByLocalId(id)) {
id++;
}
Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
Services.prefs.savePrefFile(null);
return id;
},
_appIdForManifestURL: function(aURI) {
for (let id in this.webapps) {
if (this.webapps[id].manifestURL == aURI)
return id;
}
return null;
},
makeAppId: function() {
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
return uuidGenerator.generateUUID().toString();
},
_saveApps: function() {
return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2));
},
/**
* Asynchronously reads a list of manifests
*/
_manifestCache: {},
_readManifests: function(aData) {
let manifestCache = this._manifestCache;
return Task.spawn(function*() {
for (let elem of aData) {
let id = elem.id;
if (!manifestCache[id]) {
// the manifest file used to be named manifest.json, so fallback on this.
let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath()
? "coreAppsDir" : DIRECTORY_NAME;
let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true);
let fileNames = ["manifest.webapp", "update.webapp", "manifest.json"];
for (let fileName of fileNames) {
manifestCache[id] = yield AppsUtils.loadJSONAsync(OS.Path.join(dir.path, fileName));
if (manifestCache[id]) {
break;
}
}
}
elem.manifest = manifestCache[id];
}
return aData;
}.bind(this)).then(null, Cu.reportError);
},
downloadPackage: Task.async(function*(aId, aOldApp, aManifest, aNewApp, aIsUpdate) {
// Here are the steps when installing a package:
// - create a temp directory where to store the app.
// - download the zip in this directory.
// - check the signature on the zip.
// - extract the manifest from the zip and check it.
// - ask confirmation to the user.
// - add the new app to the registry.
yield this._ensureSufficientStorage(aNewApp);
let fullPackagePath = aManifest.fullPackagePath();
// Check if it's a local file install (we've downloaded/sideloaded the
// package already, it existed on the build, or it came with an APK).
// Note that this variable also controls whether files signed with expired
// certificates are accepted or not. If isLocalFileInstall is true and the
// device date is earlier than the build generation date, then the signature
// will be accepted even if the certificate is expired.
let isLocalFileInstall =
Services.io.extractScheme(fullPackagePath) === 'file';
debug("About to download " + fullPackagePath);
let requestChannel = this._getRequestChannel(fullPackagePath,
isLocalFileInstall,
aOldApp,
aNewApp);
AppDownloadManager.add(
aNewApp.manifestURL,
{
channel: requestChannel,
appId: aId,
previousState: aIsUpdate ? "installed" : "pending"
}
);
// We set the 'downloading' flag to true right before starting the fetch.
aOldApp.downloading = true;
// We determine the app's 'installState' according to its previous
// state. Cancelled download should remain as 'pending'. Successfully
// installed apps should morph to 'updating'.
aOldApp.installState = aIsUpdate ? "updating" : "pending";
// initialize the progress to 0 right now
aOldApp.progress = 0;
// Save the current state of the app to handle cases where we may be
// retrying a past download.
yield DOMApplicationRegistry._saveApps();
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
// Clear any previous download errors.
error: null,
app: aOldApp,
id: aId
});
let zipFile = yield this._getPackage(requestChannel, aId, aOldApp, aNewApp);
// After this point, it's too late to cancel the download.
AppDownloadManager.remove(aNewApp.manifestURL);
let responseStatus = requestChannel.responseStatus;
let oldPackage = responseStatus == 304;
// If the response was 304 we probably won't have anything to hash.
let hash = null;
if (!oldPackage) {
hash = yield this._computeFileHash(zipFile.path);
}
oldPackage = oldPackage || (hash == aOldApp.packageHash);
if (oldPackage) {
debug("package's etag or hash unchanged; sending 'applied' event");
// The package's Etag or hash has not changed.
// We send an "applied" event right away so code awaiting that event
// can proceed to access the app. We also throw an error to alert
// the caller that the package wasn't downloaded.
this._sendAppliedEvent(aOldApp);
throw "PACKAGE_UNCHANGED";
}
let newManifest = yield this._openAndReadPackage(zipFile, aOldApp, aNewApp,
isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash);
return [aOldApp.id, newManifest];
}),
_ensureSufficientStorage: function(aNewApp) {
let deferred = Promise.defer();
let navigator = Services.wm.getMostRecentWindow(chromeWindowType)
.navigator;
let deviceStorage = null;
if (navigator.getDeviceStorage) {
deviceStorage = navigator.getDeviceStorage("apps");
}
if (deviceStorage) {
let req = deviceStorage.freeSpace();
req.onsuccess = req.onerror = e => {
let freeBytes = e.target.result;
let sufficientStorage = this._checkDownloadSize(freeBytes, aNewApp);
if (sufficientStorage) {
deferred.resolve();
} else {
deferred.reject("INSUFFICIENT_STORAGE");
}
}
} else {
debug("No deviceStorage");
// deviceStorage isn't available, so use FileUtils to find the size of
// available storage.
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true);
try {
let sufficientStorage = this._checkDownloadSize(dir.diskSpaceAvailable,
aNewApp);
if (sufficientStorage) {
deferred.resolve();
} else {
deferred.reject("INSUFFICIENT_STORAGE");
}
} catch(ex) {
// If disk space information isn't available, we'll end up here.
// We should proceed anyway, otherwise devices that support neither
// deviceStorage nor diskSpaceAvailable will never be able to install
// packaged apps.
deferred.resolve();
}
}
return deferred.promise;
},
_checkDownloadSize: function(aFreeBytes, aNewApp) {
if (aFreeBytes) {
debug("Free storage: " + aFreeBytes + ". Download size: " +
aNewApp.downloadSize);
if (aFreeBytes <=
aNewApp.downloadSize + AppDownloadManager.MIN_REMAINING_FREESPACE) {
return false;
}
}
return true;
},
_getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp,
aNewApp) {
let requestChannel;
let appURI = NetUtil.newURI(aNewApp.origin, null, null);
if (aIsLocalFileInstall) {
requestChannel = NetUtil.newChannel({
uri: aFullPackagePath,
loadUsingSystemPrincipal: true}
).QueryInterface(Ci.nsIFileChannel);
} else {
requestChannel = NetUtil.newChannel({
uri: aFullPackagePath,
loadUsingSystemPrincipal: true}
).QueryInterface(Ci.nsIHttpChannel);
requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
}
if (aOldApp.packageEtag && !aIsLocalFileInstall) {
debug("Add If-None-Match header: " + aOldApp.packageEtag);
requestChannel.setRequestHeader("If-None-Match", aOldApp.packageEtag,
false);
}
let lastProgressTime = 0;
requestChannel.notificationCallbacks = {
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsISupports) ||
aIID.equals(Ci.nsIProgressEventSink) ||
aIID.equals(Ci.nsILoadContext))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
getInterface: function(aIID) {
return this.QueryInterface(aIID);
},
onProgress: (function(aRequest, aContext, aProgress, aProgressMax) {
aOldApp.progress = aProgress;
let now = Date.now();
if (now - lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
debug("onProgress: " + aProgress + "/" + aProgressMax);
this._sendDownloadProgressEvent(aNewApp, aProgress);
lastProgressTime = now;
this._saveApps();
}
}).bind(this),
onStatus: function(aRequest, aContext, aStatus, aStatusArg) { },
// nsILoadContext
appId: aOldApp.installerAppId,
isInIsolatedMozBrowserElement: aOldApp.installerIsBrowser,
originAttributes: {
appId: aOldApp.installerAppId,
inIsolatedMozBrowser: aOldApp.installerIsBrowser
},
usePrivateBrowsing: false,
isContent: false,
associatedWindow: null,
topWindow : null,
isAppOfType: function(appType) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
}
};
return requestChannel;
},
_sendDownloadProgressEvent: function(aNewApp, aProgress) {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: {
progress: aProgress
},
id: aNewApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "progress",
manifestURL: aNewApp.manifestURL
});
},
_getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
let deferred = Promise.defer();
AppsUtils.getFile(aRequestChannel, aId, "application.zip").then((aFile) => {
deferred.resolve(aFile);
}, function(rejectStatus) {
debug("Failed to download package file: " + rejectStatus.msg);
if (!rejectStatus.downloadAvailable) {
aOldApp.downloadAvailable = false;
}
deferred.reject(rejectStatus.msg);
});
// send a first progress event to correctly set the DOM object's properties
this._sendDownloadProgressEvent(aNewApp, 0);
return deferred.promise;
},
/**
* Compute the MD5 hash of a file, doing async IO off the main thread.
*
* @param {String} aFilePath
* the path of the file to hash
* @returns {String} the MD5 hash of the file
*/
_computeFileHash: function(aFilePath) {
let deferred = Promise.defer();
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(aFilePath);
NetUtil.asyncFetch({
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true
}, function(inputStream, status) {
if (!Components.isSuccessCode(status)) {
debug("Error reading " + aFilePath + ": " + e);
deferred.reject();
return;
}
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
// We want to use the MD5 algorithm.
hasher.init(hasher.MD5);
const PR_UINT32_MAX = 0xffffffff;
hasher.updateFromStream(inputStream, PR_UINT32_MAX);
// Return the two-digit hexadecimal code for a byte.
function toHexString(charCode) {
return ("0" + charCode.toString(16)).slice(-2);
}
// We're passing false to get the binary hash and not base64.
let data = hasher.finish(false);
// Convert the binary hash data to a hex string.
let hash = Array.from(data, (c, i) => toHexString(data.charCodeAt(i))).join("");
debug("File hash computed: " + hash);
deferred.resolve(hash);
});
return deferred.promise;
},
/**
* Send an "applied" event right away for the package being installed.
*
* XXX We use this to exit the app update process early when the downloaded
* package is identical to the last one we installed. Presumably we do
* something similar after updating the app, and we could refactor both cases
* to use the same code to send the "applied" event.
*
* @param aApp {Object} app data
*/
_sendAppliedEvent: function(aApp) {
aApp.downloading = false;
aApp.downloadAvailable = false;
aApp.downloadSize = 0;
aApp.installState = "installed";
aApp.readyToApplyDownload = false;
if (aApp.staged && aApp.staged.manifestHash) {
// If we're here then the manifest has changed but the package
// hasn't. Let's clear this, so we don't keep offering
// a bogus update to the user
aApp.manifestHash = aApp.staged.manifestHash;
aApp.etag = aApp.staged.etag || aApp.etag;
aApp.staged = {};
// Move the staged update manifest to a non staged one.
try {
let staged = this._getAppDir(aApp.id);
staged.append("staged-update.webapp");
staged.moveTo(staged.parent, "update.webapp");
} catch (ex) {
// We don't really mind much if this fails.
}
}
// Save the updated registry, and cleanup the tmp directory.
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aApp,
id: aApp.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
manifestURL: aApp.manifestURL,
eventType: ["downloadsuccess", "downloadapplied"]
});
});
let file = FileUtils.getFile("TmpD", ["webapps", aApp.id], false);
if (file && file.exists()) {
file.remove(true);
}
},
_openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall,
aIsUpdate, aManifest, aRequestChannel, aHash) {
return Task.spawn((function*() {
let zipReader, isSigned, newManifest;
try {
[zipReader, isSigned] = yield this._openPackage(aZipFile, aOldApp,
aIsLocalFileInstall);
newManifest = yield this._readPackage(aOldApp, aNewApp,
aIsLocalFileInstall, aIsUpdate, aManifest, aRequestChannel,
aHash, zipReader, isSigned);
} catch (e) {
debug("package open/read error: " + e);
// Something bad happened when opening/reading the package.
// Unrecoverable error, don't bug the user.
// Apps with installState 'pending' does not produce any
// notification, so we are safe with its current
// downloadAvailable state.
if (aOldApp.installState !== "pending") {
aOldApp.downloadAvailable = false;
}
if (typeof e == 'object') {
Cu.reportError("Error while reading package: " + e + "\n" + e.stack);
throw "INVALID_PACKAGE";
} else {
throw e;
}
} finally {
if (zipReader) {
zipReader.close();
}
}
return newManifest;
}).bind(this));
},
_openPackage: function(aZipFile, aApp, aIsLocalFileInstall) {
return Task.spawn((function*() {
let certDb;
try {
certDb = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
} catch (e) {
debug("nsIX509CertDB error: " + e);
// unrecoverable error, don't bug the user
aApp.downloadAvailable = false;
throw "CERTDB_ERROR";
}
let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin,
aApp.manifestURL,
aZipFile,
certDb);
// We cannot really know if the system date is correct or
// not. What we can know is if it's after the build date or not,
// and assume the build date is correct (which we cannot
// really know either).
let isLaterThanBuildTime = Date.now() > PLATFORM_BUILD_ID_TIME;
let isSigned;
if (Components.isSuccessCode(result)) {
isSigned = true;
} else if (result == Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY ||
result == Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY ||
result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING) {
throw "APP_PACKAGE_CORRUPTED";
} else if (result == Cr.NS_ERROR_FILE_CORRUPTED ||
result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE ||
result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID ||
result == Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID) {
throw "APP_PACKAGE_INVALID";
} else if ((!aIsLocalFileInstall || isLaterThanBuildTime) &&
(result != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)) {
throw "INVALID_SIGNATURE";
} else {
// If it's a localFileInstall and the validation failed
// because of a expired certificate, just assume it was valid
// and that the error occurred because the system time has not
// been set yet.
isSigned = (aIsLocalFileInstall &&
(getNSPRErrorCode(result) ==
SEC_ERROR_EXPIRED_CERTIFICATE));
zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
.createInstance(Ci.nsIZipReader);
zipReader.open(aZipFile);
}
return [zipReader, isSigned];
}).bind(this));
},
_openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) {
let deferred = Promise.defer();
let root = TrustedRootCertificate.index;
let useReviewerCerts = false;
try {
useReviewerCerts = Services.prefs.
getBoolPref("dom.mozApps.use_reviewer_certs");
} catch (ex) { }
// We'll use the reviewer and dev certificates only if the pref is set to
// true.
if (useReviewerCerts) {
let manifestPath = Services.io.newURI(aManifestURL, null, null).path;
let isReviewer = false;
// There are different reviewer paths for apps & addons so we keep
// them in a comma separated preference.
try {
let reviewerPaths =
Services.prefs.getCharPref("dom.apps.reviewer_paths").split(",");
isReviewer = reviewerPaths.some(path => { return manifestPath.startsWith(path); });
} catch(e) {}
switch (aInstallOrigin) {
case "https://marketplace.firefox.com":
root = isReviewer
? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot
: Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot;
break;
case "https://marketplace-dev.allizom.org":
root = isReviewer
? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot
: Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot;
break;
// The staging server uses the same certificate for both
// public and unreviewed apps.
case "https://marketplace.allizom.org":
root = Ci.nsIX509CertDB.AppMarketplaceStageRoot;
break;
}
}
aCertDb.openSignedAppFileAsync(
root, aZipFile,
function(aRv, aZipReader) {
deferred.resolve([aRv, aZipReader]);
}
);
return deferred.promise;
},
_readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate,
aManifest, aRequestChannel, aHash, aZipReader,
aIsSigned) {
this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall);
// Chrome-style extensions only have a manifest.json manifest.
// In this case we extract it, and convert it to a minimal
// manifest.webapp manifest.
// Packages that contain both manifest.webapp and manifest.json
// are considered as apps, not extensions.
let hasWebappManifest = aZipReader.hasEntry("manifest.webapp");
let hasJsonManifest = aZipReader.hasEntry("manifest.json");
if (!hasWebappManifest && !hasJsonManifest) {
throw "MISSING_MANIFEST";
}
let istream =
aZipReader.getInputStream(hasWebappManifest ? "manifest.webapp"
: "manifest.json");
// Obtain a converter to read from a UTF-8 encoded input stream.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let newManifest = JSON.parse(converter.ConvertToUnicode(
NetUtil.readInputStreamToString(istream, istream.available()) || ""));
if (!hasWebappManifest) {
// Validate the extension manifest, and convert it.
if (!UserCustomizations.checkExtensionManifest(newManifest)) {
throw "INVALID_MANIFEST";
}
newManifest = UserCustomizations.convertManifest(newManifest);
// Keep track of the add-on version, to use for blocklisting.
if (newManifest.version) {
aNewApp.extensionVersion = newManifest.version;
}
}
if (!AppsUtils.checkManifest(newManifest, aOldApp)) {
throw "INVALID_MANIFEST";
}
// For app updates we don't forbid apps to rename themselves but
// we still retain the old name of the app. In the future we
// will use UI to allow updates to rename an app after we check
// with the user that the rename is ok.
if (aIsUpdate) {
// Call ensureSameAppName before compareManifests as `manifest`
// has been normalized to avoid app rename.
AppsUtils.ensureSameAppName(aManifest._manifest, newManifest, aOldApp);
}
if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) {
throw "MANIFEST_MISMATCH";
}
if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) {
throw "INSTALL_FROM_DENIED";
}
// Local file installs can be privileged even without the signature.
let maxStatus = aIsSigned || aIsLocalFileInstall
? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
: Ci.nsIPrincipal.APP_STATUS_INSTALLED;
try {
// Anything is possible in developer mode.
if (Services.prefs.getBoolPref("dom.apps.developer_mode")) {
maxStatus = Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
}
} catch(e) {};
let allowUnsignedLangpack = false;
try {
allowUnsignedLangpack =
Services.prefs.getBoolPref("dom.apps.allow_unsigned_langpacks") ||
Services.prefs.getBoolPref("dom.apps.developer_mode");
} catch(e) {}
let isLangPack = newManifest.role === "langpack" &&
(aIsSigned || allowUnsignedLangpack);
let isAddon = newManifest.role === "addon" &&
(aIsSigned || AppsUtils.allowUnsignedAddons);
let status = AppsUtils.getAppManifestStatus(newManifest);
if (status > maxStatus && !isLangPack && !isAddon) {
throw "INVALID_SECURITY_LEVEL";
}
// Check if the role is allowed for this app.
if (!AppsUtils.checkAppRole(newManifest.role, status)) {
throw "INVALID_ROLE";
}
this._saveEtag(aIsUpdate, aOldApp, aRequestChannel, aHash, newManifest);
this._checkOrigin(aIsSigned || aIsLocalFileInstall, aOldApp, newManifest,
aIsUpdate);
this._getIds(aIsSigned, aZipReader, converter, aNewApp, aOldApp, aIsUpdate);
return newManifest;
},
_checkSignature: function(aApp, aIsSigned, aIsLocalFileInstall) {
// XXX Security: You CANNOT safely add a new app store for
// installing privileged apps just by modifying this pref and
// adding the signing cert for that store to the cert trust
// database. *Any* origin listed can install apps signed with
// *any* certificate trusted; we don't try to maintain a strong
// association between certificate with installOrign. The
// expectation here is that in production builds the pref will
// contain exactly one origin. However, in custom development
// builds it may contain more than one origin so we can test
// different stages (dev, staging, prod) of the same app store.
//
// Only allow signed apps to be installed from a whitelist of
// domains, and require all packages installed from any of the
// domains on the whitelist to be signed. This is a stopgap until
// we have a real story for handling multiple app stores signing
// apps.
let signedAppOriginsStr =
Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from");
// If it's a local install and it's signed then we assume
// the app origin is a valid signer.
let isSignedAppOrigin = (aIsSigned && aIsLocalFileInstall) ||
signedAppOriginsStr.split(",").
indexOf(aApp.installOrigin) > -1;
if (!aIsSigned && isSignedAppOrigin) {
// Packaged apps installed from these origins must be signed;
// if not, assume somebody stripped the signature.
throw "INVALID_SIGNATURE";
} else if (aIsSigned && !isSignedAppOrigin) {
// Other origins are *prohibited* from installing signed apps.
// One reason is that our app revocation mechanism requires
// strong cooperation from the host of the mini-manifest, which
// we assume to be under the control of the install origin,
// even if it has a different origin.
throw "INSTALL_FROM_DENIED";
}
},
_saveEtag: function(aIsUpdate, aOldApp, aRequestChannel, aHash, aManifest) {
// Save the new Etag for the package.
if (aIsUpdate) {
if (!aOldApp.staged) {
aOldApp.staged = { };
}
try {
aOldApp.staged.packageEtag = aRequestChannel.getResponseHeader("Etag");
} catch(e) { }
aOldApp.staged.packageHash = aHash;
aOldApp.staged.appStatus = AppsUtils.getAppManifestStatus(aManifest);
} else {
try {
aOldApp.packageEtag = aRequestChannel.getResponseHeader("Etag");
} catch(e) { }
aOldApp.packageHash = aHash;
aOldApp.appStatus = AppsUtils.getAppManifestStatus(aManifest);
}
},
_checkOrigin: function(aIsSigned, aOldApp, aManifest, aIsUpdate) {
// Check if the app declares which origin it will use.
if (aIsSigned &&
aOldApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
aManifest.origin !== undefined) {
let uri;
try {
uri = Services.io.newURI(aManifest.origin, null, null);
} catch(e) {
throw "INVALID_ORIGIN";
}
if (uri.scheme != "app") {
throw "INVALID_ORIGIN";
}
if (aIsUpdate) {
// Changing the origin during an update is not allowed.
if (uri.prePath != aOldApp.origin) {
throw "INVALID_ORIGIN_CHANGE";
}
// Nothing else to do for an update... since the
// origin can't change we don't need to move the
// app nor can we have a duplicated origin
} else {
debug("Setting origin to " + uri.prePath +
" for " + aOldApp.manifestURL);
let newId = uri.prePath.substring(6); // "app://".length
if (newId in this.webapps) {
throw "DUPLICATE_ORIGIN";
}
aOldApp.origin = uri.prePath;
// Update the registry.
let oldId = aOldApp.id;
if (oldId == newId) {
// This could happen when we have an app in the registry
// that is not launchable. Since the app already has
// the correct id, we don't need to change it.
return;
}
aOldApp.id = newId;
this.webapps[newId] = aOldApp;
delete this.webapps[oldId];
// Rename the directories where the files are installed.
[DIRECTORY_NAME, "TmpD"].forEach(function(aDir) {
let parent = FileUtils.getDir(aDir, ["webapps"], true, true);
let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true);
dir.moveTo(parent, newId);
});
// Signals that we need to swap the old id with the new app.
MessageBroadcaster.broadcastMessage("Webapps:UpdateApp", { oldId: oldId,
newId: newId,
app: aOldApp });
}
}
},
_getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp,
aIsUpdate) {
// Get ids.json if the file is signed
if (aIsSigned) {
let idsStream;
try {
idsStream = aZipReader.getInputStream("META-INF/ids.json");
} catch (e) {
throw aZipReader.hasEntry("META-INF/ids.json")
? e
: "MISSING_IDS_JSON";
}
let ids = JSON.parse(aConverter.ConvertToUnicode(NetUtil.
readInputStreamToString( idsStream, idsStream.available()) || ""));
if ((!ids.id) || !Number.isInteger(ids.version) ||
(ids.version <= 0)) {
throw "INVALID_IDS_JSON";
}
let storeId = aNewApp.installOrigin + "#" + ids.id;
this._checkForStoreIdMatch(aIsUpdate, aOldApp, storeId, ids.version);
aOldApp.storeId = storeId;
aOldApp.storeVersion = ids.version;
}
},
// aStoreId must be a string of the form
// <installOrigin>#<storeId from ids.json>
// aStoreVersion must be a positive integer.
_checkForStoreIdMatch: function(aIsUpdate, aNewApp, aStoreId, aStoreVersion) {
// Things to check:
// 1. if it's a update:
// a. We should already have this storeId, or the original storeId must
// start with STORE_ID_PENDING_PREFIX
// b. The manifestURL for the stored app should be the same one we're
// updating
// c. And finally the version of the update should be higher than the one
// on the already installed package
// 2. else
// a. We should not have this storeId on the list
// We're currently launching WRONG_APP_STORE_ID for all the mismatch kind of
// errors, and APP_STORE_VERSION_ROLLBACK for the version error.
// Does an app with this storeID exist already?
let appId = this.getAppLocalIdByStoreId(aStoreId);
let isInstalled = appId != Ci.nsIScriptSecurityManager.NO_APP_ID;
if (aIsUpdate) {
let isDifferent = aNewApp.localId !== appId;
let isPending = aNewApp.storeId.indexOf(STORE_ID_PENDING_PREFIX) == 0;
if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) {
throw "WRONG_APP_STORE_ID";
}
if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) {
throw "APP_STORE_VERSION_ROLLBACK";
}
} else if (isInstalled) {
throw "WRONG_APP_STORE_ID";
}
},
// Removes the directory we created, and sends an error to the DOM side.
revertDownloadPackage: function(aId, aOldApp, aNewApp, aIsUpdate, aError) {
debug("Error downloading package: " + aError);
let dir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
try {
dir.remove(true);
} catch (e) { }
// We avoid notifying the error to the DOM side if the app download
// was cancelled via cancelDownload, which already sends its own
// notification.
if (aOldApp.isCanceling) {
delete aOldApp.isCanceling;
return;
}
// If the error that got us here was that the package hasn't changed,
// since we already sent a success and an applied, let's not confuse
// the clients...
if (aError == "PACKAGE_UNCHANGED") {
return;
}
let download = AppDownloadManager.get(aNewApp.manifestURL);
aOldApp.downloading = false;
// If there were not enough storage to download the package we
// won't have a record of the download details, so we just set the
// installState to 'pending' at first download and to 'installed' when
// updating.
aOldApp.installState = download ? download.previousState
: aIsUpdate ? "installed"
: "pending";
// Erase the .staged properties only if there's no download available
// anymore.
if (!aOldApp.downloadAvailable && aOldApp.staged) {
delete aOldApp.staged;
}
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: aOldApp,
error: aError,
id: aId
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloaderror",
manifestURL: aNewApp.manifestURL
});
});
AppDownloadManager.remove(aNewApp.manifestURL);
},
doUninstall: Task.async(function*(aData, aMm) {
let response = "Webapps:Uninstall:Return:OK";
try {
aData.app = yield this._getAppWithManifest(aData.manifestURL);
if (this.kAndroid == aData.app.kind) {
debug("Uninstalling android app " + aData.app.origin);
let [packageName, className] =
AndroidUtils.getPackageAndClassFromManifestURL(aData.manifestURL);
Messaging.sendRequest({ type: "Apps:Uninstall",
packagename: packageName,
classname: className });
// We have to wait for Android's uninstall before sending the
// uninstall event, so fake an error here.
response = "Webapps:Uninstall:Return:KO";
} else {
let prefName = "dom.mozApps.auto_confirm_uninstall";
if (Services.prefs.prefHasUserValue(prefName) &&
Services.prefs.getBoolPref(prefName)) {
yield this._uninstallApp(aData.app);
} else {
yield this._promptForUninstall(aData);
}
}
} catch (error) {
aData.error = error;
response = "Webapps:Uninstall:Return:KO";
}
aMm.sendAsyncMessage(response, this.formatMessage(aData));
}),
uninstall: function(aManifestURL) {
return this._getAppWithManifest(aManifestURL)
.then(this._uninstallApp.bind(this));
},
_uninstallApp: Task.async(function*(aApp) {
if (!aApp.removable) {
debug("Error: cannot uninstall a non-removable app.");
throw new Error("NON_REMOVABLE_APP");
}
let id = aApp.id;
// Check if we are downloading something for this app, and cancel the
// download if needed.
this.cancelDownload(aApp.manifestURL);
// Clean up the deprecated manifest cache if needed.
if (id in this._manifestCache) {
delete this._manifestCache[id];
}
// Clear private data first.
this._clearPrivateData(aApp.localId, false);
// Then notify observers.
Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(aApp));
if (supportSystemMessages()) {
this._unregisterActivities(aApp.manifest, aApp);
}
UserCustomizations.unregister(aApp);
Langpacks.unregister(aApp, aApp.manifest);
let dir = this._getAppDir(id);
try {
dir.remove(true);
} catch (e) {}
delete this.webapps[id];
yield this._saveApps();
MessageBroadcaster.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", aApp);
MessageBroadcaster.broadcastMessage("Webapps:RemoveApp", { id: id });
return aApp;
}),
_promptForUninstall: function(aData) {
let deferred = Promise.defer();
this._pendingUninstalls[aData.requestID] = deferred;
Services.obs.notifyObservers(null, "webapps-ask-uninstall",
JSON.stringify(aData));
return deferred.promise;
},
confirmUninstall: function(aData) {
let pending = this._pendingUninstalls[aData.requestID];
if (pending) {
delete this._pendingUninstalls[aData.requestID];
return this._uninstallApp(aData.app).then(() => {
pending.resolve();
return aData.app;
});
}
return Promise.reject(new Error("PENDING_UNINSTALL_NOT_FOUND"));
},
denyUninstall: function(aData, aReason = "ERROR_UNKNOWN_FAILURE") {
// Fails to uninstall the desired app because:
// - we cannot find the app to be uninstalled.
// - the app to be uninstalled is not removable.
// - the user declined the confirmation
debug("Failed to uninstall app: " + aReason);
let pending = this._pendingUninstalls[aData.requestID];
if (pending) {
delete this._pendingUninstalls[aData.requestID];
pending.reject(new Error(aReason));
return Promise.resolve();
}
return Promise.reject(new Error("PENDING_UNINSTALL_NOT_FOUND"));
},
getSelf: function(aData, aMm) {
aData.apps = [];
if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", this.formatMessage(aData));
return;
}
let tmp = [];
for (let id in this.webapps) {
if (this.webapps[id].origin == aData.origin &&
this.webapps[id].localId == aData.appId) {
let app = AppsUtils.cloneAppObject(this.webapps[id]);
aData.apps.push(app);
tmp.push({ id: id });
break;
}
}
if (!aData.apps.length) {
aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", this.formatMessage(aData));
return;
}
this._readManifests(tmp).then((aResult) => {
for (let i = 0; i < aResult.length; i++)
aData.apps[i].manifest = aResult[i].manifest;
aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", this.formatMessage(aData));
});
},
checkInstalled: function(aData, aMm) {
aData.app = null;
let tmp = [];
for (let appId in this.webapps) {
if (this.webapps[appId].manifestURL == aData.manifestURL) {
aData.app = AppsUtils.cloneAppObject(this.webapps[appId]);
tmp.push({ id: appId });
break;
}
}
this._readManifests(tmp).then((aResult) => {
for (let i = 0; i < aResult.length; i++) {
aData.app.manifest = aResult[i].manifest;
break;
}
aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", this.formatMessage(aData));
});
},
getInstalled: function(aData, aMm) {
aData.apps = [];
let tmp = [];
for (let id in this.webapps) {
if (this.webapps[id].installOrigin == aData.origin) {
aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
tmp.push({ id: id });
}
}
this._readManifests(tmp).then((aResult) => {
for (let i = 0; i < aResult.length; i++)
aData.apps[i].manifest = aResult[i].manifest;
aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", this.formatMessage(aData));
});
},
getIcon: function(aData, aMm) {
let sendError = (aError) => {
debug("getIcon error: " + aError);
aData.error = aError;
aMm.sendAsyncMessage("Webapps:GetIcon:Return", this.formatMessage(aData));
};
let app = this.getAppByManifestURL(aData.manifestURL);
if (!app) {
sendError("NO_APP");
return;
}
function loadIcon(aUrl) {
let fallbackMimeType = aUrl.indexOf('.') >= 0 ?
"image/" + aUrl.split(".").reverse()[0] : "";
// Set up an xhr to download a blob.
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.mozBackgroundRequest = true;
xhr.open("GET", aUrl, true);
xhr.responseType = "blob";
xhr.addEventListener("load", function() {
debug("Got http status=" + xhr.status + " for " + aUrl);
if (xhr.status == 200) {
let blob = xhr.response;
// Reusing aData with sendAsyncMessage() leads to an empty blob in
// the child.
let payload = {
"oid": aData.oid,
"requestID": aData.requestID,
"blob": blob,
"type": xhr.getResponseHeader("Content-Type") || fallbackMimeType
};
aMm.sendAsyncMessage("Webapps:GetIcon:Return", payload);
} else if (xhr.status === 0) {
sendError("NETWORK_ERROR");
} else {
sendError("FETCH_ICON_FAILED");
}
});
xhr.addEventListener("error", function() {
sendError("FETCH_ICON_FAILED");
});
xhr.send();
}
// Get the manifest, to find the icon url in the current locale.
this.getManifestFor(aData.manifestURL, aData.entryPoint)
.then((aManifest) => {
if (!aManifest) {
sendError("FETCH_ICON_FAILED");
return;
}
let manifest = new ManifestHelper(aManifest, app.origin, app.manifestURL);
let url = manifest.iconURLForSize(aData.iconID);
if (!url) {
sendError("NO_ICON");
return;
}
loadIcon(url);
}).catch(() => {
sendError("FETCH_ICON_FAILED");
return;
});
},
/* Check if |data| is actually a receipt */
isReceipt: function(data) {
try {
// The receipt data shouldn't be too big (allow up to 1 MiB of data)
const MAX_RECEIPT_SIZE = 1048576;
if (data.length > MAX_RECEIPT_SIZE) {
return "RECEIPT_TOO_BIG";
}
// Marketplace receipts are JWK + "~" + JWT
// Other receipts may contain only the JWT
let receiptParts = data.split('~');
let jwtData = null;
if (receiptParts.length == 2) {
jwtData = receiptParts[1];
} else {
jwtData = receiptParts[0];
}
let segments = jwtData.split('.');
if (segments.length != 3) {
return "INVALID_SEGMENTS_NUMBER";
}
let jwtBuffer = ChromeUtils.base64URLDecode(segments[1], {
// JWT/JWS prohibits padding per RFC 7515, section 2.
padding: "reject",
});
let textDecoder = new TextDecoder("utf-8");
let decodedReceipt = JSON.parse(textDecoder.decode(jwtBuffer));
if (!decodedReceipt) {
return "INVALID_RECEIPT_ENCODING";
}
// Required values for a receipt
if (!decodedReceipt.typ) {
return "RECEIPT_TYPE_REQUIRED";
}
if (!decodedReceipt.product) {
return "RECEIPT_PRODUCT_REQUIRED";
}
if (!decodedReceipt.user) {
return "RECEIPT_USER_REQUIRED";
}
if (!decodedReceipt.iss) {
return "RECEIPT_ISS_REQUIRED";
}
if (!decodedReceipt.nbf) {
return "RECEIPT_NBF_REQUIRED";
}
if (!decodedReceipt.iat) {
return "RECEIPT_IAT_REQUIRED";
}
let allowedTypes = [ "purchase-receipt", "developer-receipt",
"reviewer-receipt", "test-receipt" ];
if (allowedTypes.indexOf(decodedReceipt.typ) < 0) {
return "RECEIPT_TYPE_UNSUPPORTED";
}
} catch (e) {
return "RECEIPT_ERROR";
}
return null;
},
addReceipt: function(aData, aMm) {
debug("addReceipt " + aData.manifestURL);
let receipt = aData.receipt;
if (!receipt) {
aData.error = "INVALID_PARAMETERS";
aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", this.formatMessage(aData));
return;
}
let error = this.isReceipt(receipt);
if (error) {
aData.error = error;
aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", this.formatMessage(aData));
return;
}
let id = this._appIdForManifestURL(aData.manifestURL);
let app = this.webapps[id];
if (!app.receipts) {
app.receipts = [];
} else if (app.receipts.length > 500) {
aData.error = "TOO_MANY_RECEIPTS";
aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", this.formatMessage(aData));
return;
}
let index = app.receipts.indexOf(receipt);
if (index >= 0) {
aData.error = "RECEIPT_ALREADY_EXISTS";
aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", this.formatMessage(aData));
return;
}
app.receipts.push(receipt);
this._saveApps().then(() => {
aData.receipts = app.receipts;
aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", this.formatMessage(aData));
});
},
removeReceipt: function(aData, aMm) {
debug("removeReceipt " + aData.manifestURL);
let receipt = aData.receipt;
if (!receipt) {
aData.error = "INVALID_PARAMETERS";
aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", this.formatMessage(aData));
return;
}
let id = this._appIdForManifestURL(aData.manifestURL);
let app = this.webapps[id];
if (!app.receipts) {
aData.error = "NO_SUCH_RECEIPT";
aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", this.formatMessage(aData));
return;
}
let index = app.receipts.indexOf(receipt);
if (index == -1) {
aData.error = "NO_SUCH_RECEIPT";
aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", this.formatMessage(aData));
return;
}
app.receipts.splice(index, 1);
this._saveApps().then(() => {
aData.receipts = app.receipts;
aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", this.formatMessage(aData));
});
},
replaceReceipt: function(aData, aMm) {
debug("replaceReceipt " + aData.manifestURL);
let oldReceipt = aData.oldReceipt;
let newReceipt = aData.newReceipt;
if (!oldReceipt || !newReceipt) {
aData.error = "INVALID_PARAMETERS";
aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", this.formatMessage(aData));
return;
}
let error = this.isReceipt(newReceipt);
if (error) {
aData.error = error;
aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", this.formatMessage(aData));
return;
}
let id = this._appIdForManifestURL(aData.manifestURL);
let app = this.webapps[id];
if (!app.receipts) {
aData.error = "NO_SUCH_RECEIPT";
aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", this.formatMessage(aData));
return;
}
let oldIndex = app.receipts.indexOf(oldReceipt);
if (oldIndex == -1) {
aData.error = "NO_SUCH_RECEIPT";
aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", this.formatMessage(aData));
return;
}
app.receipts[oldIndex] = newReceipt;
this._saveApps().then(() => {
aData.receipts = app.receipts;
aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", this.formatMessage(aData));
});
},
setBlockedStatus: function(aManifestURL, aSeverity) {
let id = this._appIdForManifestURL(aManifestURL);
if (!id || !this.webapps[id]) {
return;
}
debug(`Setting blocked status ${aSeverity} on ${id}`);
let app = this.webapps[id];
app.blockedStatus = aSeverity;
let enabled = aSeverity == Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
this.setEnabled({ manifestURL: aManifestURL, enabled });
},
setEnabled: function(aData) {
debug("setEnabled " + aData.manifestURL + " : " + aData.enabled);
let id = this._appIdForManifestURL(aData.manifestURL);
if (!id || !this.webapps[id]) {
return;
}
debug("Enabling " + id);
let app = this.webapps[id];
// If we try to enable an app, check if it's not blocked.
if (!aData.enabled ||
app.blockedStatus == Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
app.enabled = aData.enabled;
}
this._saveApps().then(() => {
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:SetEnabled:Return", app);
});
// Update customization.
if (app.enabled) {
UserCustomizations.register(app);
} else {
UserCustomizations.unregister(app);
}
},
// Returns a promise that resolves once all the add-ons are disabled.
disableAllAddons: function() {
for (let id in this.webapps) {
let app = this.webapps[id];
if (app.role == "addon" && app.enabled) {
app.enabled = false;
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:SetEnabled:Return", app);
UserCustomizations.unregister(app);
}
}
return this._saveApps();
},
getManifestFor: function(aManifestURL, aEntryPoint) {
let id = this._appIdForManifestURL(aManifestURL);
let app = this.webapps[id];
if (!id || (app.installState == "pending" && !app.retryingDownload)) {
return Promise.resolve(null);
}
return this._readManifests([{ id: id }]).then((aResult) => {
if (aEntryPoint) {
return aResult[0].manifest.entry_points[aEntryPoint];
} else {
return aResult[0].manifest;
}
});
},
getAppByManifestURL: function(aManifestURL) {
return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
},
// Returns a promise that resolves to the app object with the manifest.
getFullAppByManifestURL: function(aManifestURL, aEntryPoint, aLang) {
let app = this.getAppByManifestURL(aManifestURL);
if (!app) {
return Promise.reject("NoSuchApp");
}
return this.getManifestFor(aManifestURL).then((aManifest) => {
if (!aManifest) {
return Promise.reject("NoManifest");
}
let manifest = aEntryPoint && aManifest.entry_points &&
aManifest.entry_points[aEntryPoint]
? aManifest.entry_points[aEntryPoint]
: aManifest;
// `version` doesn't change based on entry points, and we need it
// to check langpack versions.
if (manifest !== aManifest) {
manifest.version = aManifest.version;
}
app.manifest =
new ManifestHelper(manifest, app.origin, app.manifestURL, aLang);
return app;
});
},
_getAppWithManifest: Task.async(function*(aManifestURL) {
let app = this.getAppByManifestURL(aManifestURL);
if (!app) {
throw new Error("NO_SUCH_APP");
}
app.manifest = ( yield this._readManifests([{ id: app.id }]) )[0].manifest;
return app;
}),
getManifestCSPByLocalId: function(aLocalId) {
debug("getManifestCSPByLocalId:" + aLocalId);
return AppsUtils.getManifestCSPByLocalId(this.webapps, aLocalId);
},
getDefaultCSPByLocalId: function(aLocalId) {
debug("getDefaultCSPByLocalId:" + aLocalId);
return AppsUtils.getDefaultCSPByLocalId(this.webapps, aLocalId);
},
getAppLocalIdByStoreId: function(aStoreId) {
debug("getAppLocalIdByStoreId:" + aStoreId);
return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
},
getAppByLocalId: function(aLocalId) {
return AppsUtils.getAppByLocalId(this.webapps, aLocalId);
},
getManifestURLByLocalId: function(aLocalId) {
return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
},
getAppLocalIdByManifestURL: function(aManifestURL) {
return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
},
getCoreAppsBasePath: function() {
return AppsUtils.getCoreAppsBasePath();
},
getWebAppsBasePath: function() {
return OS.Path.dirname(this.appsFile);
},
areAnyAppsInstalled: function() {
return AppsUtils.areAnyAppsInstalled(this.webapps);
},
_notifyCategoryAndObservers: function(subject, topic, data, msg) {
const serviceMarker = "service,";
// First create observers from the category manager.
let cm =
Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
let enumerator = cm.enumerateCategory(topic);
let observers = [];
while (enumerator.hasMoreElements()) {
let entry =
enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
let contractID = cm.getCategoryEntry(topic, entry);
let factoryFunction;
if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
contractID = contractID.substring(serviceMarker.length);
factoryFunction = "getService";
}
else {
factoryFunction = "createInstance";
}
try {
let handler = Cc[contractID][factoryFunction]();
if (handler) {
let observer = handler.QueryInterface(Ci.nsIObserver);
observers.push(observer);
}
} catch(e) { }
}
// Next enumerate the registered observers.
enumerator = Services.obs.enumerateObservers(topic);
while (enumerator.hasMoreElements()) {
try {
let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver);
if (observers.indexOf(observer) == -1) {
observers.push(observer);
}
} catch (e) { }
}
observers.forEach(function (observer) {
try {
observer.observe(subject, topic, data);
} catch(e) { }
});
// Send back an answer to the child.
if (msg) {
ppmm.broadcastAsyncMessage("Webapps:ClearBrowserData:Return", msg);
}
},
registerBrowserElementParentForApp: function(aMsg, aMn) {
let appId = this.getAppLocalIdByManifestURL(aMsg.manifestURL);
if (appId == Ci.nsIScriptSecurityManager.NO_APP_ID) {
return;
}
// Make a listener function that holds on to this appId.
let listener = this.receiveAppMessage.bind(this, appId);
this.frameMessages.forEach(function(msgName) {
aMn.addMessageListener(msgName, listener);
});
},
receiveAppMessage: function(appId, message) {
switch (message.name) {
case "Webapps:ClearBrowserData":
this._clearPrivateData(appId, true, message.data);
break;
}
},
_clearPrivateData: function(appId, browserOnly, msg) {
let subject = {
appId: appId,
browserOnly: browserOnly,
QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
};
this._clearOriginData(appId, browserOnly);
this._notifyCategoryAndObservers(subject, "webapps-clear-data", null, msg);
},
_clearOriginData: function(appId, browserOnly) {
let attributes = {appId: appId};
if (browserOnly) {
attributes.inIsolatedMozBrowser = true;
}
this._notifyCategoryAndObservers(null, "clear-origin-data", JSON.stringify(attributes));
}
};
/**
* Appcache download observer
*/
var AppcacheObserver = function(aApp) {
debug("Creating AppcacheObserver for " + aApp.origin +
" - " + aApp.installState);
this.app = aApp;
this.startStatus = aApp.installState;
this.lastProgressTime = 0;
// Send a first progress event to correctly set the DOM object's properties.
this._sendProgressEvent();
};
AppcacheObserver.prototype = {
// nsIOfflineCacheUpdateObserver implementation
_sendProgressEvent: function() {
let app = this.app;
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "progress",
manifestURL: app.manifestURL
});
},
updateStateChanged: function appObs_Update(aUpdate, aState) {
let mustSave = false;
let app = this.app;
debug("Offline cache state change for " + app.origin + " : " + aState);
var self = this;
let setStatus = function appObs_setStatus(aStatus, aProgress) {
debug("Offlinecache setStatus to " + aStatus + " with progress " +
aProgress + " for " + app.origin);
mustSave = (app.installState != aStatus);
app.installState = aStatus;
app.progress = aProgress;
if (aStatus != "installed") {
self._sendProgressEvent();
return;
}
app.updateTime = Date.now();
app.downloading = false;
app.downloadAvailable = false;
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: ["downloadsuccess", "downloadapplied"],
manifestURL: app.manifestURL
});
}
let setError = function appObs_setError(aError) {
debug("Offlinecache setError to " + aError);
app.downloading = false;
mustSave = true;
// If we are canceling the download, we already send a DOWNLOAD_CANCELED
// error.
if (app.isCanceling) {
delete app.isCanceling;
return;
}
MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
app: app,
error: aError,
id: app.id
});
MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
eventType: "downloaderror",
manifestURL: app.manifestURL
});
}
switch (aState) {
case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR:
aUpdate.removeObserver(this);
AppDownloadManager.remove(app.manifestURL);
setError("APP_CACHE_DOWNLOAD_ERROR");
break;
case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE:
case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED:
aUpdate.removeObserver(this);
AppDownloadManager.remove(app.manifestURL);
setStatus("installed", aUpdate.byteProgress);
break;
case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING:
setStatus(this.startStatus, aUpdate.byteProgress);
break;
case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED:
case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS:
let now = Date.now();
if (now - this.lastProgressTime > MIN_PROGRESS_EVENT_DELAY) {
setStatus(this.startStatus, aUpdate.byteProgress);
this.lastProgressTime = now;
}
break;
}
// Status changed, update the stored version.
if (mustSave) {
DOMApplicationRegistry._saveApps();
}
},
applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) {
// Nothing to do.
}
};
DOMApplicationRegistry.init();