mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 16:46:26 +00:00
f3a986a379
MozReview-Commit-ID: 5SNGsRU4m9V --HG-- extra : rebase_source : ccc6860481faa17b4b52571ae4fa6d9f2fc749de
4818 lines
159 KiB
JavaScript
4818 lines
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/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();
|