1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/webapps.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,947 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +let Cu = Components.utils; 1.11 +let Cc = Components.classes; 1.12 +let Ci = Components.interfaces; 1.13 +let CC = Components.Constructor; 1.14 + 1.15 +Cu.import("resource://gre/modules/osfile.jsm"); 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 + 1.18 +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 1.19 + 1.20 +let promise; 1.21 + 1.22 +function debug(aMsg) { 1.23 + /* 1.24 + Cc["@mozilla.org/consoleservice;1"] 1.25 + .getService(Ci.nsIConsoleService) 1.26 + .logStringMessage("--*-- WebappsActor : " + aMsg); 1.27 + */ 1.28 +} 1.29 + 1.30 +function PackageUploadActor(aPath, aFile) { 1.31 + this._path = aPath; 1.32 + this._file = aFile; 1.33 + this.size = 0; 1.34 +} 1.35 + 1.36 +PackageUploadActor.prototype = { 1.37 + actorPrefix: "packageUploadActor", 1.38 + 1.39 + /** 1.40 + * This method isn't exposed to the client. 1.41 + * It is meant to be called by server code, in order to get 1.42 + * access to the temporary file out of the actor ID. 1.43 + */ 1.44 + getFilePath: function () { 1.45 + return this._path; 1.46 + }, 1.47 + 1.48 + /** 1.49 + * This method allows you to upload a piece of file. 1.50 + * It expects a chunk argument that is the a string to write to the file. 1.51 + */ 1.52 + chunk: function (aRequest) { 1.53 + let chunk = aRequest.chunk; 1.54 + if (!chunk || chunk.length <= 0) { 1.55 + return {error: "parameterError", 1.56 + message: "Missing or invalid chunk argument"}; 1.57 + } 1.58 + // Translate the string used to transfer the chunk over JSON 1.59 + // back to a typed array 1.60 + let data = new Uint8Array(chunk.length); 1.61 + for (let i = 0, l = chunk.length; i < l ; i++) { 1.62 + data[i] = chunk.charCodeAt(i); 1.63 + } 1.64 + return this._file.write(data) 1.65 + .then((written) => { 1.66 + this.size += written; 1.67 + return { 1.68 + written: written, 1.69 + size: this.size 1.70 + }; 1.71 + }); 1.72 + }, 1.73 + 1.74 + /** 1.75 + * This method needs to be called, when you are done uploading 1.76 + * chunks, before trying to access/use the temporary file. 1.77 + * Otherwise, the file may be partially written 1.78 + * and also be locked. 1.79 + */ 1.80 + done: function (aRequest) { 1.81 + this._file.close(); 1.82 + return {}; 1.83 + }, 1.84 + 1.85 + /** 1.86 + * This method allows you to delete the temporary file, 1.87 + * when you are done using it. 1.88 + */ 1.89 + remove: function (aRequest) { 1.90 + this._cleanupFile(); 1.91 + return {}; 1.92 + }, 1.93 + 1.94 + _cleanupFile: function () { 1.95 + try { 1.96 + this._file.close(); 1.97 + } catch(e) {} 1.98 + try { 1.99 + OS.File.remove(this._path); 1.100 + } catch(e) {} 1.101 + } 1.102 +}; 1.103 + 1.104 +/** 1.105 + * The request types this actor can handle. 1.106 + */ 1.107 +PackageUploadActor.prototype.requestTypes = { 1.108 + "chunk": PackageUploadActor.prototype.chunk, 1.109 + "done": PackageUploadActor.prototype.done, 1.110 + "remove": PackageUploadActor.prototype.remove 1.111 +}; 1.112 + 1.113 +/** 1.114 + * Creates a WebappsActor. WebappsActor provides remote access to 1.115 + * install apps. 1.116 + */ 1.117 +function WebappsActor(aConnection) { 1.118 + debug("init"); 1.119 + // Load actor dependencies lazily as this actor require extra environnement 1.120 + // preparation to work (like have a profile setup in xpcshell tests) 1.121 + 1.122 + Cu.import("resource://gre/modules/Webapps.jsm"); 1.123 + Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.124 + Cu.import("resource://gre/modules/FileUtils.jsm"); 1.125 + 1.126 + // Keep reference of already created app actors. 1.127 + // key: app frame message manager, value: ContentActor's grip() value 1.128 + this._appActorsMap = new Map(); 1.129 + 1.130 + this.conn = aConnection; 1.131 + this._uploads = []; 1.132 + this._actorPool = new ActorPool(this.conn); 1.133 + this.conn.addActorPool(this._actorPool); 1.134 +} 1.135 + 1.136 +WebappsActor.prototype = { 1.137 + actorPrefix: "webapps", 1.138 + 1.139 + disconnect: function () { 1.140 + // When we stop using this actor, we should ensure removing all files. 1.141 + for (let upload of this._uploads) { 1.142 + upload.remove(); 1.143 + } 1.144 + this._uploads = null; 1.145 + 1.146 + this.conn.removeActorPool(this._actorPool); 1.147 + this._actorPool = null; 1.148 + this.conn = null; 1.149 + }, 1.150 + 1.151 + _registerApp: function wa_actorRegisterApp(aDeferred, aApp, aId, aDir) { 1.152 + debug("registerApp"); 1.153 + let reg = DOMApplicationRegistry; 1.154 + let self = this; 1.155 + 1.156 + // Clean up the deprecated manifest cache if needed. 1.157 + if (aId in reg._manifestCache) { 1.158 + delete reg._manifestCache[aId]; 1.159 + } 1.160 + 1.161 + aApp.installTime = Date.now(); 1.162 + aApp.installState = "installed"; 1.163 + aApp.removable = true; 1.164 + aApp.id = aId; 1.165 + aApp.basePath = reg.getWebAppsBasePath(); 1.166 + aApp.localId = (aId in reg.webapps) ? reg.webapps[aId].localId 1.167 + : reg._nextLocalId(); 1.168 + 1.169 + reg.webapps[aId] = aApp; 1.170 + reg.updatePermissionsForApp(aId); 1.171 + 1.172 + reg._readManifests([{ id: aId }]).then((aResult) => { 1.173 + let manifest = aResult[0].manifest; 1.174 + aApp.name = manifest.name; 1.175 + reg.updateAppHandlers(null, manifest, aApp); 1.176 + 1.177 + reg._saveApps().then(() => { 1.178 + aApp.manifest = manifest; 1.179 + 1.180 + // Needed to evict manifest cache on content side 1.181 + // (has to be dispatched first, otherwise other messages like 1.182 + // Install:Return:OK are going to use old manifest version) 1.183 + reg.broadcastMessage("Webapps:UpdateState", { 1.184 + app: aApp, 1.185 + manifest: manifest, 1.186 + manifestURL: aApp.manifestURL 1.187 + }); 1.188 + reg.broadcastMessage("Webapps:FireEvent", { 1.189 + eventType: ["downloadsuccess", "downloadapplied"], 1.190 + manifestURL: aApp.manifestURL 1.191 + }); 1.192 + reg.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp }); 1.193 + reg.broadcastMessage("Webapps:Install:Return:OK", { 1.194 + app: aApp, 1.195 + oid: "foo", 1.196 + requestID: "bar" 1.197 + }); 1.198 + 1.199 + Services.obs.notifyObservers(null, "webapps-installed", 1.200 + JSON.stringify({ manifestURL: aApp.manifestURL })); 1.201 + 1.202 + delete aApp.manifest; 1.203 + aDeferred.resolve({ appId: aId, path: aDir.path }); 1.204 + 1.205 + // We can't have appcache for packaged apps. 1.206 + if (!aApp.origin.startsWith("app://")) { 1.207 + reg.startOfflineCacheDownload(new ManifestHelper(manifest, aApp.origin)); 1.208 + } 1.209 + }); 1.210 + // Cleanup by removing the temporary directory. 1.211 + if (aDir.exists()) 1.212 + aDir.remove(true); 1.213 + }); 1.214 + }, 1.215 + 1.216 + _sendError: function wa_actorSendError(aDeferred, aMsg, aId) { 1.217 + debug("Sending error: " + aMsg); 1.218 + aDeferred.resolve({ 1.219 + error: "installationFailed", 1.220 + message: aMsg, 1.221 + appId: aId 1.222 + }); 1.223 + }, 1.224 + 1.225 + _getAppType: function wa_actorGetAppType(aType) { 1.226 + let type = Ci.nsIPrincipal.APP_STATUS_INSTALLED; 1.227 + 1.228 + if (aType) { 1.229 + type = aType == "privileged" ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED 1.230 + : aType == "certified" ? Ci.nsIPrincipal.APP_STATUS_CERTIFIED 1.231 + : Ci.nsIPrincipal.APP_STATUS_INSTALLED; 1.232 + } 1.233 + 1.234 + return type; 1.235 + }, 1.236 + 1.237 + uploadPackage: function () { 1.238 + debug("uploadPackage\n"); 1.239 + let tmpDir = FileUtils.getDir("TmpD", ["file-upload"], true, false); 1.240 + if (!tmpDir.exists() || !tmpDir.isDirectory()) { 1.241 + return {error: "fileAccessError", 1.242 + message: "Unable to create temporary folder"}; 1.243 + } 1.244 + let tmpFile = tmpDir; 1.245 + tmpFile.append("package.zip"); 1.246 + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); 1.247 + if (!tmpFile.exists() || !tmpDir.isFile()) { 1.248 + return {error: "fileAccessError", 1.249 + message: "Unable to create temporary file"}; 1.250 + } 1.251 + 1.252 + return OS.File.open(tmpFile.path, { write: true, truncate: true }) 1.253 + .then((file) => { 1.254 + let actor = new PackageUploadActor(tmpFile.path, file); 1.255 + this._actorPool.addActor(actor); 1.256 + this._uploads.push(actor); 1.257 + return { actor: actor.actorID }; 1.258 + }); 1.259 + }, 1.260 + 1.261 + installHostedApp: function wa_actorInstallHosted(aDir, aId, aReceipts, 1.262 + aManifest, aMetadata) { 1.263 + debug("installHostedApp"); 1.264 + let self = this; 1.265 + let deferred = promise.defer(); 1.266 + 1.267 + function readManifest() { 1.268 + if (aManifest) { 1.269 + return promise.resolve(aManifest); 1.270 + } else { 1.271 + let manFile = OS.Path.join(aDir.path, "manifest.webapp"); 1.272 + return AppsUtils.loadJSONAsync(manFile); 1.273 + } 1.274 + } 1.275 + function checkSideloading(aManifest) { 1.276 + return self._getAppType(aManifest.type); 1.277 + } 1.278 + function writeManifest(aAppType) { 1.279 + // Move manifest.webapp to the destination directory. 1.280 + // The destination directory for this app. 1.281 + let installDir = DOMApplicationRegistry._getAppDir(aId); 1.282 + if (aManifest) { 1.283 + let manFile = OS.Path.join(installDir.path, "manifest.webapp"); 1.284 + return DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest)).then(() => { 1.285 + return aAppType; 1.286 + }); 1.287 + } else { 1.288 + let manFile = aDir.clone(); 1.289 + manFile.append("manifest.webapp"); 1.290 + manFile.moveTo(installDir, "manifest.webapp"); 1.291 + } 1.292 + return null; 1.293 + } 1.294 + function readMetadata(aAppType) { 1.295 + if (aMetadata) { 1.296 + return { metadata: aMetadata, appType: aAppType }; 1.297 + } 1.298 + // Read the origin and manifest url from metadata.json 1.299 + let metaFile = OS.Path.join(aDir.path, "metadata.json"); 1.300 + return AppsUtils.loadJSONAsync(metaFile).then((aMetadata) => { 1.301 + if (!aMetadata) { 1.302 + throw("Error parsing metadata.json."); 1.303 + } 1.304 + if (!aMetadata.origin) { 1.305 + throw("Missing 'origin' property in metadata.json"); 1.306 + } 1.307 + return { metadata: aMetadata, appType: aAppType }; 1.308 + }); 1.309 + } 1.310 + let runnable = { 1.311 + run: function run() { 1.312 + try { 1.313 + readManifest(). 1.314 + then(writeManifest). 1.315 + then(checkSideloading). 1.316 + then(readMetadata). 1.317 + then(function ({ metadata, appType }) { 1.318 + let origin = metadata.origin; 1.319 + let manifestURL = metadata.manifestURL || 1.320 + origin + "/manifest.webapp"; 1.321 + // Create a fake app object with the minimum set of properties we need. 1.322 + let app = { 1.323 + origin: origin, 1.324 + installOrigin: metadata.installOrigin || origin, 1.325 + manifestURL: manifestURL, 1.326 + appStatus: appType, 1.327 + receipts: aReceipts, 1.328 + }; 1.329 + 1.330 + self._registerApp(deferred, app, aId, aDir); 1.331 + }, function (error) { 1.332 + self._sendError(deferred, error, aId); 1.333 + }); 1.334 + } catch(e) { 1.335 + // If anything goes wrong, just send it back. 1.336 + self._sendError(deferred, e.toString(), aId); 1.337 + } 1.338 + } 1.339 + } 1.340 + 1.341 + Services.tm.currentThread.dispatch(runnable, 1.342 + Ci.nsIThread.DISPATCH_NORMAL); 1.343 + return deferred.promise; 1.344 + }, 1.345 + 1.346 + installPackagedApp: function wa_actorInstallPackaged(aDir, aId, aReceipts) { 1.347 + debug("installPackagedApp"); 1.348 + let self = this; 1.349 + let deferred = promise.defer(); 1.350 + 1.351 + let runnable = { 1.352 + run: function run() { 1.353 + try { 1.354 + // Open the app zip package 1.355 + let zipFile = aDir.clone(); 1.356 + zipFile.append("application.zip"); 1.357 + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] 1.358 + .createInstance(Ci.nsIZipReader); 1.359 + zipReader.open(zipFile); 1.360 + 1.361 + // Read app manifest `manifest.webapp` from `application.zip` 1.362 + let istream = zipReader.getInputStream("manifest.webapp"); 1.363 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.364 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.365 + converter.charset = "UTF-8"; 1.366 + let jsonString = converter.ConvertToUnicode( 1.367 + NetUtil.readInputStreamToString(istream, istream.available()) 1.368 + ); 1.369 + 1.370 + let manifest; 1.371 + try { 1.372 + manifest = JSON.parse(jsonString); 1.373 + } catch(e) { 1.374 + self._sendError(deferred, "Error Parsing manifest.webapp: " + e, aId); 1.375 + } 1.376 + 1.377 + let appType = self._getAppType(manifest.type); 1.378 + 1.379 + // Privileged and certified packaged apps can setup a custom origin 1.380 + // via `origin` manifest property 1.381 + let id = aId; 1.382 + if (appType >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED && 1.383 + manifest.origin !== undefined) { 1.384 + let uri; 1.385 + try { 1.386 + uri = Services.io.newURI(manifest.origin, null, null); 1.387 + } catch(e) { 1.388 + self._sendError(deferred, "Invalid origin in webapp's manifest", aId); 1.389 + } 1.390 + 1.391 + if (uri.scheme != "app") { 1.392 + self._sendError(deferred, "Invalid origin in webapp's manifest", aId); 1.393 + } 1.394 + id = uri.prePath.substring(6); 1.395 + } 1.396 + 1.397 + // Only after security checks are made and after final app id is computed 1.398 + // we can move application.zip to the destination directory, and 1.399 + // extract manifest.webapp there. 1.400 + let installDir = DOMApplicationRegistry._getAppDir(id); 1.401 + let manFile = installDir.clone(); 1.402 + manFile.append("manifest.webapp"); 1.403 + zipReader.extract("manifest.webapp", manFile); 1.404 + zipReader.close(); 1.405 + zipFile.moveTo(installDir, "application.zip"); 1.406 + 1.407 + let origin = "app://" + id; 1.408 + let manifestURL = origin + "/manifest.webapp"; 1.409 + 1.410 + // Refresh application.zip content (e.g. reinstall app), as done here: 1.411 + // http://hg.mozilla.org/mozilla-central/annotate/aaefec5d34f8/dom/apps/src/Webapps.jsm#l1125 1.412 + // Do it in parent process for the simulator 1.413 + let jar = installDir.clone(); 1.414 + jar.append("application.zip"); 1.415 + Services.obs.notifyObservers(jar, "flush-cache-entry", null); 1.416 + 1.417 + // And then in app content process 1.418 + // This function will be evaluated in the scope of the content process 1.419 + // frame script. That will flush the jar cache for this app and allow 1.420 + // loading fresh updated resources if we reload its document. 1.421 + let FlushFrameScript = function (path) { 1.422 + let jar = Components.classes["@mozilla.org/file/local;1"] 1.423 + .createInstance(Components.interfaces.nsILocalFile); 1.424 + jar.initWithPath(path); 1.425 + let obs = Components.classes["@mozilla.org/observer-service;1"] 1.426 + .getService(Components.interfaces.nsIObserverService); 1.427 + obs.notifyObservers(jar, "flush-cache-entry", null); 1.428 + }; 1.429 + for each (let frame in self._appFrames()) { 1.430 + if (frame.getAttribute("mozapp") == manifestURL) { 1.431 + let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; 1.432 + mm.loadFrameScript("data:," + 1.433 + encodeURIComponent("(" + FlushFrameScript.toString() + ")" + 1.434 + "('" + jar.path + "')"), false); 1.435 + } 1.436 + } 1.437 + 1.438 + // Create a fake app object with the minimum set of properties we need. 1.439 + let app = { 1.440 + origin: origin, 1.441 + installOrigin: origin, 1.442 + manifestURL: manifestURL, 1.443 + appStatus: appType, 1.444 + receipts: aReceipts, 1.445 + } 1.446 + 1.447 + self._registerApp(deferred, app, id, aDir); 1.448 + } catch(e) { 1.449 + // If anything goes wrong, just send it back. 1.450 + self._sendError(deferred, e.toString(), aId); 1.451 + } 1.452 + } 1.453 + } 1.454 + 1.455 + Services.tm.currentThread.dispatch(runnable, 1.456 + Ci.nsIThread.DISPATCH_NORMAL); 1.457 + return deferred.promise; 1.458 + }, 1.459 + 1.460 + /** 1.461 + * @param appId : The id of the app we want to install. We will look for 1.462 + * the files for the app in $TMP/b2g/$appId : 1.463 + * For packaged apps: application.zip 1.464 + * For hosted apps: metadata.json and manifest.webapp 1.465 + */ 1.466 + install: function wa_actorInstall(aRequest) { 1.467 + debug("install"); 1.468 + 1.469 + let appId = aRequest.appId; 1.470 + let reg = DOMApplicationRegistry; 1.471 + if (!appId) { 1.472 + appId = reg.makeAppId(); 1.473 + } 1.474 + 1.475 + // Check that we are not overriding a preinstalled application. 1.476 + if (appId in reg.webapps && reg.webapps[appId].removable === false) { 1.477 + return { error: "badParameterType", 1.478 + message: "The application " + appId + " can't be overriden." 1.479 + } 1.480 + } 1.481 + 1.482 + let appDir = FileUtils.getDir("TmpD", ["b2g", appId], false, false); 1.483 + 1.484 + if (aRequest.upload) { 1.485 + // Ensure creating the directory (recursively) 1.486 + appDir = FileUtils.getDir("TmpD", ["b2g", appId], true, false); 1.487 + let actor = this.conn.getActor(aRequest.upload); 1.488 + if (!actor) { 1.489 + return { error: "badParameter", 1.490 + message: "Unable to find upload actor '" + aRequest.upload 1.491 + + "'" }; 1.492 + } 1.493 + let appFile = FileUtils.File(actor.getFilePath()); 1.494 + if (!appFile.exists()) { 1.495 + return { error: "badParameter", 1.496 + message: "The uploaded file doesn't exist on device" }; 1.497 + } 1.498 + appFile.moveTo(appDir, "application.zip"); 1.499 + } else if ((!appDir || !appDir.exists()) && 1.500 + !aRequest.manifest && !aRequest.metadata) { 1.501 + return { error: "badParameterType", 1.502 + message: "missing directory " + appDir.path 1.503 + }; 1.504 + } 1.505 + 1.506 + let testFile = appDir.clone(); 1.507 + testFile.append("application.zip"); 1.508 + 1.509 + let receipts = (aRequest.receipts && Array.isArray(aRequest.receipts)) 1.510 + ? aRequest.receipts 1.511 + : []; 1.512 + 1.513 + if (testFile.exists()) { 1.514 + return this.installPackagedApp(appDir, appId, receipts); 1.515 + } 1.516 + 1.517 + let manifest, metadata; 1.518 + let missing = 1.519 + ["manifest.webapp", "metadata.json"] 1.520 + .some(function(aName) { 1.521 + testFile = appDir.clone(); 1.522 + testFile.append(aName); 1.523 + return !testFile.exists(); 1.524 + }); 1.525 + if (missing) { 1.526 + if (aRequest.manifest && aRequest.metadata && 1.527 + aRequest.metadata.origin) { 1.528 + manifest = aRequest.manifest; 1.529 + metadata = aRequest.metadata; 1.530 + } else { 1.531 + try { 1.532 + appDir.remove(true); 1.533 + } catch(e) {} 1.534 + return { error: "badParameterType", 1.535 + message: "hosted app file and manifest/metadata fields " + 1.536 + "are missing" 1.537 + }; 1.538 + } 1.539 + } 1.540 + 1.541 + return this.installHostedApp(appDir, appId, receipts, manifest, metadata); 1.542 + }, 1.543 + 1.544 + getAll: function wa_actorGetAll(aRequest) { 1.545 + debug("getAll"); 1.546 + 1.547 + let deferred = promise.defer(); 1.548 + let reg = DOMApplicationRegistry; 1.549 + reg.getAll(apps => { 1.550 + deferred.resolve({ apps: this._filterAllowedApps(apps) }); 1.551 + }); 1.552 + 1.553 + return deferred.promise; 1.554 + }, 1.555 + 1.556 + getApp: function wa_actorGetApp(aRequest) { 1.557 + debug("getApp"); 1.558 + 1.559 + let manifestURL = aRequest.manifestURL; 1.560 + if (!manifestURL) { 1.561 + return { error: "missingParameter", 1.562 + message: "missing parameter manifestURL" }; 1.563 + } 1.564 + 1.565 + let reg = DOMApplicationRegistry; 1.566 + let app = reg.getAppByManifestURL(manifestURL); 1.567 + if (!app) { 1.568 + return { error: "appNotFound" }; 1.569 + } 1.570 + 1.571 + return this._isAppAllowedForURL(app.manifestURL).then(allowed => { 1.572 + if (!allowed) { 1.573 + return { error: "forbidden" }; 1.574 + } 1.575 + return reg.getManifestFor(manifestURL).then(function (manifest) { 1.576 + app.manifest = manifest; 1.577 + return { app: app }; 1.578 + }); 1.579 + }); 1.580 + }, 1.581 + 1.582 + _areCertifiedAppsAllowed: function wa__areCertifiedAppsAllowed() { 1.583 + let pref = "devtools.debugger.forbid-certified-apps"; 1.584 + return !Services.prefs.getBoolPref(pref); 1.585 + }, 1.586 + 1.587 + _isAppAllowedForManifest: function wa__isAppAllowedForManifest(aManifest) { 1.588 + if (this._areCertifiedAppsAllowed()) { 1.589 + return true; 1.590 + } 1.591 + let type = this._getAppType(aManifest.type); 1.592 + return type !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED; 1.593 + }, 1.594 + 1.595 + _filterAllowedApps: function wa__filterAllowedApps(aApps) { 1.596 + return aApps.filter(app => this._isAppAllowedForManifest(app.manifest)); 1.597 + }, 1.598 + 1.599 + _isAppAllowedForURL: function wa__isAppAllowedForURL(aManifestURL) { 1.600 + return this._findManifestByURL(aManifestURL).then(manifest => { 1.601 + return this._isAppAllowedForManifest(manifest); 1.602 + }); 1.603 + }, 1.604 + 1.605 + uninstall: function wa_actorUninstall(aRequest) { 1.606 + debug("uninstall"); 1.607 + 1.608 + let manifestURL = aRequest.manifestURL; 1.609 + if (!manifestURL) { 1.610 + return { error: "missingParameter", 1.611 + message: "missing parameter manifestURL" }; 1.612 + } 1.613 + 1.614 + let deferred = promise.defer(); 1.615 + let reg = DOMApplicationRegistry; 1.616 + reg.uninstall( 1.617 + manifestURL, 1.618 + function onsuccess() { 1.619 + deferred.resolve({}); 1.620 + }, 1.621 + function onfailure(reason) { 1.622 + deferred.resolve({ error: reason }); 1.623 + } 1.624 + ); 1.625 + 1.626 + return deferred.promise; 1.627 + }, 1.628 + 1.629 + _findManifestByURL: function wa__findManifestByURL(aManifestURL) { 1.630 + let deferred = promise.defer(); 1.631 + 1.632 + let reg = DOMApplicationRegistry; 1.633 + let id = reg._appIdForManifestURL(aManifestURL); 1.634 + 1.635 + reg._readManifests([{ id: id }]).then((aResults) => { 1.636 + deferred.resolve(aResults[0].manifest); 1.637 + }); 1.638 + 1.639 + return deferred.promise; 1.640 + }, 1.641 + 1.642 + getIconAsDataURL: function (aRequest) { 1.643 + debug("getIconAsDataURL"); 1.644 + 1.645 + let manifestURL = aRequest.manifestURL; 1.646 + if (!manifestURL) { 1.647 + return { error: "missingParameter", 1.648 + message: "missing parameter manifestURL" }; 1.649 + } 1.650 + 1.651 + let reg = DOMApplicationRegistry; 1.652 + let app = reg.getAppByManifestURL(manifestURL); 1.653 + if (!app) { 1.654 + return { error: "wrongParameter", 1.655 + message: "No application for " + manifestURL }; 1.656 + } 1.657 + 1.658 + let deferred = promise.defer(); 1.659 + 1.660 + this._findManifestByURL(manifestURL).then(jsonManifest => { 1.661 + let manifest = new ManifestHelper(jsonManifest, app.origin); 1.662 + let iconURL = manifest.iconURLForSize(aRequest.size || 128); 1.663 + if (!iconURL) { 1.664 + deferred.resolve({ 1.665 + error: "noIcon", 1.666 + message: "This app has no icon" 1.667 + }); 1.668 + return; 1.669 + } 1.670 + 1.671 + // Download the URL as a blob 1.672 + // bug 899177: there is a bug with xhr and app:// and jar:// uris 1.673 + // that ends up forcing the content type to application/xml. 1.674 + let req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1'] 1.675 + .createInstance(Ci.nsIXMLHttpRequest); 1.676 + req.open("GET", iconURL, false); 1.677 + req.responseType = "blob"; 1.678 + 1.679 + try { 1.680 + req.send(null); 1.681 + } catch(e) { 1.682 + deferred.resolve({ 1.683 + error: "noIcon", 1.684 + message: "The icon file '" + iconURL + "' doesn't exist" 1.685 + }); 1.686 + return; 1.687 + } 1.688 + 1.689 + // Convert the blog to a base64 encoded data URI 1.690 + let reader = Cc["@mozilla.org/files/filereader;1"] 1.691 + .createInstance(Ci.nsIDOMFileReader); 1.692 + reader.onload = function () { 1.693 + deferred.resolve({ 1.694 + url: reader.result 1.695 + }); 1.696 + }; 1.697 + reader.onerror = function () { 1.698 + deferred.resolve({ 1.699 + error: reader.error.name, 1.700 + message: String(reader.error) 1.701 + }); 1.702 + }; 1.703 + reader.readAsDataURL(req.response); 1.704 + }); 1.705 + 1.706 + return deferred.promise; 1.707 + }, 1.708 + 1.709 + launch: function wa_actorLaunch(aRequest) { 1.710 + debug("launch"); 1.711 + 1.712 + let manifestURL = aRequest.manifestURL; 1.713 + if (!manifestURL) { 1.714 + return { error: "missingParameter", 1.715 + message: "missing parameter manifestURL" }; 1.716 + } 1.717 + 1.718 + let deferred = promise.defer(); 1.719 + 1.720 + DOMApplicationRegistry.launch( 1.721 + aRequest.manifestURL, 1.722 + aRequest.startPoint || "", 1.723 + Date.now(), 1.724 + function onsuccess() { 1.725 + deferred.resolve({}); 1.726 + }, 1.727 + function onfailure(reason) { 1.728 + deferred.resolve({ error: reason }); 1.729 + }); 1.730 + 1.731 + return deferred.promise; 1.732 + }, 1.733 + 1.734 + close: function wa_actorLaunch(aRequest) { 1.735 + debug("close"); 1.736 + 1.737 + let manifestURL = aRequest.manifestURL; 1.738 + if (!manifestURL) { 1.739 + return { error: "missingParameter", 1.740 + message: "missing parameter manifestURL" }; 1.741 + } 1.742 + 1.743 + let reg = DOMApplicationRegistry; 1.744 + let app = reg.getAppByManifestURL(manifestURL); 1.745 + if (!app) { 1.746 + return { error: "missingParameter", 1.747 + message: "No application for " + manifestURL }; 1.748 + } 1.749 + 1.750 + reg.close(app); 1.751 + 1.752 + return {}; 1.753 + }, 1.754 + 1.755 + _appFrames: function () { 1.756 + // For now, we only support app frames on b2g 1.757 + if (Services.appinfo.ID != "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { 1.758 + return; 1.759 + } 1.760 + // Register the system app 1.761 + let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); 1.762 + let systemAppFrame = chromeWindow.shell.contentBrowser; 1.763 + yield systemAppFrame; 1.764 + 1.765 + // Register apps hosted in the system app. i.e. the homescreen, all regular 1.766 + // apps and the keyboard. 1.767 + // Bookmark apps and other system app internal frames like captive portal 1.768 + // are also hosted in system app, but they are not using mozapp attribute. 1.769 + let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]"); 1.770 + for (let i = 0; i < frames.length; i++) { 1.771 + yield frames[i]; 1.772 + } 1.773 + }, 1.774 + 1.775 + listRunningApps: function (aRequest) { 1.776 + debug("listRunningApps\n"); 1.777 + 1.778 + let appPromises = []; 1.779 + let apps = []; 1.780 + 1.781 + for each (let frame in this._appFrames()) { 1.782 + let manifestURL = frame.getAttribute("mozapp"); 1.783 + 1.784 + appPromises.push(this._isAppAllowedForURL(manifestURL).then(allowed => { 1.785 + if (allowed) { 1.786 + apps.push(manifestURL); 1.787 + } 1.788 + })); 1.789 + } 1.790 + 1.791 + return promise.all(appPromises).then(() => { 1.792 + return { apps: apps }; 1.793 + }); 1.794 + }, 1.795 + 1.796 + getAppActor: function ({ manifestURL }) { 1.797 + debug("getAppActor\n"); 1.798 + 1.799 + let appFrame = null; 1.800 + for each (let frame in this._appFrames()) { 1.801 + if (frame.getAttribute("mozapp") == manifestURL) { 1.802 + appFrame = frame; 1.803 + break; 1.804 + } 1.805 + } 1.806 + 1.807 + let notFoundError = { 1.808 + error: "appNotFound", 1.809 + message: "Unable to find any opened app whose manifest " + 1.810 + "is '" + manifestURL + "'" 1.811 + }; 1.812 + 1.813 + if (!appFrame) { 1.814 + return notFoundError; 1.815 + } 1.816 + 1.817 + return this._isAppAllowedForURL(manifestURL).then(allowed => { 1.818 + if (!allowed) { 1.819 + return notFoundError; 1.820 + } 1.821 + 1.822 + // Only create a new actor, if we haven't already 1.823 + // instanciated one for this connection. 1.824 + let map = this._appActorsMap; 1.825 + let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner) 1.826 + .frameLoader 1.827 + .messageManager; 1.828 + let actor = map.get(mm); 1.829 + if (!actor) { 1.830 + let onConnect = actor => { 1.831 + map.set(mm, actor); 1.832 + return { actor: actor }; 1.833 + }; 1.834 + let onDisconnect = mm => { 1.835 + map.delete(mm); 1.836 + }; 1.837 + return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect) 1.838 + .then(onConnect); 1.839 + } 1.840 + 1.841 + return { actor: actor }; 1.842 + }); 1.843 + }, 1.844 + 1.845 + watchApps: function () { 1.846 + this._openedApps = new Set(); 1.847 + // For now, app open/close events are only implement on b2g 1.848 + if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { 1.849 + let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); 1.850 + let systemAppFrame = chromeWindow.getContentWindow(); 1.851 + systemAppFrame.addEventListener("appwillopen", this); 1.852 + systemAppFrame.addEventListener("appterminated", this); 1.853 + } 1.854 + Services.obs.addObserver(this, "webapps-installed", false); 1.855 + Services.obs.addObserver(this, "webapps-uninstall", false); 1.856 + 1.857 + return {}; 1.858 + }, 1.859 + 1.860 + unwatchApps: function () { 1.861 + this._openedApps = null; 1.862 + if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { 1.863 + let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); 1.864 + let systemAppFrame = chromeWindow.getContentWindow(); 1.865 + systemAppFrame.removeEventListener("appwillopen", this); 1.866 + systemAppFrame.removeEventListener("appterminated", this); 1.867 + } 1.868 + Services.obs.removeObserver(this, "webapps-installed", false); 1.869 + Services.obs.removeObserver(this, "webapps-uninstall", false); 1.870 + 1.871 + return {}; 1.872 + }, 1.873 + 1.874 + handleEvent: function (event) { 1.875 + let manifestURL; 1.876 + switch(event.type) { 1.877 + case "appwillopen": 1.878 + manifestURL = event.detail.manifestURL; 1.879 + 1.880 + // Ignore the event if we already received an appwillopen for this app 1.881 + // (appwillopen is also fired when the app has been moved to background 1.882 + // and get back to foreground) 1.883 + if (this._openedApps.has(manifestURL)) { 1.884 + return; 1.885 + } 1.886 + this._openedApps.add(manifestURL); 1.887 + 1.888 + this._isAppAllowedForURL(manifestURL).then(allowed => { 1.889 + if (allowed) { 1.890 + this.conn.send({ from: this.actorID, 1.891 + type: "appOpen", 1.892 + manifestURL: manifestURL 1.893 + }); 1.894 + } 1.895 + }); 1.896 + 1.897 + break; 1.898 + 1.899 + case "appterminated": 1.900 + manifestURL = event.detail.manifestURL; 1.901 + this._openedApps.delete(manifestURL); 1.902 + 1.903 + this._isAppAllowedForURL(manifestURL).then(allowed => { 1.904 + if (allowed) { 1.905 + this.conn.send({ from: this.actorID, 1.906 + type: "appClose", 1.907 + manifestURL: manifestURL 1.908 + }); 1.909 + } 1.910 + }); 1.911 + 1.912 + break; 1.913 + } 1.914 + }, 1.915 + 1.916 + observe: function (subject, topic, data) { 1.917 + let app = JSON.parse(data); 1.918 + if (topic == "webapps-installed") { 1.919 + this.conn.send({ from: this.actorID, 1.920 + type: "appInstall", 1.921 + manifestURL: app.manifestURL 1.922 + }); 1.923 + } else if (topic == "webapps-uninstall") { 1.924 + this.conn.send({ from: this.actorID, 1.925 + type: "appUninstall", 1.926 + manifestURL: app.manifestURL 1.927 + }); 1.928 + } 1.929 + } 1.930 +}; 1.931 + 1.932 +/** 1.933 + * The request types this actor can handle. 1.934 + */ 1.935 +WebappsActor.prototype.requestTypes = { 1.936 + "install": WebappsActor.prototype.install, 1.937 + "uploadPackage": WebappsActor.prototype.uploadPackage, 1.938 + "getAll": WebappsActor.prototype.getAll, 1.939 + "getApp": WebappsActor.prototype.getApp, 1.940 + "launch": WebappsActor.prototype.launch, 1.941 + "close": WebappsActor.prototype.close, 1.942 + "uninstall": WebappsActor.prototype.uninstall, 1.943 + "listRunningApps": WebappsActor.prototype.listRunningApps, 1.944 + "getAppActor": WebappsActor.prototype.getAppActor, 1.945 + "watchApps": WebappsActor.prototype.watchApps, 1.946 + "unwatchApps": WebappsActor.prototype.unwatchApps, 1.947 + "getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL 1.948 +}; 1.949 + 1.950 +DebuggerServer.addGlobalActor(WebappsActor, "webappsActor");