/* 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"; var {Cu, Cc, Ci} = require("chrome"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/UserCustomizations.jsm"); var promise = require("promise"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var { ActorPool } = require("devtools/server/actors/common"); var { DebuggerServer } = require("devtools/server/main"); var Services = require("Services"); // Comma separated list of permissions that a sideloaded app can't ask for const UNSAFE_PERMISSIONS = Services.prefs.getCharPref("devtools.apps.forbidden-permissions"); var FramesMock = null; exports.setFramesMock = function (mock) { FramesMock = mock; }; DevToolsUtils.defineLazyGetter(this, "Frames", () => { // Offer a way for unit test to provide a mock if (FramesMock) { return FramesMock; } try { return Cu.import("resource://gre/modules/Frames.jsm", {}).Frames; } catch(e) {} return null; }); function debug(aMsg) { /* Cc["@mozilla.org/consoleservice;1"] .getService(Ci.nsIConsoleService) .logStringMessage("--*-- WebappsActor : " + aMsg); */ } function PackageUploadActor(file) { this._file = file; this._path = file.path; } PackageUploadActor.fromRequest = function(request, file) { if (request.bulk) { return new PackageUploadBulkActor(file); } return new PackageUploadJSONActor(file); }; PackageUploadActor.prototype = { /** * 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. */ get filePath() { return this._path; }, get openedFile() { if (this._openedFile) { return this._openedFile; } this._openedFile = this._openFile(); return this._openedFile; }, /** * This method allows you to delete the temporary file, * when you are done using it. */ remove: function () { this._cleanupFile(); return {}; }, _cleanupFile: function () { try { this._closeFile(); } catch(e) {} try { OS.File.remove(this._path); } catch(e) {} } }; /** * Create a new JSON package upload actor. * @param file nsIFile temporary file to write to */ function PackageUploadJSONActor(file) { PackageUploadActor.call(this, file); this._size = 0; } PackageUploadJSONActor.prototype = Object.create(PackageUploadActor.prototype); PackageUploadJSONActor.prototype.actorPrefix = "packageUploadJSONActor"; PackageUploadJSONActor.prototype._openFile = function() { return OS.File.open(this._path, { write: true, truncate: true }); }; PackageUploadJSONActor.prototype._closeFile = function() { this.openedFile.then(file => file.close()); }; /** * 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. */ PackageUploadJSONActor.prototype.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.openedFile .then(file => 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. */ PackageUploadJSONActor.prototype.done = function() { this._closeFile(); return {}; }; /** * The request types this actor can handle. */ PackageUploadJSONActor.prototype.requestTypes = { "chunk": PackageUploadJSONActor.prototype.chunk, "done": PackageUploadJSONActor.prototype.done, "remove": PackageUploadJSONActor.prototype.remove }; /** * Create a new bulk package upload actor. * @param file nsIFile temporary file to write to */ function PackageUploadBulkActor(file) { PackageUploadActor.call(this, file); } PackageUploadBulkActor.prototype = Object.create(PackageUploadActor.prototype); PackageUploadBulkActor.prototype.actorPrefix = "packageUploadBulkActor"; PackageUploadBulkActor.prototype._openFile = function() { return FileUtils.openSafeFileOutputStream(this._file); }; PackageUploadBulkActor.prototype._closeFile = function() { FileUtils.closeSafeFileOutputStream(this.openedFile); }; PackageUploadBulkActor.prototype.stream = function({copyTo}) { return copyTo(this.openedFile).then(() => { this._closeFile(); return {}; }); }; /** * The request types this actor can handle. */ PackageUploadBulkActor.prototype.requestTypes = { "stream": PackageUploadBulkActor.prototype.stream, "remove": PackageUploadBulkActor.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/MessageBroadcaster.jsm"); this.appsChild = {}; Cu.import("resource://gre/modules/AppsServiceChild.jsm", this.appsChild); // Keep reference of already connected app processes. // values: app frame message manager this._connectedApps = new Set(); this.conn = aConnection; this._uploads = []; this._actorPool = new ActorPool(this.conn); this.conn.addActorPool(this._actorPool); } WebappsActor.prototype = { actorPrefix: "webapps", disconnect: function () { try { this.unwatchApps(); } catch(e) {} // 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; if (aId in reg.webapps && !reg.webapps[aId].sideloaded && !this._isUnrestrictedAccessAllowed()) { throw new Error("Replacing non-sideloaded apps is not permitted."); } // 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(); aApp.sideloaded = true; reg.webapps[aId] = aApp; reg.updatePermissionsForApp(aId); reg._readManifests([{ id: aId }]).then((aResult) => { let manifest = aResult[0].manifest; aApp.name = manifest.name; aApp.csp = manifest.csp || ""; aApp.role = manifest.role || ""; reg.updateAppHandlers(null, manifest, aApp); reg._saveApps().then(() => { aApp.manifest = manifest; // We need the manifest to set the app kind for hosted apps, // because of appcache. if (aApp.kind == undefined) { aApp.kind = manifest.appcache_path ? reg.kHostedAppcache : reg.kHosted; } // 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) MessageBroadcaster.broadcastMessage("Webapps:UpdateState", { app: aApp, manifest: manifest, id: aApp.id }); MessageBroadcaster.broadcastMessage("Webapps:FireEvent", { eventType: ["downloadsuccess", "downloadapplied"], manifestURL: aApp.manifestURL }); MessageBroadcaster.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp }); MessageBroadcaster.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, aApp.manifestURL), aApp); } }); // 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; }, _createTmpPackage: function() { 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 tmpFile; }, uploadPackage: function (request) { debug("uploadPackage"); let tmpFile = this._createTmpPackage(); if ("error" in tmpFile) { return tmpFile; } let actor = PackageUploadActor.fromRequest(request, tmpFile); 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 manFile = OS.Path.join(aDir.path, "manifest.webapp"); return AppsUtils.loadJSONAsync(manFile); } } function writeManifest(resolution) { // Move manifest.webapp to the destination directory. // The destination directory for this app. let installDir = DOMApplicationRegistry._getAppDir(aId); if (aManifest) { let manFile = OS.Path.join(installDir.path, "manifest.webapp"); return DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest)).then(() => { return resolution; }); } else { let manFile = aDir.clone(); manFile.append("manifest.webapp"); manFile.moveTo(installDir, "manifest.webapp"); } return promise.resolve(resolution); } function readMetadata(aAppType) { if (aMetadata) { return { metadata: aMetadata, appType: aAppType }; } // Read the origin and manifest url from metadata.json let metaFile = OS.Path.join(aDir.path, "metadata.json"); return AppsUtils.loadJSONAsync(metaFile).then((aMetadata) => { if (!aMetadata) { throw("Error parsing metadata.json."); } if (!aMetadata.origin) { throw("Missing 'origin' property in metadata.json."); } return { metadata: aMetadata, appType: aAppType }; }); } let runnable = { run: function run() { try { let metadata, appType; readManifest(). 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, }; return writeManifest(app); }).then(function (app) { 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); // Prefer manifest.webapp when available let hasWebappManifest = zipReader.hasEntry("manifest.webapp"); let hasJsonManifest = zipReader.hasEntry("manifest.json"); if (!hasWebappManifest && !hasJsonManifest) { self._sendError(deferred, "Missing manifest.webapp or manifest.json", aId); return; } let manifestName = hasWebappManifest ? "manifest.webapp" : "manifest.json"; // Read app manifest from `application.zip` let istream = zipReader.getInputStream(manifestName); let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let jsonString = converter.ConvertToUnicode( NetUtil.readInputStreamToString(istream, istream.available()) ); zipReader.close(); let manifest; try { manifest = JSON.parse(jsonString); } catch(e) { self._sendError(deferred, "Error Parsing " + manifestName + ": " + e, aId); return; } if (manifestName === "manifest.json") { if (!UserCustomizations.checkExtensionManifest(manifest)) { self._sendError(deferred, "Invalid manifest", aId); return; } manifest = UserCustomizations.convertManifest(manifest); } // Completely forbid pushing apps asking for unsafe permissions if ("permissions" in manifest) { let list = UNSAFE_PERMISSIONS.split(","); let hasOne = list.some(p => p.trim() in manifest.permissions); if (hasOne) { self._sendError(deferred, "Installing apps with any of these " + "permissions is forbidden: " + UNSAFE_PERMISSIONS, aId); return; } } 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); } // Prevent overriding preinstalled apps if (id in DOMApplicationRegistry.webapps && DOMApplicationRegistry.webapps[id].removable === false && !self._isUnrestrictedAccessAllowed()) { self._sendError(deferred, "The application " + id + " can't be overridden."); return; } // Only after security checks are made and after final app id is computed // we can move application.zip to the destination directory, and // write manifest.webapp there. let installDir = DOMApplicationRegistry._getAppDir(id); zipFile.moveTo(installDir, "application.zip"); let manFile = installDir.clone(); manFile.append("manifest.webapp"); DOMApplicationRegistry._writeFile(manFile.path, JSON.stringify(manifest)) .then(() => { 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 = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsILocalFile); jar.initWithPath(path); let obs = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); obs.notifyObservers(jar, "flush-cache-entry", null); }; for (let frame of 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, kind: DOMApplicationRegistry.kPackaged, } 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 && !this._isUnrestrictedAccessAllowed()) { return { error: "installationFailed", message: "The application " + appId + " can't be overridden." }; } 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.filePath); 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(); this.appsChild.DOMApplicationRegistry.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._isAppAllowedForURL(app.manifestURL)) { return { error: "forbidden" }; } return reg.getManifestFor(manifestURL).then(function (manifest) { app.manifest = manifest; return { app: app }; }); }, _isUnrestrictedAccessAllowed: function() { let pref = "devtools.debugger.forbid-certified-apps"; return !Services.prefs.getBoolPref(pref); }, _isAppAllowed: function(aApp) { if (this._isUnrestrictedAccessAllowed()) { return true; } return aApp.sideloaded; }, _filterAllowedApps: function wa__filterAllowedApps(aApps) { return aApps.filter(app => this._isAppAllowed(app)); }, _isAppAllowedForURL: function wa__isAppAllowedForURL(aManifestURL) { let reg = DOMApplicationRegistry; let app = reg.getAppByManifestURL(aManifestURL); return this._isAppAllowed(app); }, uninstall: function wa_actorUninstall(aRequest) { debug("uninstall"); let manifestURL = aRequest.manifestURL; if (!manifestURL) { return { error: "missingParameter", message: "missing parameter manifestURL" }; } if (!this._isAppAllowedForURL(manifestURL)) { return { error: "forbidden" }; } return DOMApplicationRegistry.uninstall(manifestURL); }, _findManifestByURL: function wa__findManifestByURL(aManifestURL) { let deferred = promise.defer(); let reg = DOMApplicationRegistry; let id = reg._appIdForManifestURL(aManifestURL); reg._readManifests([{ id: id }]).then((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, manifestURL); 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 exist" }); 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(); if (Services.appinfo.ID && Services.appinfo.ID != "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { return { error: "notSupported", message: "Not B2G. Can't launch app." }; } 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 () { // Try to filter on b2g and mulet if (Frames) { return Frames.list().filter(frame => { return frame.getAttribute('mozapp'); }); } else { return []; } }, listRunningApps: function (aRequest) { debug("listRunningApps\n"); let appPromises = []; let apps = []; for (let frame of this._appFrames()) { let manifestURL = frame.getAttribute("mozapp"); // _appFrames can return more than one frame with the same manifest url if (apps.indexOf(manifestURL) != -1) { continue; } if (this._isAppAllowedForURL(manifestURL)) { apps.push(manifestURL); } } return { apps: apps }; }, getAppActor: function ({ manifestURL }) { debug("getAppActor\n"); // Connects to the main app frame, whose `name` attribute // is set to 'main' by gaia. If for any reason, gaia doesn't set any // frame as main, no frame matches, then we connect arbitrary // to the first app frame... let appFrame = null; let frames = []; for (let frame of this._appFrames()) { if (frame.getAttribute("mozapp") == manifestURL) { if (frame.name == "main") { appFrame = frame; break; } frames.push(frame); } } if (!appFrame && frames.length > 0) { appFrame = frames[0]; } let notFoundError = { error: "appNotFound", message: "Unable to find any opened app whose manifest " + "is '" + manifestURL + "'" }; if (!appFrame) { return notFoundError; } if (!this._isAppAllowedForURL(manifestURL)) { return notFoundError; } // Only create a new actor, if we haven't already // instanciated one for this connection. let set = this._connectedApps; let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner) .frameLoader .messageManager; if (!set.has(mm)) { let onConnect = actor => { set.add(mm); return { actor: actor }; }; let onDisconnect = mm => { set.delete(mm); }; return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect) .then(onConnect); } // We have to update the form as it may have changed // if we detached the TabActor let deferred = promise.defer(); let onFormUpdate = msg => { mm.removeMessageListener("debug:form", onFormUpdate); deferred.resolve({ actor: msg.json }); }; mm.addMessageListener("debug:form", onFormUpdate); mm.sendAsyncMessage("debug:form"); return deferred.promise; }, watchApps: function () { // For now, app open/close events are only implement on b2g if (Frames) { Frames.addObserver(this); } Services.obs.addObserver(this, "webapps-installed", false); Services.obs.addObserver(this, "webapps-uninstall", false); return {}; }, unwatchApps: function () { if (Frames) { Frames.removeObserver(this); } Services.obs.removeObserver(this, "webapps-installed", false); Services.obs.removeObserver(this, "webapps-uninstall", false); return {}; }, onFrameCreated: function (frame, isFirstAppFrame) { let mozapp = frame.getAttribute('mozapp'); if (!mozapp || !isFirstAppFrame) { return; } let manifestURL = frame.appManifestURL; // Only track app frames if (!manifestURL) { return; } if (this._isAppAllowedForURL(manifestURL)) { this.conn.send({ from: this.actorID, type: "appOpen", manifestURL: manifestURL }); } }, onFrameDestroyed: function (frame, isLastAppFrame) { let mozapp = frame.getAttribute('mozapp'); if (!mozapp || !isLastAppFrame) { return; } let manifestURL = frame.appManifestURL; // Only track app frames if (!manifestURL) { return; } if (this._isAppAllowedForURL(manifestURL)) { this.conn.send({ from: this.actorID, type: "appClose", manifestURL: manifestURL }); } }, 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, "uploadPackage": WebappsActor.prototype.uploadPackage, "getAll": WebappsActor.prototype.getAll, "getApp": WebappsActor.prototype.getApp, "launch": WebappsActor.prototype.launch, "close": WebappsActor.prototype.close, "uninstall": WebappsActor.prototype.uninstall, "listRunningApps": WebappsActor.prototype.listRunningApps, "getAppActor": WebappsActor.prototype.getAppActor, "watchApps": WebappsActor.prototype.watchApps, "unwatchApps": WebappsActor.prototype.unwatchApps, "getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL }; exports.WebappsActor = WebappsActor;