gecko/toolkit/devtools/server/actors/webapps.js

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");