mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1030 lines
33 KiB
JavaScript
1030 lines
33 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";
|
|
|
|
let Cu = Components.utils;
|
|
let Cc = Components.classes;
|
|
let Ci = Components.interfaces;
|
|
let CC = Components.Constructor;
|
|
|
|
Cu.import("resource://gre/modules/osfile.jsm");
|
|
|
|
let promise;
|
|
|
|
function debug(aMsg) {
|
|
/*
|
|
Cc["@mozilla.org/consoleservice;1"]
|
|
.getService(Ci.nsIConsoleService)
|
|
.logStringMessage("--*-- WebappsActor : " + aMsg);
|
|
*/
|
|
}
|
|
|
|
function PackageUploadActor(aPath, aFile) {
|
|
this._path = aPath;
|
|
this._file = aFile;
|
|
this.size = 0;
|
|
}
|
|
|
|
PackageUploadActor.prototype = {
|
|
actorPrefix: "packageUploadActor",
|
|
|
|
/**
|
|
* This method isn't exposed to the client.
|
|
* It is meant to be called by server code, in order to get
|
|
* access to the temporary file out of the actor ID.
|
|
*/
|
|
getFilePath: function () {
|
|
return this._path;
|
|
},
|
|
|
|
/**
|
|
* This method allows you to upload a piece of file.
|
|
* It expects a chunk argument that is the a string to write to the file.
|
|
*/
|
|
chunk: function (aRequest) {
|
|
let chunk = aRequest.chunk;
|
|
if (!chunk || chunk.length <= 0) {
|
|
return {error: "parameterError",
|
|
message: "Missing or invalid chunk argument"};
|
|
}
|
|
// Translate the string used to transfer the chunk over JSON
|
|
// back to a typed array
|
|
let data = new Uint8Array(chunk.length);
|
|
for (let i = 0, l = chunk.length; i < l ; i++) {
|
|
data[i] = chunk.charCodeAt(i);
|
|
}
|
|
return this._file.write(data)
|
|
.then((written) => {
|
|
this.size += written;
|
|
return {
|
|
written: written,
|
|
size: this.size
|
|
};
|
|
});
|
|
},
|
|
|
|
/**
|
|
* This method needs to be called, when you are done uploading
|
|
* chunks, before trying to access/use the temporary file.
|
|
* Otherwise, the file may be partially written
|
|
* and also be locked.
|
|
*/
|
|
done: function (aRequest) {
|
|
return this._file.close();
|
|
},
|
|
|
|
/**
|
|
* This method allows you to delete the temporary file,
|
|
* when you are done using it.
|
|
*/
|
|
remove: function (aRequest) {
|
|
this._cleanupFile();
|
|
|
|
return {};
|
|
},
|
|
|
|
_cleanupFile: function () {
|
|
try {
|
|
this._file.close();
|
|
} catch(e) {}
|
|
try {
|
|
OS.File.remove(this._path);
|
|
} catch(e) {}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The request types this actor can handle.
|
|
*/
|
|
PackageUploadActor.prototype.requestTypes = {
|
|
"chunk": PackageUploadActor.prototype.chunk,
|
|
"done": PackageUploadActor.prototype.done,
|
|
"remove": PackageUploadActor.prototype.remove
|
|
};
|
|
|
|
/**
|
|
* Creates a WebappsActor. WebappsActor provides remote access to
|
|
* install apps.
|
|
*/
|
|
function WebappsActor(aConnection) {
|
|
debug("init");
|
|
// Load actor dependencies lazily as this actor require extra environnement
|
|
// preparation to work (like have a profile setup in xpcshell tests)
|
|
|
|
Cu.import("resource://gre/modules/Webapps.jsm");
|
|
Cu.import("resource://gre/modules/AppsUtils.jsm");
|
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
|
Cu.import('resource://gre/modules/Services.jsm');
|
|
promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
|
|
|
|
// Keep reference of already created app actors.
|
|
// key: app frame message manager, value: ContentTabActor's grip() value
|
|
this._appActorsMap = new Map();
|
|
|
|
this.conn = aConnection;
|
|
this._uploads = [];
|
|
this._actorPool = new ActorPool(this.conn);
|
|
this.conn.addActorPool(this._actorPool);
|
|
}
|
|
|
|
WebappsActor.prototype = {
|
|
actorPrefix: "webapps",
|
|
|
|
disconnect: function () {
|
|
// When we stop using this actor, we should ensure removing all files.
|
|
for (let upload of this._uploads) {
|
|
upload.remove();
|
|
}
|
|
this._uploads = null;
|
|
|
|
this.conn.removeActorPool(this._actorPool);
|
|
this._actorPool = null;
|
|
this.conn = null;
|
|
},
|
|
|
|
_registerApp: function wa_actorRegisterApp(aDeferred, aApp, aId, aDir) {
|
|
debug("registerApp");
|
|
let reg = DOMApplicationRegistry;
|
|
let self = this;
|
|
|
|
// Clean up the deprecated manifest cache if needed.
|
|
if (aId in reg._manifestCache) {
|
|
delete reg._manifestCache[aId];
|
|
}
|
|
|
|
aApp.installTime = Date.now();
|
|
aApp.installState = "installed";
|
|
aApp.removable = true;
|
|
aApp.id = aId;
|
|
aApp.basePath = reg.getWebAppsBasePath();
|
|
aApp.localId = (aId in reg.webapps) ? reg.webapps[aId].localId
|
|
: reg._nextLocalId();
|
|
|
|
reg.webapps[aId] = aApp;
|
|
reg.updatePermissionsForApp(aId);
|
|
|
|
reg._readManifests([{ id: aId }], function(aResult) {
|
|
let manifest = aResult[0].manifest;
|
|
aApp.name = manifest.name;
|
|
reg.updateAppHandlers(null, manifest, aApp);
|
|
|
|
reg._saveApps(function() {
|
|
aApp.manifest = manifest;
|
|
|
|
// Needed to evict manifest cache on content side
|
|
// (has to be dispatched first, otherwise other messages like
|
|
// Install:Return:OK are going to use old manifest version)
|
|
reg.broadcastMessage("Webapps:UpdateState", {
|
|
app: aApp,
|
|
manifest: manifest,
|
|
manifestURL: aApp.manifestURL
|
|
});
|
|
reg.broadcastMessage("Webapps:FireEvent", {
|
|
eventType: ["downloadsuccess", "downloadapplied"],
|
|
manifestURL: aApp.manifestURL
|
|
});
|
|
reg.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp });
|
|
reg.broadcastMessage("Webapps:Install:Return:OK", {
|
|
app: aApp,
|
|
oid: "foo",
|
|
requestID: "bar"
|
|
});
|
|
|
|
Services.obs.notifyObservers(null, "webapps-installed",
|
|
JSON.stringify({ manifestURL: aApp.manifestURL }));
|
|
|
|
delete aApp.manifest;
|
|
aDeferred.resolve({ appId: aId, path: aDir.path });
|
|
|
|
// We can't have appcache for packaged apps.
|
|
if (!aApp.origin.startsWith("app://")) {
|
|
reg.startOfflineCacheDownload(new ManifestHelper(manifest, aApp.origin));
|
|
}
|
|
});
|
|
// Cleanup by removing the temporary directory.
|
|
if (aDir.exists())
|
|
aDir.remove(true);
|
|
});
|
|
},
|
|
|
|
_sendError: function wa_actorSendError(aDeferred, aMsg, aId) {
|
|
debug("Sending error: " + aMsg);
|
|
aDeferred.resolve({
|
|
error: "installationFailed",
|
|
message: aMsg,
|
|
appId: aId
|
|
});
|
|
},
|
|
|
|
_getAppType: function wa_actorGetAppType(aType) {
|
|
let type = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
|
|
|
|
if (aType) {
|
|
type = aType == "privileged" ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
|
|
: aType == "certified" ? Ci.nsIPrincipal.APP_STATUS_CERTIFIED
|
|
: Ci.nsIPrincipal.APP_STATUS_INSTALLED;
|
|
}
|
|
|
|
return type;
|
|
},
|
|
|
|
uploadPackage: function () {
|
|
debug("uploadPackage\n");
|
|
let tmpDir = FileUtils.getDir("TmpD", ["file-upload"], true, false);
|
|
if (!tmpDir.exists() || !tmpDir.isDirectory()) {
|
|
return {error: "fileAccessError",
|
|
message: "Unable to create temporary folder"};
|
|
}
|
|
let tmpFile = tmpDir;
|
|
tmpFile.append("package.zip");
|
|
tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
|
|
if (!tmpFile.exists() || !tmpDir.isFile()) {
|
|
return {error: "fileAccessError",
|
|
message: "Unable to create temporary file"};
|
|
}
|
|
|
|
return OS.File.open(tmpFile.path, { write: true, truncate: true })
|
|
.then((file) => {
|
|
let actor = new PackageUploadActor(tmpFile.path, file);
|
|
this._actorPool.addActor(actor);
|
|
this._uploads.push(actor);
|
|
return { actor: actor.actorID };
|
|
});
|
|
},
|
|
|
|
installHostedApp: function wa_actorInstallHosted(aDir, aId, aReceipts,
|
|
aManifest, aMetadata) {
|
|
debug("installHostedApp");
|
|
let self = this;
|
|
let deferred = promise.defer();
|
|
|
|
function readManifest() {
|
|
if (aManifest) {
|
|
return promise.resolve(aManifest);
|
|
} else {
|
|
let deferred = promise.defer();
|
|
let manFile = aDir.clone();
|
|
manFile.append("manifest.webapp");
|
|
DOMApplicationRegistry._loadJSONAsync(manFile, function(aManifest) {
|
|
if (!aManifest) {
|
|
deferred.reject("Error parsing manifest.webapp.");
|
|
} else {
|
|
deferred.resolve(aManifest);
|
|
}
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
}
|
|
function checkSideloading(aManifest) {
|
|
return self._getAppType(aManifest.type);
|
|
}
|
|
function writeManifest(aAppType) {
|
|
// Move manifest.webapp to the destination directory.
|
|
// The destination directory for this app.
|
|
let installDir = DOMApplicationRegistry._getAppDir(aId);
|
|
if (aManifest) {
|
|
let deferred = promise.defer();
|
|
let manFile = installDir.clone();
|
|
manFile.append("manifest.webapp");
|
|
DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest), function () {
|
|
deferred.resolve(aAppType);
|
|
});
|
|
return deferred.promise;
|
|
} else {
|
|
let manFile = aDir.clone();
|
|
manFile.append("manifest.webapp");
|
|
manFile.moveTo(installDir, "manifest.webapp");
|
|
}
|
|
return null;
|
|
}
|
|
function readMetadata(aAppType) {
|
|
if (aMetadata) {
|
|
return { metadata: aMetadata, appType: aAppType };
|
|
}
|
|
// Read the origin and manifest url from metadata.json
|
|
let deferred = promise.defer();
|
|
let metaFile = aDir.clone();
|
|
metaFile.append("metadata.json");
|
|
DOMApplicationRegistry._loadJSONAsync(metaFile, function(aMetadata) {
|
|
if (!aMetadata) {
|
|
deferred.reject("Error parsing metadata.json.");
|
|
return;
|
|
}
|
|
if (!aMetadata.origin) {
|
|
deferred.reject("Missing 'origin' property in metadata.json");
|
|
return;
|
|
}
|
|
deferred.resolve({ metadata: aMetadata, appType: aAppType });
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
let runnable = {
|
|
run: function run() {
|
|
try {
|
|
readManifest().
|
|
then(writeManifest).
|
|
then(checkSideloading).
|
|
then(readMetadata).
|
|
then(function ({ metadata, appType }) {
|
|
let origin = metadata.origin;
|
|
let manifestURL = metadata.manifestURL ||
|
|
origin + "/manifest.webapp";
|
|
// Create a fake app object with the minimum set of properties we need.
|
|
let app = {
|
|
origin: origin,
|
|
installOrigin: metadata.installOrigin || origin,
|
|
manifestURL: manifestURL,
|
|
appStatus: appType,
|
|
receipts: aReceipts,
|
|
};
|
|
|
|
self._registerApp(deferred, app, aId, aDir);
|
|
}, function (error) {
|
|
self._sendError(deferred, error, aId);
|
|
});
|
|
} catch(e) {
|
|
// If anything goes wrong, just send it back.
|
|
self._sendError(deferred, e.toString(), aId);
|
|
}
|
|
}
|
|
}
|
|
|
|
Services.tm.currentThread.dispatch(runnable,
|
|
Ci.nsIThread.DISPATCH_NORMAL);
|
|
return deferred.promise;
|
|
},
|
|
|
|
installPackagedApp: function wa_actorInstallPackaged(aDir, aId, aReceipts) {
|
|
debug("installPackagedApp");
|
|
let self = this;
|
|
let deferred = promise.defer();
|
|
|
|
let runnable = {
|
|
run: function run() {
|
|
try {
|
|
// Open the app zip package
|
|
let zipFile = aDir.clone();
|
|
zipFile.append("application.zip");
|
|
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
|
|
.createInstance(Ci.nsIZipReader);
|
|
zipReader.open(zipFile);
|
|
|
|
// Read app manifest `manifest.webapp` from `application.zip`
|
|
let istream = zipReader.getInputStream("manifest.webapp");
|
|
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
|
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
converter.charset = "UTF-8";
|
|
let jsonString = converter.ConvertToUnicode(
|
|
NetUtil.readInputStreamToString(istream, istream.available())
|
|
);
|
|
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(jsonString);
|
|
} catch(e) {
|
|
self._sendError(deferred, "Error Parsing manifest.webapp: " + e, aId);
|
|
}
|
|
|
|
let appType = self._getAppType(manifest.type);
|
|
|
|
// Privileged and certified packaged apps can setup a custom origin
|
|
// via `origin` manifest property
|
|
let id = aId;
|
|
if (appType >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
|
|
manifest.origin !== undefined) {
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(manifest.origin, null, null);
|
|
} catch(e) {
|
|
self._sendError(deferred, "Invalid origin in webapp's manifest", aId);
|
|
}
|
|
|
|
if (uri.scheme != "app") {
|
|
self._sendError(deferred, "Invalid origin in webapp's manifest", aId);
|
|
}
|
|
id = uri.prePath.substring(6);
|
|
}
|
|
|
|
// Only after security checks are made and after final app id is computed
|
|
// we can move application.zip to the destination directory, and
|
|
// extract manifest.webapp there.
|
|
let installDir = DOMApplicationRegistry._getAppDir(id);
|
|
let manFile = installDir.clone();
|
|
manFile.append("manifest.webapp");
|
|
zipReader.extract("manifest.webapp", manFile);
|
|
zipReader.close();
|
|
zipFile.moveTo(installDir, "application.zip");
|
|
|
|
let origin = "app://" + id;
|
|
let manifestURL = origin + "/manifest.webapp";
|
|
|
|
// Refresh application.zip content (e.g. reinstall app), as done here:
|
|
// http://hg.mozilla.org/mozilla-central/annotate/aaefec5d34f8/dom/apps/src/Webapps.jsm#l1125
|
|
// Do it in parent process for the simulator
|
|
let jar = installDir.clone();
|
|
jar.append("application.zip");
|
|
Services.obs.notifyObservers(jar, "flush-cache-entry", null);
|
|
|
|
// And then in app content process
|
|
// This function will be evaluated in the scope of the content process
|
|
// frame script. That will flush the jar cache for this app and allow
|
|
// loading fresh updated resources if we reload its document.
|
|
let FlushFrameScript = function (path) {
|
|
let jar = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(Components.interfaces.nsILocalFile);
|
|
jar.initWithPath(path);
|
|
let obs = Components.classes["@mozilla.org/observer-service;1"]
|
|
.getService(Components.interfaces.nsIObserverService);
|
|
obs.notifyObservers(jar, "flush-cache-entry", null);
|
|
};
|
|
for each (let frame in self._appFrames()) {
|
|
if (frame.getAttribute("mozapp") == manifestURL) {
|
|
let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
|
|
mm.loadFrameScript("data:," +
|
|
encodeURIComponent("(" + FlushFrameScript.toString() + ")" +
|
|
"('" + jar.path + "')"), false);
|
|
}
|
|
}
|
|
|
|
// Create a fake app object with the minimum set of properties we need.
|
|
let app = {
|
|
origin: origin,
|
|
installOrigin: origin,
|
|
manifestURL: manifestURL,
|
|
appStatus: appType,
|
|
receipts: aReceipts,
|
|
}
|
|
|
|
self._registerApp(deferred, app, id, aDir);
|
|
} catch(e) {
|
|
// If anything goes wrong, just send it back.
|
|
self._sendError(deferred, e.toString(), aId);
|
|
}
|
|
}
|
|
}
|
|
|
|
Services.tm.currentThread.dispatch(runnable,
|
|
Ci.nsIThread.DISPATCH_NORMAL);
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* @param appId : The id of the app we want to install. We will look for
|
|
* the files for the app in $TMP/b2g/$appId :
|
|
* For packaged apps: application.zip
|
|
* For hosted apps: metadata.json and manifest.webapp
|
|
*/
|
|
install: function wa_actorInstall(aRequest) {
|
|
debug("install");
|
|
|
|
let appId = aRequest.appId;
|
|
let reg = DOMApplicationRegistry;
|
|
if (!appId) {
|
|
appId = reg.makeAppId();
|
|
}
|
|
|
|
// Check that we are not overriding a preinstalled application.
|
|
if (appId in reg.webapps && reg.webapps[appId].removable === false) {
|
|
return { error: "badParameterType",
|
|
message: "The application " + appId + " can't be overriden."
|
|
}
|
|
}
|
|
|
|
let appDir = FileUtils.getDir("TmpD", ["b2g", appId], false, false);
|
|
|
|
if (aRequest.upload) {
|
|
// Ensure creating the directory (recursively)
|
|
appDir = FileUtils.getDir("TmpD", ["b2g", appId], true, false);
|
|
let actor = this.conn.getActor(aRequest.upload);
|
|
if (!actor) {
|
|
return { error: "badParameter",
|
|
message: "Unable to find upload actor '" + aRequest.upload
|
|
+ "'" };
|
|
}
|
|
let appFile = FileUtils.File(actor.getFilePath());
|
|
if (!appFile.exists()) {
|
|
return { error: "badParameter",
|
|
message: "The uploaded file doesn't exist on device" };
|
|
}
|
|
appFile.moveTo(appDir, "application.zip");
|
|
} else if ((!appDir || !appDir.exists()) &&
|
|
!aRequest.manifest && !aRequest.metadata) {
|
|
return { error: "badParameterType",
|
|
message: "missing directory " + appDir.path
|
|
};
|
|
}
|
|
|
|
let testFile = appDir.clone();
|
|
testFile.append("application.zip");
|
|
|
|
let receipts = (aRequest.receipts && Array.isArray(aRequest.receipts))
|
|
? aRequest.receipts
|
|
: [];
|
|
|
|
if (testFile.exists()) {
|
|
return this.installPackagedApp(appDir, appId, receipts);
|
|
}
|
|
|
|
let manifest, metadata;
|
|
let missing =
|
|
["manifest.webapp", "metadata.json"]
|
|
.some(function(aName) {
|
|
testFile = appDir.clone();
|
|
testFile.append(aName);
|
|
return !testFile.exists();
|
|
});
|
|
if (missing) {
|
|
if (aRequest.manifest && aRequest.metadata &&
|
|
aRequest.metadata.origin) {
|
|
manifest = aRequest.manifest;
|
|
metadata = aRequest.metadata;
|
|
} else {
|
|
try {
|
|
appDir.remove(true);
|
|
} catch(e) {}
|
|
return { error: "badParameterType",
|
|
message: "hosted app file and manifest/metadata fields " +
|
|
"are missing"
|
|
};
|
|
}
|
|
}
|
|
|
|
return this.installHostedApp(appDir, appId, receipts, manifest, metadata);
|
|
},
|
|
|
|
getAll: function wa_actorGetAll(aRequest) {
|
|
debug("getAll");
|
|
|
|
let deferred = promise.defer();
|
|
let reg = DOMApplicationRegistry;
|
|
reg.getAll(apps => {
|
|
deferred.resolve({ apps: this._filterAllowedApps(apps) });
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
getApp: function wa_actorGetApp(aRequest) {
|
|
debug("getApp");
|
|
|
|
let manifestURL = aRequest.manifestURL;
|
|
if (!manifestURL) {
|
|
return { error: "missingParameter",
|
|
message: "missing parameter manifestURL" };
|
|
}
|
|
|
|
let reg = DOMApplicationRegistry;
|
|
let app = reg.getAppByManifestURL(manifestURL);
|
|
if (!app) {
|
|
return { error: "appNotFound" };
|
|
}
|
|
|
|
if (this._isAppAllowedForManifest(app.manifestURL)) {
|
|
let deferred = promise.defer();
|
|
reg.getManifestFor(manifestURL, function (manifest) {
|
|
app.manifest = manifest;
|
|
deferred.resolve({app: app});
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
return { error: "forbidden" };
|
|
},
|
|
|
|
_areCertifiedAppsAllowed: function wa__areCertifiedAppsAllowed() {
|
|
let pref = "devtools.debugger.forbid-certified-apps";
|
|
return !Services.prefs.getBoolPref(pref);
|
|
},
|
|
|
|
_isAppAllowedForManifest: function wa__isAppAllowedForManifest(aManifest) {
|
|
if (this._areCertifiedAppsAllowed()) {
|
|
return true;
|
|
}
|
|
let type = this._getAppType(aManifest.type);
|
|
return type !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
|
|
},
|
|
|
|
_filterAllowedApps: function wa__filterAllowedApps(aApps) {
|
|
return aApps.filter(app => this._isAppAllowedForManifest(app.manifest));
|
|
},
|
|
|
|
_isAppAllowedForURL: function wa__isAppAllowedForURL(aManifestURL) {
|
|
return this._findManifestByURL(aManifestURL).then(manifest => {
|
|
return this._isAppAllowedForManifest(manifest);
|
|
});
|
|
},
|
|
|
|
uninstall: function wa_actorUninstall(aRequest) {
|
|
debug("uninstall");
|
|
|
|
let manifestURL = aRequest.manifestURL;
|
|
if (!manifestURL) {
|
|
return { error: "missingParameter",
|
|
message: "missing parameter manifestURL" };
|
|
}
|
|
|
|
let deferred = promise.defer();
|
|
let reg = DOMApplicationRegistry;
|
|
reg.uninstall(
|
|
manifestURL,
|
|
function onsuccess() {
|
|
deferred.resolve({});
|
|
},
|
|
function onfailure(reason) {
|
|
deferred.resolve({ error: reason });
|
|
}
|
|
);
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
_findManifestByURL: function wa__findManifestByURL(aManifestURL) {
|
|
let deferred = promise.defer();
|
|
|
|
let reg = DOMApplicationRegistry;
|
|
let id = reg._appIdForManifestURL(aManifestURL);
|
|
|
|
reg._readManifests([{ id: id }], function (aResults) {
|
|
deferred.resolve(aResults[0].manifest);
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
getIconAsDataURL: function (aRequest) {
|
|
debug("getIconAsDataURL");
|
|
|
|
let manifestURL = aRequest.manifestURL;
|
|
if (!manifestURL) {
|
|
return { error: "missingParameter",
|
|
message: "missing parameter manifestURL" };
|
|
}
|
|
|
|
let reg = DOMApplicationRegistry;
|
|
let app = reg.getAppByManifestURL(manifestURL);
|
|
if (!app) {
|
|
return { error: "wrongParameter",
|
|
message: "No application for " + manifestURL };
|
|
}
|
|
|
|
let deferred = promise.defer();
|
|
|
|
this._findManifestByURL(manifestURL).then(jsonManifest => {
|
|
let manifest = new ManifestHelper(jsonManifest, app.origin);
|
|
let iconURL = manifest.iconURLForSize(aRequest.size || 128);
|
|
if (!iconURL) {
|
|
deferred.resolve({
|
|
error: "noIcon",
|
|
message: "This app has no icon"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Download the URL as a blob
|
|
// bug 899177: there is a bug with xhr and app:// and jar:// uris
|
|
// that ends up forcing the content type to application/xml.
|
|
let req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
|
|
.createInstance(Ci.nsIXMLHttpRequest);
|
|
req.open("GET", iconURL, false);
|
|
req.responseType = "blob";
|
|
|
|
try {
|
|
req.send(null);
|
|
} catch(e) {
|
|
deferred.resolve({
|
|
error: "noIcon",
|
|
message: "The icon file '" + iconURL + "' doesn't exists"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Convert the blog to a base64 encoded data URI
|
|
let reader = Cc["@mozilla.org/files/filereader;1"]
|
|
.createInstance(Ci.nsIDOMFileReader);
|
|
reader.onload = function () {
|
|
deferred.resolve({
|
|
url: reader.result
|
|
});
|
|
};
|
|
reader.onerror = function () {
|
|
deferred.resolve({
|
|
error: reader.error.name,
|
|
message: String(reader.error)
|
|
});
|
|
};
|
|
reader.readAsDataURL(req.response);
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
launch: function wa_actorLaunch(aRequest) {
|
|
debug("launch");
|
|
|
|
let manifestURL = aRequest.manifestURL;
|
|
if (!manifestURL) {
|
|
return { error: "missingParameter",
|
|
message: "missing parameter manifestURL" };
|
|
}
|
|
|
|
let deferred = promise.defer();
|
|
|
|
DOMApplicationRegistry.launch(
|
|
aRequest.manifestURL,
|
|
aRequest.startPoint || "",
|
|
Date.now(),
|
|
function onsuccess() {
|
|
deferred.resolve({});
|
|
},
|
|
function onfailure(reason) {
|
|
deferred.resolve({ error: reason });
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
close: function wa_actorLaunch(aRequest) {
|
|
debug("close");
|
|
|
|
let manifestURL = aRequest.manifestURL;
|
|
if (!manifestURL) {
|
|
return { error: "missingParameter",
|
|
message: "missing parameter manifestURL" };
|
|
}
|
|
|
|
let reg = DOMApplicationRegistry;
|
|
let app = reg.getAppByManifestURL(manifestURL);
|
|
if (!app) {
|
|
return { error: "missingParameter",
|
|
message: "No application for " + manifestURL };
|
|
}
|
|
|
|
reg.close(app);
|
|
|
|
return {};
|
|
},
|
|
|
|
_appFrames: function () {
|
|
// For now, we only support app frames on b2g
|
|
if (Services.appinfo.ID != "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
|
|
return;
|
|
}
|
|
// Register the system app
|
|
let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
|
|
let systemAppFrame = chromeWindow.shell.contentBrowser;
|
|
yield systemAppFrame;
|
|
|
|
// Register apps hosted in the system app. i.e. the homescreen, all regular
|
|
// apps and the keyboard.
|
|
// Bookmark apps and other system app internal frames like captive portal
|
|
// are also hosted in system app, but they are not using mozapp attribute.
|
|
let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]");
|
|
for (let i = 0; i < frames.length; i++) {
|
|
yield frames[i];
|
|
}
|
|
},
|
|
|
|
listRunningApps: function (aRequest) {
|
|
debug("listRunningApps\n");
|
|
|
|
let appPromises = [];
|
|
let apps = [];
|
|
|
|
for each (let frame in this._appFrames()) {
|
|
let manifestURL = frame.getAttribute("mozapp");
|
|
|
|
appPromises.push(this._isAppAllowedForURL(manifestURL).then(allowed => {
|
|
if (allowed) {
|
|
apps.push(manifestURL);
|
|
}
|
|
}));
|
|
}
|
|
|
|
return promise.all(appPromises).then(() => {
|
|
return { apps: apps };
|
|
});
|
|
},
|
|
|
|
_connectToApp: function (aFrame) {
|
|
let deferred = Promise.defer();
|
|
|
|
let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
|
|
mm.loadFrameScript("resource://gre/modules/devtools/server/child.js", false);
|
|
|
|
let childTransport, prefix;
|
|
|
|
let onActorCreated = makeInfallible(function (msg) {
|
|
mm.removeMessageListener("debug:actor", onActorCreated);
|
|
|
|
dump("***** Got debug:actor\n");
|
|
let { actor, appId } = msg.json;
|
|
prefix = msg.json.prefix;
|
|
|
|
// Pipe Debugger message from/to parent/child via the message manager
|
|
childTransport = new ChildDebuggerTransport(mm, prefix);
|
|
childTransport.hooks = {
|
|
onPacket: this.conn.send.bind(this.conn),
|
|
onClosed: function () {}
|
|
};
|
|
childTransport.ready();
|
|
|
|
this.conn.setForwarding(prefix, childTransport);
|
|
|
|
debug("establishing forwarding for app with prefix " + prefix);
|
|
|
|
this._appActorsMap.set(mm, actor);
|
|
|
|
deferred.resolve(actor);
|
|
}).bind(this);
|
|
mm.addMessageListener("debug:actor", onActorCreated);
|
|
|
|
let onMessageManagerDisconnect = makeInfallible(function (subject, topic, data) {
|
|
if (subject == mm) {
|
|
Services.obs.removeObserver(onMessageManagerDisconnect, topic);
|
|
if (childTransport) {
|
|
// If we have a child transport, the actor has already
|
|
// been created. We need to stop using this message manager.
|
|
childTransport.close();
|
|
this.conn.cancelForwarding(prefix);
|
|
} else {
|
|
// Otherwise, the app has been closed before the actor
|
|
// had a chance to be created, so we are not able to create
|
|
// the actor.
|
|
deferred.resolve(null);
|
|
}
|
|
let actor = this._appActorsMap.get(mm);
|
|
if (actor) {
|
|
// The ContentAppActor within the child process doesn't necessary
|
|
// have to time to uninitialize itself when the app is closed/killed.
|
|
// So ensure telling the client that the related actor is detached.
|
|
this.conn.send({ from: actor.actor,
|
|
type: "tabDetached" });
|
|
this._appActorsMap.delete(mm);
|
|
}
|
|
}
|
|
}).bind(this);
|
|
Services.obs.addObserver(onMessageManagerDisconnect,
|
|
"message-manager-disconnect", false);
|
|
|
|
let prefixStart = this.conn.prefix + "child";
|
|
mm.sendAsyncMessage("debug:connect", { prefix: prefixStart });
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
getAppActor: function ({ manifestURL }) {
|
|
debug("getAppActor\n");
|
|
|
|
let appFrame = null;
|
|
for each (let frame in this._appFrames()) {
|
|
if (frame.getAttribute("mozapp") == manifestURL) {
|
|
appFrame = frame;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let notFoundError = {
|
|
error: "appNotFound",
|
|
message: "Unable to find any opened app whose manifest " +
|
|
"is '" + manifestURL + "'"
|
|
};
|
|
|
|
if (!appFrame) {
|
|
return notFoundError;
|
|
}
|
|
|
|
return this._isAppAllowedForURL(manifestURL).then(allowed => {
|
|
if (!allowed) {
|
|
return notFoundError;
|
|
}
|
|
|
|
// Only create a new actor, if we haven't already
|
|
// instanciated one for this connection.
|
|
let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
|
|
.frameLoader
|
|
.messageManager;
|
|
let actor = this._appActorsMap.get(mm);
|
|
if (!actor) {
|
|
return this._connectToApp(appFrame)
|
|
.then(function (actor) ({ actor: actor }));
|
|
}
|
|
|
|
return { actor: actor };
|
|
});
|
|
},
|
|
|
|
watchApps: function () {
|
|
this._openedApps = new Set();
|
|
// For now, app open/close events are only implement on b2g
|
|
if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
|
|
let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
|
|
let systemAppFrame = chromeWindow.getContentWindow();
|
|
systemAppFrame.addEventListener("appwillopen", this);
|
|
systemAppFrame.addEventListener("appterminated", this);
|
|
}
|
|
Services.obs.addObserver(this, "webapps-installed", false);
|
|
Services.obs.addObserver(this, "webapps-uninstall", false);
|
|
|
|
return {};
|
|
},
|
|
|
|
unwatchApps: function () {
|
|
this._openedApps = null;
|
|
if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
|
|
let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
|
|
let systemAppFrame = chromeWindow.getContentWindow();
|
|
systemAppFrame.removeEventListener("appwillopen", this);
|
|
systemAppFrame.removeEventListener("appterminated", this);
|
|
}
|
|
Services.obs.removeObserver(this, "webapps-installed", false);
|
|
Services.obs.removeObserver(this, "webapps-uninstall", false);
|
|
|
|
return {};
|
|
},
|
|
|
|
handleEvent: function (event) {
|
|
let manifestURL;
|
|
switch(event.type) {
|
|
case "appwillopen":
|
|
let frame = event.target;
|
|
manifestURL = frame.getAttribute("mozapp")
|
|
|
|
// Ignore the event if we already received an appwillopen for this app
|
|
// (appwillopen is also fired when the app has been moved to background
|
|
// and get back to foreground)
|
|
if (this._openedApps.has(manifestURL)) {
|
|
return;
|
|
}
|
|
this._openedApps.add(manifestURL);
|
|
|
|
this._isAppAllowedForURL(manifestURL).then(allowed => {
|
|
if (allowed) {
|
|
this.conn.send({ from: this.actorID,
|
|
type: "appOpen",
|
|
manifestURL: manifestURL
|
|
});
|
|
}
|
|
});
|
|
|
|
break;
|
|
|
|
case "appterminated":
|
|
manifestURL = event.detail.manifestURL;
|
|
this._openedApps.delete(manifestURL);
|
|
|
|
this._isAppAllowedForURL(manifestURL).then(allowed => {
|
|
if (allowed) {
|
|
this.conn.send({ from: this.actorID,
|
|
type: "appClose",
|
|
manifestURL: manifestURL
|
|
});
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
},
|
|
|
|
observe: function (subject, topic, data) {
|
|
let app = JSON.parse(data);
|
|
if (topic == "webapps-installed") {
|
|
this.conn.send({ from: this.actorID,
|
|
type: "appInstall",
|
|
manifestURL: app.manifestURL
|
|
});
|
|
} else if (topic == "webapps-uninstall") {
|
|
this.conn.send({ from: this.actorID,
|
|
type: "appUninstall",
|
|
manifestURL: app.manifestURL
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The request types this actor can handle.
|
|
*/
|
|
WebappsActor.prototype.requestTypes = {
|
|
"install": WebappsActor.prototype.install
|
|
};
|
|
|
|
// Until we implement unix domain socket, we only enable app install
|
|
// only on production devices
|
|
if (Services.prefs.getBoolPref("devtools.debugger.enable-content-actors")) {
|
|
let requestTypes = WebappsActor.prototype.requestTypes;
|
|
requestTypes.uploadPackage = WebappsActor.prototype.uploadPackage;
|
|
requestTypes.getAll = WebappsActor.prototype.getAll;
|
|
requestTypes.getApp = WebappsActor.prototype.getApp;
|
|
requestTypes.launch = WebappsActor.prototype.launch;
|
|
requestTypes.close = WebappsActor.prototype.close;
|
|
requestTypes.uninstall = WebappsActor.prototype.uninstall;
|
|
requestTypes.listRunningApps = WebappsActor.prototype.listRunningApps;
|
|
requestTypes.getAppActor = WebappsActor.prototype.getAppActor;
|
|
requestTypes.watchApps = WebappsActor.prototype.watchApps;
|
|
requestTypes.unwatchApps = WebappsActor.prototype.unwatchApps;
|
|
requestTypes.getIconAsDataURL = WebappsActor.prototype.getIconAsDataURL;
|
|
}
|
|
|
|
DebuggerServer.addGlobalActor(WebappsActor, "webappsActor");
|