--- a/content/base/public/File.h
+++ b/content/base/public/File.h
@@ -747,16 +747,17 @@ public:
ErrorResult& aRv) MOZ_OVERRIDE;
virtual nsresult GetInternalStream(nsIInputStream**) MOZ_OVERRIDE;
void SetPath(const nsAString& aFullPath);
protected:
virtual ~FileImplFile() {
if (mFile && mIsTemporary) {
+ NS_WARNING("In temporary ~FileImplFile");
// Ignore errors if any, not much we can do. Clean-up will be done by
// https://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAnonymousTemporaryFile.cpp?rev=6c1c7e45c902#127
#ifdef DEBUG
nsresult rv =
#endif
mFile->Remove(false);
NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to remove temporary DOMFile.");
}
--- a/content/base/test/chrome/test_fileconstructor_tempfile.xul
+++ b/content/base/test/chrome/test_fileconstructor_tempfile.xul
@@ -69,17 +69,17 @@ try {
0666, 0);
outStream.write(fileData, fileData.length);
outStream.close();
// Create a scoped DOMFile so the gc will happily get rid of it.
{
let dirfile = new File(tmp, { temporary: true });
ok(true, "Temporary File() created");
- var reader = new FileReader();
+ let reader = new FileReader();
reader.readAsArrayBuffer(dirfile);
reader.onload = function(event) {
let buffer = event.target.result;
ok(buffer.byteLength > 0,
"Blob size should be > 0 : " + buffer.byteLength);
cleanup(tmp);
}
}
new file mode 100644
--- /dev/null
+++ b/dom/apps/ImportExport.jsm
@@ -0,0 +1,488 @@
+/* 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;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Webapps.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
+ "resource://gre/modules/PermissionsInstaller.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+this.EXPORTED_SYMBOLS = ["ImportExport"];
+
+const kAppArchiveMimeType = "application/openwebapp+zip";
+const kAppArchiveExtension = ".wpk"; // Webapp Package
+const kAppArchiveVersion = 1;
+
+// From prio.h
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+function debug(aMsg) {
+//#ifdef DEBUG
+ dump("-*- ImportExport.jsm : " + aMsg + "\n");
+//#endif
+}
+
+/*
+The full meta-data for an app looks like the properties set by
+the mozIApplication constructor in AppsUtils.jsm. We don't export anything
+that can't be recreated from the manifest or by using sane defaults.
+*/
+
+// Reads a JSON object from a zip.
+function readObjectFromZip(aZipReader, aPath) {
+ if (!aZipReader.hasEntry(aPath)) {
+ debug("ZIP doesn't have entry " + aPath);
+ return;
+ }
+
+ let istream = aZipReader.getInputStream(aPath);
+
+ // 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 res;
+ try {
+ res = JSON.parse(converter.ConvertToUnicode(
+ NetUtil.readInputStreamToString(istream, istream.available()) || ""));
+ } catch(e) {
+ debug("error reading " + aPath + " from ZIP: " + e);
+ }
+ return res;
+}
+
+this.ImportExport = {
+ getUUID: function() {
+ let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ return uuidGenerator.generateUUID().toString();
+ },
+
+ // Exports to a Blob. Returns a promise that is resolved with the Blob and
+ // rejected with an error string.
+ // Possible errors are:
+ // NoSuchApp, AppNotFullyInstalled, CertifiedAppExportForbidden, ZipWriteError,
+ // NoAppDirectory
+ export: Task.async(function*(aApp) {
+ if (!aApp) {
+ // Should not happen!
+ throw "NoSuchApp";
+ }
+
+ debug("Exporting " + aApp.manifestURL);
+
+ if (aApp.installState != "installed") {
+ throw "AppNotFullyInstalled";
+ }
+
+ // Exporting certified apps is forbidden, as it is to import them.
+ // We *have* to do this check in the parent process.
+ if (aApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
+ throw "CertifiedAppExportForbidden";
+ }
+
+ // Add the metadata we'll need to recreate the app object.
+ let meta = {
+ installOrigin: aApp.InstallOrigin,
+ manifestURL: aApp.manifestURL,
+ version: kAppArchiveVersion
+ };
+
+ // Add all the needed files in the app's base path to the archive.
+
+ debug("Adding files from " + aApp.basePath + "/" + aApp.id);
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(aApp.basePath);
+ dir.append(aApp.id);
+ if (!dir.exists() || !dir.isDirectory()) {
+ throw "NoAppDirectory";
+ }
+
+ let files = [];
+ if (aApp.origin.startsWith("app://")) {
+ files.push("update.webapp");
+ files.push("application.zip");
+ } else {
+ files.push("manifest.webapp");
+ }
+
+ // Creates the archive and adds the application meta-data.
+ // Using the app id as the file name prevents name collisions.
+ let zipWriter = Cc["@mozilla.org/zipwriter;1"]
+ .createInstance(Ci.nsIZipWriter);
+ let uuid = this.getUUID();
+
+ // We create the file in ${TempDir}/mozilla-temp-files to make sure we
+ // can remove it once the blob has been used even on windows.
+ // See https://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAnonymousTemporaryFile.cpp?rev=6c1c7e45c902#127
+ let zipFile = FileUtils.getFile("TmpD",
+ ["mozilla-temp-files", uuid + kAppArchiveExtension]);
+ debug("Creating archive " + zipFile.path);
+
+ zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+ let blob;
+
+ try {
+ debug("Adding metadata.json to exported blob.");
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ let s = JSON.stringify(meta);
+ stream.setData(s, s.length);
+ zipWriter.addEntryStream("metadata.json", Date.now(),
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ stream, false);
+
+ files.forEach((aName) => {
+ let file = dir.clone();
+ file.append(aName);
+ debug("Adding " + file.leafName + " to export blob.");
+ zipWriter.addEntryFile(file.leafName,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file, false);
+ });
+
+ zipWriter.close();
+ // Reads back the file as Blob.
+ // File is only available on Window and Worker objects, so we need
+ // to get a window there...
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!win) {
+ throw "NoWindowAvailable";
+ }
+ blob = new win.File(zipFile, { name: aApp.id + kAppArchiveExtension,
+ type: kAppArchiveMimeType,
+ temporary: true });
+ } catch(e) {
+ debug("Error: " + e);
+ zipWriter.close();
+ zipFile.remove(false);
+ throw "ZipWriteError";
+ }
+
+ return blob;
+ }),
+
+ // Returns the manifest for this hosted app.
+ _importHostedApp: function(aZipReader, aManifestURL) {
+ debug("Importing hosted app " + aManifestURL);
+
+ if (!aZipReader.hasEntry("manifest.webapp")) {
+ throw "NoManifestFound";
+ }
+
+ let manifest = readObjectFromZip(aZipReader, "manifest.webapp");
+ if (!manifest) {
+ throw "NoManifestFound";
+ }
+
+ return manifest;
+ },
+
+ // Returns the manifest for this packaged app.
+ _importPackagedApp: function(aZipReader, aManifestURL, aDir) {
+ debug("Importing packaged app " + aManifestURL);
+
+ if (!aZipReader.hasEntry("update.webapp")) {
+ throw "NoUpdateManifestFound";
+ }
+
+ if (!aZipReader.hasEntry("application.zip")) {
+ throw "NoPackageFound";
+ }
+
+ // Extract application.zip and update.webapp
+ // We get manifest.webapp from application.zip itself.
+ let file;
+ ["update.webapp", "application.zip"].forEach((aName) => {
+ file = aDir.clone();
+ file.append(aName);
+ aZipReader.extract(aName, file);
+ });
+
+ // |file| now points to application.zip, open it.
+ let appZipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+ .createInstance(Ci.nsIZipReader);
+ appZipReader.open(file);
+ if (!appZipReader.hasEntry("manifest.webapp")) {
+ throw "NoManifestFound";
+ }
+
+ return [readObjectFromZip(appZipReader, "manifest.webapp"), file];
+ },
+
+ // Imports a blob, returning a Promise that resolves to
+ // [manifestURL, manifest]
+ // Possible errors are:
+ // NoBlobFound, UnsupportedBlobArchive, MissingMetadataFile, IncorrectVersion,
+ // AppAlreadyInstalled, DontImportCertifiedApps, InvalidManifest,
+ // InvalidPrivilegeLevel, InvalidOrigin, DuplicateOrigin
+ import: Task.async(function*(aBlob) {
+
+ // First, do we even have a blob?
+ if (!aBlob || !aBlob instanceof Ci.nsIDOMBlob) {
+ throw "NoBlobFound";
+ }
+
+ let isFile = aBlob instanceof Ci.nsIDOMFile;
+ if (!isFile) {
+ // XXX: TODO Store the blob on disk.
+ throw "UnsupportedBlobArchive";
+ }
+
+ // We can't QI the DOMFile to nsIFile, so we need to create one.
+ let zipFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ zipFile.initWithPath(aBlob.mozFullPath);
+
+ debug("Importing from " + zipFile.path);
+
+ let meta;
+ let appDir;
+ let manifest;
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+ .createInstance(Ci.nsIZipReader);
+ zipReader.open(zipFile);
+ try {
+ // Do some sanity checks on the metadata.json and manifest.webapp files.
+ if (!zipReader.hasEntry("metadata.json")) {
+ throw "MissingMetadataFile";
+ }
+
+ meta = readObjectFromZip(zipReader, "metadata.json");
+ if (!meta) {
+ throw "NoMetadata";
+ }
+ debug("metadata: " + uneval(meta));
+
+ // Bail out if that comes from an unsupported archive version.
+ if (meta.version !== 1) {
+ throw "IncorrectVersion";
+ }
+
+ // Check if we already have an app installed for this manifest url.
+ let app = DOMApplicationRegistry.getAppByManifestURL(meta.manifestURL);
+ if (app) {
+ throw "AppAlreadyInstalled";
+ }
+
+ // Create a new app id & localId.
+ // TODO: stop accessing internal methods of other objects.
+ meta.localId = DOMApplicationRegistry._nextLocalId();
+ meta.id = this.getUUID();
+ meta.basePath = FileUtils.getDir(DOMApplicationRegistry.dirKey,
+ ["webapps"], false).path;
+
+ appDir = FileUtils.getDir(DOMApplicationRegistry.dirKey,
+ ["webapps", meta.id], true);
+
+ let isPackage = zipReader.hasEntry("application.zip");
+
+ let appFile;
+
+ if (isPackage) {
+ [manifest, appFile] =
+ this._importPackagedApp(zipReader, meta.manifestURL, appDir);
+ } else {
+ manifest = this._importHostedApp(zipReader, meta.manifestURL);
+ }
+
+ if (!AppsUtils.checkManifest(manifest)) {
+ throw "InvalidManifest";
+ }
+
+ let manifestFile = appDir.clone();
+ manifestFile.append("manifest.webapp");
+
+ let manifestString = JSON.stringify(manifest);
+
+ // We now have the correct manifest. Save it.
+ // TODO: stop accessing internal methods of other objects.
+ yield DOMApplicationRegistry._writeFile(manifestFile.path,
+ manifestString)
+ .then(() => { debug("Manifest saved."); },
+ aError => { debug("Error saving manifest: " + aError )});
+
+ // Default values for the common fields.
+ // TODO: share this code with the main install flow and with
+ // DOMApplicationRegistry::addInstalledApp
+ meta.name = manifest.name;
+ meta.csp = manifest.csp;
+ meta.installTime = Date.now();
+ meta.removable = true;
+ meta.progress = 0.0;
+ meta.installState = "installed";
+ meta.downloadAvailable = false;
+ meta.downloading = false;
+ meta.readyToApplyDownload = false;
+ meta.downloadSize = 0;
+ meta.lastUpdateCheck = Date.now();
+ meta.updateTime = Date.now();
+ meta.manifestHash = AppsUtils.computeHash(manifestString);
+ meta.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
+ meta.installerIsBrowser = false;
+ meta.role = manifest.role;
+
+ // Set the appropriate metadata for hosted and packaged apps.
+ if (isPackage) {
+ meta.origin = "app://" + meta.id;
+ // Signature check
+ // TODO: stop accessing internal methods of other objects.
+ let [reader, isSigned] =
+ yield DOMApplicationRegistry._openPackage(appFile, meta, false);
+ let maxStatus = isSigned ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
+ : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+ meta.appStatus = AppsUtils.getAppManifestStatus(manifest);
+ debug("Signed app? " + isSigned);
+ if (meta.appStatus > maxStatus) {
+ throw "InvalidPrivilegeLevel";
+ }
+
+ // Custom origin.
+ // We unfortunately can't reuse _checkOrigin here.
+ if (isSigned &&
+ meta.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
+ manifest.origin) {
+ let uri;
+ try {
+ uri = Services.io.newURI(aManifest.origin, null, null);
+ } catch(e) {
+ throw "InvalidOrigin";
+ }
+ if (uri.scheme != "app") {
+ throw "InvalidOrigin";
+ }
+ meta.id = uri.prePath.substring(6); // "app://".length
+ if (meta.id in DOMApplicationRegistry.webapps) {
+ throw "DuplicateOrigin";
+ }
+ meta.origin = uri.prePath;
+ }
+ } else {
+ let uri = Services.io.newURI(meta.manifestURL, null, null);
+ meta.origin = uri.resolve("/");
+ meta.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+ if (manifest.appcache_path) {
+ // We don't export the content of the appcache, so set the app
+ // in the state that will trigger download.
+ meta.installState = "pending";
+ meta.downloadAvailable = true;
+ }
+ }
+ meta.kind = DOMApplicationRegistry.appKind(meta, manifest);
+
+ DOMApplicationRegistry.webapps[meta.id] = meta;
+
+ // Set permissions and handlers
+ PermissionsInstaller.installPermissions(
+ {
+ origin: meta.origin,
+ manifestURL: meta.manifestURL,
+ manifest: manifest
+ },
+ false,
+ null);
+ DOMApplicationRegistry.updateAppHandlers(null /* old manifest */,
+ manifest,
+ meta);
+
+ // Save the app registry, and sends the various notifications.
+ // TODO: stop accessing internal methods of other objects.
+ yield DOMApplicationRegistry._saveApps();
+
+ app = AppsUtils.cloneAppObject(meta);
+ app.manifest = manifest;
+ DOMApplicationRegistry.broadcastMessage("Webapps:AddApp",
+ { id: meta.id, app: app });
+ DOMApplicationRegistry.broadcastMessage("Webapps:Install:Return:OK",
+ { app: app });
+ Services.obs.notifyObservers(null, "webapps-installed",
+ JSON.stringify({ manifestURL: meta.manifestURL }));
+
+ } catch(e) {
+ debug("Import failed: " + e);
+ if (appDir && appDir.exists()) {
+ appDir.remove(true);
+ }
+ throw e;
+ } finally {
+ zipReader.close();
+ }
+
+ return [meta.manifestURL, manifest];
+ }),
+
+ // Extracts the manifest from a blob, returning a Promise that resolves to
+ // the manifest
+ // Possible errors are:
+ // NoBlobFound, UnsupportedBlobArchive, MissingMetadataFile.
+ extractManifest: Task.async(function*(aBlob) {
+ // First, do we even have a blob?
+ if (!aBlob || !aBlob instanceof Ci.nsIDOMBlob) {
+ throw "NoBlobFound";
+ }
+
+ let isFile = aBlob instanceof Ci.nsIDOMFile;
+ if (!isFile) {
+ // XXX: TODO Store the blob on disk.
+ throw "UnsupportedBlobArchive";
+ }
+
+ // We can't QI the DOMFile to nsIFile, so we need to create one.
+ let zipFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ zipFile.initWithPath(aBlob.mozFullPath);
+ debug("extractManifest from " + zipFile.path);
+
+ // Do some sanity checks on the metadata.json and manifest.webapp files.
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+ .createInstance(Ci.nsIZipReader);
+ zipReader.open(zipFile);
+
+ let manifest;
+ try {
+ if (zipReader.hasEntry("manifest.webapp")) {
+ manifest = readObjectFromZip(zipReader, "manifest.webapp");
+ if (!manifest) {
+ throw "NoManifest";
+ }
+ } else if (zipReader.hasEntry("application.zip")) {
+ // That's a packaged app, we need to extract from the inner zip.
+ let innerReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+ .createInstance(Ci.nsIZipReader);
+ innerReader.openInner(zipReader, "application.zip");
+ manifest = readObjectFromZip(innerReader, "manifest.webapp");
+ innerReader.close();
+ if (!manifest) {
+ throw "NoManifest";
+ }
+ } else {
+ throw "MissingManifestFile";
+ }
+ } finally {
+ zipReader.close();
+ }
+
+ return manifest;
+ }),
+};
--- a/dom/apps/Webapps.js
+++ b/dom/apps/Webapps.js
@@ -543,16 +543,30 @@ WebappsApplication.prototype = {
newReceipt: newReceipt,
oldReceipt: oldReceipt,
oid: this._id,
requestID: this.getRequestId(request) });
return request;
},
+ export: function() {
+ this.addMessageListeners(["Webapps:Export:Return"]);
+ return this.createPromise((aResolve, aReject) => {
+ cpmm.sendAsyncMessage("Webapps:Export",
+ { manifestURL: this.manifestURL,
+ oid: this._id,
+ requestID: this.getPromiseResolverId({
+ resolve: aResolve,
+ reject: aReject
+ })
+ });
+ });
+ },
+
_prepareForContent: function() {
if (this.__DOM_IMPL__) {
return this.__DOM_IMPL__;
}
return this._window.DOMApplication._create(this._window, this.wrappedJSObject);
},
uninit: function() {
@@ -579,17 +593,18 @@ WebappsApplication.prototype = {
: Services.DOMRequest.fireSuccess(req, msg.result);
},
receiveMessage: function(aMessage) {
let msg = aMessage.json;
let req;
if (aMessage.name == "Webapps:Connect:Return:OK" ||
aMessage.name == "Webapps:Connect:Return:KO" ||
- aMessage.name == "Webapps:GetConnections:Return:OK") {
+ aMessage.name == "Webapps:GetConnections:Return:OK" ||
+ aMessage.name == "Webapps:Export:Return") {
req = this.takePromiseResolver(msg.requestID);
} else {
req = this.takeRequest(msg.requestID);
}
if (msg.oid !== this._id || !req) {
return;
}
@@ -667,16 +682,24 @@ WebappsApplication.prototype = {
this._proxy.receipts = msg.receipts;
Services.DOMRequest.fireSuccess(req, null);
break;
case "Webapps:ReplaceReceipt:Return:KO":
this.removeMessageListeners(["Webapps:ReplaceReceipt:Return:OK",
"Webapps:ReplaceReceipt:Return:KO"]);
Services.DOMRequest.fireError(req, msg.error);
break;
+ case "Webapps:Export:Return":
+ this.removeMessageListeners(["Webapps:Export:Return"]);
+ if (msg.success) {
+ req.resolve(Cu.cloneInto(msg.blob, this._window));
+ } else {
+ req.reject(new this._window.DOMError(msg.error || ""));
+ }
+ break;
}
},
classID: Components.ID("{723ed303-7757-4fb0-b261-4f78b1f6bd22}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
Ci.nsIObserver,
Ci.nsISupportsWeakReference])
@@ -692,17 +715,19 @@ function WebappsApplicationMgmt() {
WebappsApplicationMgmt.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
init: function(aWindow) {
this.initDOMRequestHelper(aWindow, ["Webapps:Uninstall:Return:OK",
"Webapps:Uninstall:Broadcast:Return:OK",
"Webapps:Uninstall:Return:KO",
"Webapps:Install:Return:OK",
- "Webapps:GetNotInstalled:Return:OK"]);
+ "Webapps:GetNotInstalled:Return:OK",
+ "Webapps:Import:Return",
+ "Webapps:ExtractManifest:Return"]);
cpmm.sendAsyncMessage("Webapps:RegisterForMessages",
{
messages: ["Webapps:Install:Return:OK",
"Webapps:Uninstall:Return:OK",
"Webapps:Uninstall:Broadcast:Return:OK"]
}
);
},
@@ -750,16 +775,40 @@ WebappsApplicationMgmt.prototype = {
getNotInstalled: function() {
let request = this.createRequest();
cpmm.sendAsyncMessage("Webapps:GetNotInstalled", { oid: this._id,
requestID: this.getRequestId(request) });
return request;
},
+ import: function(aBlob) {
+ return this.createPromise((aResolve, aReject) => {
+ cpmm.sendAsyncMessage("Webapps:Import",
+ { blob: aBlob,
+ oid: this._id,
+ requestID: this.getPromiseResolverId({
+ resolve: aResolve,
+ reject: aReject
+ })});
+ });
+ },
+
+ extractManifest: function(aBlob) {
+ return this.createPromise((aResolve, aReject) => {
+ cpmm.sendAsyncMessage("Webapps:ExtractManifest",
+ { blob: aBlob,
+ oid: this._id,
+ requestID: this.getPromiseResolverId({
+ resolve: aResolve,
+ reject: aReject
+ })});
+ });
+ },
+
get oninstall() {
return this.__DOM_IMPL__.getEventHandler("oninstall");
},
get onuninstall() {
return this.__DOM_IMPL__.getEventHandler("onuninstall");
},
@@ -767,25 +816,34 @@ WebappsApplicationMgmt.prototype = {
this.__DOM_IMPL__.setEventHandler("oninstall", aCallback);
},
set onuninstall(aCallback) {
this.__DOM_IMPL__.setEventHandler("onuninstall", aCallback);
},
receiveMessage: function(aMessage) {
- var msg = aMessage.json;
- let req = this.getRequest(msg.requestID);
+ let msg = aMessage.data;
+ let req;
+ if (["Webapps:Import:Return",
+ "Webapps:ExtractManifest:Return"]
+ .indexOf(aMessage.name) != -1) {
+ req = this.takePromiseResolver(msg.requestID);
+ } else {
+ req = this.getRequest(msg.requestID);
+ }
+
// We want Webapps:Install:Return:OK and Webapps:Uninstall:Broadcast:Return:OK
// to be broadcasted to all instances of mozApps.mgmt.
if (!((msg.oid == this._id && req) ||
aMessage.name == "Webapps:Install:Return:OK" ||
aMessage.name == "Webapps:Uninstall:Broadcast:Return:OK")) {
return;
}
+
switch (aMessage.name) {
case "Webapps:GetNotInstalled:Return:OK":
Services.DOMRequest.fireSuccess(req, convertAppsArray(msg.apps, this._window));
break;
case "Webapps:Install:Return:OK":
{
let app = createContentApplicationObject(this._window, msg.app);
let event =
@@ -802,16 +860,30 @@ WebappsApplicationMgmt.prototype = {
}
break;
case "Webapps:Uninstall:Return:OK":
Services.DOMRequest.fireSuccess(req, msg.manifestURL);
break;
case "Webapps:Uninstall:Return:KO":
Services.DOMRequest.fireError(req, "NOT_INSTALLED");
break;
+ case "Webapps:Import:Return":
+ if (msg.success) {
+ req.resolve(createContentApplicationObject(this._window, msg.app));
+ } else {
+ req.reject(new this._window.DOMError(msg.error || ""));
+ }
+ break;
+ case "Webapps:ExtractManifest:Return":
+ if (msg.success) {
+ req.resolve(Cu.cloneInto(msg.manifest, this._window));
+ } else {
+ req.reject(new this._window.DOMError(msg.error || ""));
+ }
+ break;
}
if (aMessage.name !== "Webapps:Uninstall:Broadcast:Return:OK") {
this.removeRequest(msg.requestID);
}
},
classID: Components.ID("{8c1bca96-266f-493a-8d57-ec7a95098c15}"),
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -62,16 +62,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
"resource://gre/modules/ScriptPreloader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TrustedHostedAppsUtils",
"resource://gre/modules/TrustedHostedAppsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ImportExport",
+ "resource://gre/modules/ImportExport.jsm");
+
#ifdef MOZ_WIDGET_GONK
XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
Cu.import("resource://gre/modules/systemlibs.js");
return libcutils;
});
#endif
#ifdef MOZ_WIDGET_ANDROID
@@ -162,30 +165,33 @@ this.DOMApplicationRegistry = {
// Path to the webapps.json file where we store the registry data.
appsFile: null,
webapps: { },
children: [ ],
allAppsLaunchable: false,
_updateHandlers: [ ],
_pendingUninstalls: {},
+ dirKey: DIRECTORY_NAME,
init: function() {
this.messages = ["Webapps:Install", "Webapps:Uninstall",
"Webapps:GetSelf", "Webapps:CheckInstalled",
"Webapps:GetInstalled", "Webapps:GetNotInstalled",
"Webapps:Launch",
"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:ExtractManifest",
"child-process-shutdown"];
this.frameMessages = ["Webapps:ClearBrowserData"];
this.messages.forEach((function(msgName) {
ppmm.addMessageListener(msgName, this);
}).bind(this));
@@ -1170,20 +1176,23 @@ this.DOMApplicationRegistry = {
},
receiveMessage: function(aMessage) {
// nsIPrefBranch throws if pref does not exist, faster to simply write
// the pref instead of first checking if it is false.
Services.prefs.setBoolPref("dom.mozApps.used", true);
// We need to check permissions for calls coming from mozApps.mgmt.
- // These are: getNotInstalled(), applyDownload() and uninstall().
+ // These are: getNotInstalled(), applyDownload(), uninstall(), import() and
+ // extractManifest().
if (["Webapps:GetNotInstalled",
"Webapps:ApplyDownload",
- "Webapps:Uninstall"].indexOf(aMessage.name) != -1) {
+ "Webapps:Uninstall",
+ "Webapps:Import",
+ "Webapps:ExtractManifest"].indexOf(aMessage.name) != -1) {
if (!aMessage.target.assertPermission("webapps-manage")) {
debug("mozApps message " + aMessage.name +
" from a content process with no 'webapps-manage' privileges.");
return null;
}
}
// And RegisterBEP requires "browser" permission...
if ("Webapps:RegisterBEP" == aMessage.name) {
@@ -1299,16 +1308,25 @@ this.DOMApplicationRegistry = {
this.removeReceipt(msg, mm);
break;
case "Webapps:ReplaceReceipt":
this.replaceReceipt(msg, mm);
break;
case "Webapps:RegisterBEP":
this.registerBrowserElementParentForApp(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;
}
});
},
getAppInfo: function getAppInfo(aAppId) {
return AppsUtils.getAppInfo(this.webapps, aAppId);
},
@@ -1366,18 +1384,20 @@ this.DOMApplicationRegistry = {
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)) {
- deferred.reject()
+ debug("Error saving " + aPath + " : " + aResult);
+ deferred.reject(aResult)
} else {
+ debug("Success saving " + aPath);
deferred.resolve();
}
});
return deferred.promise;
},
/**
@@ -1406,16 +1426,109 @@ this.DOMApplicationRegistry = {
let thread = Services.tm.currentThread;
while (!done) {
thread.processNextEvent(/* mayWait */ true);
}
return { webapps: this.webapps, manifests: res };
},
+ doExport: function(aMsg, aMm) {
+
+ function 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) {
+ function 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) {
+ function sendError() {
+ 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,
function onsuccess() {
aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData);
--- a/dom/apps/moz.build
+++ b/dom/apps/moz.build
@@ -37,16 +37,17 @@ EXTRA_JS_MODULES += [
'OfflineCacheInstaller.jsm',
'PermissionsInstaller.jsm',
'PermissionsTable.jsm',
'StoreTrustAnchor.jsm',
]
EXTRA_PP_JS_MODULES += [
'AppsUtils.jsm',
+ 'ImportExport.jsm',
'OperatorApps.jsm',
'ScriptPreloader.jsm',
'TrustedHostedAppsUtils.jsm',
'Webapps.jsm',
]
FAIL_ON_WARNINGS = True
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -19,16 +19,17 @@ support-files =
signed_app_template.webapp
signed/*
test_packaged_app_common.js
marketplace/*
pkg_install_iframe.html
[test_app_update.html]
[test_bug_795164.html]
+[test_import_export.html]
[test_install_multiple_apps_origin.html]
[test_install_receipts.html]
[test_marketplace_pkg_install.html]
skip-if = buildapp == "b2g" || toolkit == "android" # see bug 989806
[test_packaged_app_install.html]
[test_packaged_app_update.html]
[test_receipt_operations.html]
[test_signed_pkg_install.html]
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/test_import_export.html
@@ -0,0 +1,315 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={982874}
+-->
+<head>
+ <title>Test for Bug {982874}</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="test_packaged_app_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={982874}">Mozilla Bug {982874}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+var gManifestURL = "http://test/tests/dom/apps/tests/file_app.sjs?apptype=hosted&getmanifest=true";
+var gGenerator = runTest();
+
+function runApp(aApp, aCallback) {
+ var ifr = document.createElement('iframe');
+ ifr.setAttribute('mozbrowser', 'true');
+ ifr.setAttribute('mozapp', aApp.manifestURL);
+ ifr.src = aApp.origin + aApp.manifest.launch_path;
+
+ ifr.addEventListener('mozbrowsershowmodalprompt', function onAlert(e) {
+ var message = e.detail.message;
+ info("Got message " + message);
+
+ if (message.startsWith("OK: ")) {
+ ok(true, message.substring(4, message.length));
+ } else if (message.startsWith("ERROR: ")) {
+ ok(false, message.substring(7, message.length));
+ } else if (message == "DONE") {
+ ifr.removeEventListener('mozbrowsershowmodalprompt', onAlert, false);
+ document.body.removeChild(ifr);
+ aCallback();
+ }
+ }, false);
+
+ document.body.appendChild(ifr);
+}
+
+function go() {
+ SpecialPowers.pushPermissions(
+ [{ "type": "webapps-manage", "allow": 1, "context": document },
+ { "type": "browser", "allow": 1, "context": document },
+ { "type": "embed-apps", "allow": 1, "context": document }],
+ function() {
+ SpecialPowers.pushPrefEnv({'set': [["dom.mozBrowserFramesEnabled", true]]}, continueTest) });
+}
+
+function continueTest() {
+ try {
+ gGenerator.next();
+ } catch (e if e instanceof StopIteration) {
+ finish();
+ }
+}
+
+function finish() {
+ SimpleTest.finish();
+}
+
+function cbError(aEvent) {
+ ok(false, "Error callback invoked " +
+ aEvent.target.error.name + " " + aEvent.target.error.message);
+ finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+/**
+ * Install 2 apps from the same origin and uninstall them.
+ */
+function runTest() {
+ SpecialPowers.setAllAppsLaunchable(true);
+
+ SpecialPowers.autoConfirmAppInstall(continueTest);
+ yield undefined;
+
+ SpecialPowers.autoConfirmAppUninstall(continueTest);
+ yield undefined;
+
+ // Check how many apps we are starting with.
+ request = navigator.mozApps.mgmt.getAll();
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ var initialAppsCount = request.result.length;
+ info("Starting with " + initialAppsCount + " apps installed.");
+
+ // Install a hosted app.
+ var request = navigator.mozApps.install(gManifestURL, { });
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+
+ var app = request.result;
+ ok(app, "App is non-null");
+ is(app.manifestURL, gManifestURL, "App manifest url is correct.");
+
+ // Export the hosted app.
+ var exported;
+
+ app.export().then(blob => {
+ exported = blob;
+ ok(blob !== null, "Exported blob is not null");
+ ok(blob.size > 0, "Exported blob size is > 0");
+ continueTest();
+ }, error => {
+ info(error);
+ ok(false, "Export 1 should not fail");
+ });
+
+ yield undefined;
+
+ // Try to import the same blob. That will fails since the app is
+ // already installed.
+ navigator.mozApps.mgmt.import(exported)
+ .then((app) => {
+ ok(false, "Can't import an app already installed!");
+ }, (error) => {
+ is(error.name, "AppAlreadyInstalled", "Reject import of already installed app.");
+ continueTest();
+ }
+ );
+
+ yield undefined;
+
+ // Get some information from the manifest.
+ navigator.mozApps.mgmt.extractManifest(exported)
+ .then((manifest) => {
+ is(manifest.name, "Really Rapid Release (hosted)",
+ "Check manifest.name");
+ continueTest();
+ }, (error) => {
+ ok(false, "Should not fail to extract the manifest.");
+ }
+ );
+
+ yield undefined;
+
+ // Uninstall the hosted app.
+ navigator.mozApps.mgmt.onuninstall = function(event) {
+ var app = event.application;
+ is(app.manifestURL, gManifestURL, "App uninstall event ok.");
+ is(app.manifest.name, "Really Rapid Release (hosted)",
+ "App uninstall manifest ok.");
+ continueTest();
+ }
+ request = navigator.mozApps.mgmt.uninstall(app);
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ yield undefined;
+ is(request.result, gManifestURL, "Hosted App uninstalled.");
+ navigator.mozApps.mgmt.onuninstall = null;
+
+ // Re-import the app. This time this will succeed.
+ navigator.mozApps.mgmt.import(exported)
+ .then((imported) => {
+ ok(imported !== null, "Imported app is not null.");
+ is(imported.manifest.name, "Really Rapid Release (hosted)",
+ "Verifying manifest name");
+ app = imported;
+ continueTest();
+ }, (error) => {
+ ok(false, "We should not fail to import!");
+ }
+ );
+
+ yield undefined;
+
+ // Launch the imported hosted app.
+ info("Running " + app.manifestURL);
+ runApp(app, continueTest);
+ yield undefined;
+
+ // Uninstall the imported app to cleanup after ourself.
+ navigator.mozApps.mgmt.onuninstall = function(event) {
+ var app = event.application;
+ is(app.manifestURL, gManifestURL, "App uninstall event ok.");
+ is(app.manifest.name, "Really Rapid Release (hosted)",
+ "App uninstall manifest ok.");
+ continueTest();
+ }
+ request = navigator.mozApps.mgmt.uninstall(app);
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ yield undefined;
+ is(request.result, gManifestURL, "Hosted App imported uninstalled.");
+ navigator.mozApps.mgmt.onuninstall = null;
+
+ // Install a packaged app.
+ PackagedTestHelper.setAppVersion(2, continueTest);
+
+ yield undefined;
+
+ var miniManifestURL = PackagedTestHelper.gSJS +
+ "?getManifest=true";
+ info("Install packaged app from " + miniManifestURL);
+ navigator.mozApps.mgmt.oninstall = function(evt) {
+ info("Got oninstall event");
+ PackagedTestHelper.gApp = evt.application;
+ PackagedTestHelper.gApp.ondownloaderror = function() {
+ ok(false, "Package Download error " +
+ PackagedTestHelper.gApp.downloadError.name);
+ PackagedTestHelper.finish();
+ };
+ PackagedTestHelper.gApp.ondownloadsuccess = function() {
+ info("Packaged App downloaded");
+ var expected = {
+ name: PackagedTestHelper.gAppName,
+ manifestURL: miniManifestURL,
+ installOrigin: PackagedTestHelper.gInstallOrigin,
+ progress: 0,
+ installState: "installed",
+ downloadAvailable: false,
+ downloading: false,
+ downloadSize: 0,
+ size: 0,
+ readyToApplyDownload: false
+ };
+ PackagedTestHelper.checkAppState(PackagedTestHelper.gApp, 2, expected,
+ true, false, continueTest);
+ };
+ };
+
+ var request = navigator.mozApps.installPackage(miniManifestURL);
+ request.onerror = PackagedTestHelper.mozAppsError;
+ request.onsuccess = function() {
+ info("Packaged Application installed");
+ };
+
+ yield undefined;
+
+ // Export the packaged app.
+ PackagedTestHelper.gApp.export().then((blob) => {
+ exported = blob;
+ ok(blob !== null, "Exported blob is not null");
+ info("blob size is " + blob.size);
+ ok(blob.size > 0, "Exported blob size is > 0");
+ continueTest();
+ }, (error) => {
+ ok(false, "Export 2 should not fail");
+ });
+
+ yield undefined;
+
+ // Uninstall the packaged app.
+ navigator.mozApps.mgmt.onuninstall = function(event) {
+ var app = event.application;
+ is(app.manifestURL, miniManifestURL, "Packaged App uninstall event ok.");
+ is(app.manifest.name, "appname", "Packaged App uninstall manifest ok.");
+ continueTest();
+ }
+ request = navigator.mozApps.mgmt.uninstall(PackagedTestHelper.gApp);
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ yield undefined;
+ is(request.result, miniManifestURL, "Packaged App uninstalled.");
+ navigator.mozApps.mgmt.onuninstall = null;
+
+ // Import the packaged app.
+ navigator.mozApps.mgmt.import(exported)
+ .then((imported) => {
+ ok(imported !== null, "Imported app is not null.");
+ is(imported.manifest.name, "appname", "Verifying imported app name");
+ app = imported;
+ continueTest();
+ }, (error) => {
+ ok(false, "We should not fail to import!");
+ }
+ );
+
+ yield undefined;
+
+ // Uninstall the imported packaged app.
+ navigator.mozApps.mgmt.onuninstall = function(event) {
+ var app = event.application;
+ is(app.manifestURL, miniManifestURL, "Packaged App uninstall event ok.");
+ is(app.manifest.name, "appname", "Packaged App uninstall manifest ok.");
+ continueTest();
+ }
+ request = navigator.mozApps.mgmt.uninstall(PackagedTestHelper.gApp);
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ yield undefined;
+ is(request.result, miniManifestURL, "Packaged App uninstalled.");
+ navigator.mozApps.mgmt.onuninstall = null;
+
+ // Check that we restored the app registry.
+ request = navigator.mozApps.mgmt.getAll();
+ request.onerror = cbError;
+ request.onsuccess = continueTest;
+ yield undefined;
+ is(request.result.length, initialAppsCount, "All apps are uninstalled.");
+}
+
+addLoadEvent(go);
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/webapps/test_list_api.xul
+++ b/dom/tests/mochitest/webapps/test_list_api.xul
@@ -42,16 +42,18 @@ var mgmtProps = {
getAll: "function",
getNotInstalled: "function",
uninstall: "function",
oninstall: "object",
onuninstall: "object",
ownerGlobal: "object",
removeEventListener: "function",
setEventHandler: "function",
+ extractManifest: "function",
+ import: "function"
};
isDeeply([p for (p in navigator.mozApps.mgmt)].sort(),
Object.keys(mgmtProps).sort(),
"navigator.mozApps.mgmt has only the expected properties");
for (var p in mgmtProps) {
is(typeof navigator.mozApps.mgmt[p], mgmtProps[p], "typeof mgmt." + p);
--- a/dom/webidl/Apps.webidl
+++ b/dom/webidl/Apps.webidl
@@ -74,22 +74,28 @@ interface DOMApplication : EventTarget {
Promise<sequence<MozInterAppMessagePort>> getConnections();
// Receipts handling functions.
DOMRequest addReceipt(optional DOMString receipt);
DOMRequest removeReceipt(optional DOMString receipt);
DOMRequest replaceReceipt(optional DOMString oldReceipt,
optional DOMString newReceipt);
+
+ // Export this app as a shareable Blob.
+ Promise<Blob> export();
};
[JSImplementation="@mozilla.org/webapps/manager;1",
ChromeOnly,
CheckPermissions="webapps-manage"]
interface DOMApplicationsManager : EventTarget {
DOMRequest getAll();
DOMRequest getNotInstalled();
void applyDownload(DOMApplication app);
DOMRequest uninstall(DOMApplication app);
+ Promise<DOMApplication> import(Blob blob);
+ Promise<any> extractManifest(Blob blob);
+
attribute EventHandler oninstall;
attribute EventHandler onuninstall;
};