|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 let Cu = Components.utils; |
|
8 let Cc = Components.classes; |
|
9 let Ci = Components.interfaces; |
|
10 let CC = Components.Constructor; |
|
11 |
|
12 Cu.import("resource://gre/modules/osfile.jsm"); |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 |
|
15 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
16 |
|
17 let promise; |
|
18 |
|
19 function debug(aMsg) { |
|
20 /* |
|
21 Cc["@mozilla.org/consoleservice;1"] |
|
22 .getService(Ci.nsIConsoleService) |
|
23 .logStringMessage("--*-- WebappsActor : " + aMsg); |
|
24 */ |
|
25 } |
|
26 |
|
27 function PackageUploadActor(aPath, aFile) { |
|
28 this._path = aPath; |
|
29 this._file = aFile; |
|
30 this.size = 0; |
|
31 } |
|
32 |
|
33 PackageUploadActor.prototype = { |
|
34 actorPrefix: "packageUploadActor", |
|
35 |
|
36 /** |
|
37 * This method isn't exposed to the client. |
|
38 * It is meant to be called by server code, in order to get |
|
39 * access to the temporary file out of the actor ID. |
|
40 */ |
|
41 getFilePath: function () { |
|
42 return this._path; |
|
43 }, |
|
44 |
|
45 /** |
|
46 * This method allows you to upload a piece of file. |
|
47 * It expects a chunk argument that is the a string to write to the file. |
|
48 */ |
|
49 chunk: function (aRequest) { |
|
50 let chunk = aRequest.chunk; |
|
51 if (!chunk || chunk.length <= 0) { |
|
52 return {error: "parameterError", |
|
53 message: "Missing or invalid chunk argument"}; |
|
54 } |
|
55 // Translate the string used to transfer the chunk over JSON |
|
56 // back to a typed array |
|
57 let data = new Uint8Array(chunk.length); |
|
58 for (let i = 0, l = chunk.length; i < l ; i++) { |
|
59 data[i] = chunk.charCodeAt(i); |
|
60 } |
|
61 return this._file.write(data) |
|
62 .then((written) => { |
|
63 this.size += written; |
|
64 return { |
|
65 written: written, |
|
66 size: this.size |
|
67 }; |
|
68 }); |
|
69 }, |
|
70 |
|
71 /** |
|
72 * This method needs to be called, when you are done uploading |
|
73 * chunks, before trying to access/use the temporary file. |
|
74 * Otherwise, the file may be partially written |
|
75 * and also be locked. |
|
76 */ |
|
77 done: function (aRequest) { |
|
78 this._file.close(); |
|
79 return {}; |
|
80 }, |
|
81 |
|
82 /** |
|
83 * This method allows you to delete the temporary file, |
|
84 * when you are done using it. |
|
85 */ |
|
86 remove: function (aRequest) { |
|
87 this._cleanupFile(); |
|
88 return {}; |
|
89 }, |
|
90 |
|
91 _cleanupFile: function () { |
|
92 try { |
|
93 this._file.close(); |
|
94 } catch(e) {} |
|
95 try { |
|
96 OS.File.remove(this._path); |
|
97 } catch(e) {} |
|
98 } |
|
99 }; |
|
100 |
|
101 /** |
|
102 * The request types this actor can handle. |
|
103 */ |
|
104 PackageUploadActor.prototype.requestTypes = { |
|
105 "chunk": PackageUploadActor.prototype.chunk, |
|
106 "done": PackageUploadActor.prototype.done, |
|
107 "remove": PackageUploadActor.prototype.remove |
|
108 }; |
|
109 |
|
110 /** |
|
111 * Creates a WebappsActor. WebappsActor provides remote access to |
|
112 * install apps. |
|
113 */ |
|
114 function WebappsActor(aConnection) { |
|
115 debug("init"); |
|
116 // Load actor dependencies lazily as this actor require extra environnement |
|
117 // preparation to work (like have a profile setup in xpcshell tests) |
|
118 |
|
119 Cu.import("resource://gre/modules/Webapps.jsm"); |
|
120 Cu.import("resource://gre/modules/AppsUtils.jsm"); |
|
121 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
122 |
|
123 // Keep reference of already created app actors. |
|
124 // key: app frame message manager, value: ContentActor's grip() value |
|
125 this._appActorsMap = new Map(); |
|
126 |
|
127 this.conn = aConnection; |
|
128 this._uploads = []; |
|
129 this._actorPool = new ActorPool(this.conn); |
|
130 this.conn.addActorPool(this._actorPool); |
|
131 } |
|
132 |
|
133 WebappsActor.prototype = { |
|
134 actorPrefix: "webapps", |
|
135 |
|
136 disconnect: function () { |
|
137 // When we stop using this actor, we should ensure removing all files. |
|
138 for (let upload of this._uploads) { |
|
139 upload.remove(); |
|
140 } |
|
141 this._uploads = null; |
|
142 |
|
143 this.conn.removeActorPool(this._actorPool); |
|
144 this._actorPool = null; |
|
145 this.conn = null; |
|
146 }, |
|
147 |
|
148 _registerApp: function wa_actorRegisterApp(aDeferred, aApp, aId, aDir) { |
|
149 debug("registerApp"); |
|
150 let reg = DOMApplicationRegistry; |
|
151 let self = this; |
|
152 |
|
153 // Clean up the deprecated manifest cache if needed. |
|
154 if (aId in reg._manifestCache) { |
|
155 delete reg._manifestCache[aId]; |
|
156 } |
|
157 |
|
158 aApp.installTime = Date.now(); |
|
159 aApp.installState = "installed"; |
|
160 aApp.removable = true; |
|
161 aApp.id = aId; |
|
162 aApp.basePath = reg.getWebAppsBasePath(); |
|
163 aApp.localId = (aId in reg.webapps) ? reg.webapps[aId].localId |
|
164 : reg._nextLocalId(); |
|
165 |
|
166 reg.webapps[aId] = aApp; |
|
167 reg.updatePermissionsForApp(aId); |
|
168 |
|
169 reg._readManifests([{ id: aId }]).then((aResult) => { |
|
170 let manifest = aResult[0].manifest; |
|
171 aApp.name = manifest.name; |
|
172 reg.updateAppHandlers(null, manifest, aApp); |
|
173 |
|
174 reg._saveApps().then(() => { |
|
175 aApp.manifest = manifest; |
|
176 |
|
177 // Needed to evict manifest cache on content side |
|
178 // (has to be dispatched first, otherwise other messages like |
|
179 // Install:Return:OK are going to use old manifest version) |
|
180 reg.broadcastMessage("Webapps:UpdateState", { |
|
181 app: aApp, |
|
182 manifest: manifest, |
|
183 manifestURL: aApp.manifestURL |
|
184 }); |
|
185 reg.broadcastMessage("Webapps:FireEvent", { |
|
186 eventType: ["downloadsuccess", "downloadapplied"], |
|
187 manifestURL: aApp.manifestURL |
|
188 }); |
|
189 reg.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp }); |
|
190 reg.broadcastMessage("Webapps:Install:Return:OK", { |
|
191 app: aApp, |
|
192 oid: "foo", |
|
193 requestID: "bar" |
|
194 }); |
|
195 |
|
196 Services.obs.notifyObservers(null, "webapps-installed", |
|
197 JSON.stringify({ manifestURL: aApp.manifestURL })); |
|
198 |
|
199 delete aApp.manifest; |
|
200 aDeferred.resolve({ appId: aId, path: aDir.path }); |
|
201 |
|
202 // We can't have appcache for packaged apps. |
|
203 if (!aApp.origin.startsWith("app://")) { |
|
204 reg.startOfflineCacheDownload(new ManifestHelper(manifest, aApp.origin)); |
|
205 } |
|
206 }); |
|
207 // Cleanup by removing the temporary directory. |
|
208 if (aDir.exists()) |
|
209 aDir.remove(true); |
|
210 }); |
|
211 }, |
|
212 |
|
213 _sendError: function wa_actorSendError(aDeferred, aMsg, aId) { |
|
214 debug("Sending error: " + aMsg); |
|
215 aDeferred.resolve({ |
|
216 error: "installationFailed", |
|
217 message: aMsg, |
|
218 appId: aId |
|
219 }); |
|
220 }, |
|
221 |
|
222 _getAppType: function wa_actorGetAppType(aType) { |
|
223 let type = Ci.nsIPrincipal.APP_STATUS_INSTALLED; |
|
224 |
|
225 if (aType) { |
|
226 type = aType == "privileged" ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED |
|
227 : aType == "certified" ? Ci.nsIPrincipal.APP_STATUS_CERTIFIED |
|
228 : Ci.nsIPrincipal.APP_STATUS_INSTALLED; |
|
229 } |
|
230 |
|
231 return type; |
|
232 }, |
|
233 |
|
234 uploadPackage: function () { |
|
235 debug("uploadPackage\n"); |
|
236 let tmpDir = FileUtils.getDir("TmpD", ["file-upload"], true, false); |
|
237 if (!tmpDir.exists() || !tmpDir.isDirectory()) { |
|
238 return {error: "fileAccessError", |
|
239 message: "Unable to create temporary folder"}; |
|
240 } |
|
241 let tmpFile = tmpDir; |
|
242 tmpFile.append("package.zip"); |
|
243 tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); |
|
244 if (!tmpFile.exists() || !tmpDir.isFile()) { |
|
245 return {error: "fileAccessError", |
|
246 message: "Unable to create temporary file"}; |
|
247 } |
|
248 |
|
249 return OS.File.open(tmpFile.path, { write: true, truncate: true }) |
|
250 .then((file) => { |
|
251 let actor = new PackageUploadActor(tmpFile.path, file); |
|
252 this._actorPool.addActor(actor); |
|
253 this._uploads.push(actor); |
|
254 return { actor: actor.actorID }; |
|
255 }); |
|
256 }, |
|
257 |
|
258 installHostedApp: function wa_actorInstallHosted(aDir, aId, aReceipts, |
|
259 aManifest, aMetadata) { |
|
260 debug("installHostedApp"); |
|
261 let self = this; |
|
262 let deferred = promise.defer(); |
|
263 |
|
264 function readManifest() { |
|
265 if (aManifest) { |
|
266 return promise.resolve(aManifest); |
|
267 } else { |
|
268 let manFile = OS.Path.join(aDir.path, "manifest.webapp"); |
|
269 return AppsUtils.loadJSONAsync(manFile); |
|
270 } |
|
271 } |
|
272 function checkSideloading(aManifest) { |
|
273 return self._getAppType(aManifest.type); |
|
274 } |
|
275 function writeManifest(aAppType) { |
|
276 // Move manifest.webapp to the destination directory. |
|
277 // The destination directory for this app. |
|
278 let installDir = DOMApplicationRegistry._getAppDir(aId); |
|
279 if (aManifest) { |
|
280 let manFile = OS.Path.join(installDir.path, "manifest.webapp"); |
|
281 return DOMApplicationRegistry._writeFile(manFile, JSON.stringify(aManifest)).then(() => { |
|
282 return aAppType; |
|
283 }); |
|
284 } else { |
|
285 let manFile = aDir.clone(); |
|
286 manFile.append("manifest.webapp"); |
|
287 manFile.moveTo(installDir, "manifest.webapp"); |
|
288 } |
|
289 return null; |
|
290 } |
|
291 function readMetadata(aAppType) { |
|
292 if (aMetadata) { |
|
293 return { metadata: aMetadata, appType: aAppType }; |
|
294 } |
|
295 // Read the origin and manifest url from metadata.json |
|
296 let metaFile = OS.Path.join(aDir.path, "metadata.json"); |
|
297 return AppsUtils.loadJSONAsync(metaFile).then((aMetadata) => { |
|
298 if (!aMetadata) { |
|
299 throw("Error parsing metadata.json."); |
|
300 } |
|
301 if (!aMetadata.origin) { |
|
302 throw("Missing 'origin' property in metadata.json"); |
|
303 } |
|
304 return { metadata: aMetadata, appType: aAppType }; |
|
305 }); |
|
306 } |
|
307 let runnable = { |
|
308 run: function run() { |
|
309 try { |
|
310 readManifest(). |
|
311 then(writeManifest). |
|
312 then(checkSideloading). |
|
313 then(readMetadata). |
|
314 then(function ({ metadata, appType }) { |
|
315 let origin = metadata.origin; |
|
316 let manifestURL = metadata.manifestURL || |
|
317 origin + "/manifest.webapp"; |
|
318 // Create a fake app object with the minimum set of properties we need. |
|
319 let app = { |
|
320 origin: origin, |
|
321 installOrigin: metadata.installOrigin || origin, |
|
322 manifestURL: manifestURL, |
|
323 appStatus: appType, |
|
324 receipts: aReceipts, |
|
325 }; |
|
326 |
|
327 self._registerApp(deferred, app, aId, aDir); |
|
328 }, function (error) { |
|
329 self._sendError(deferred, error, aId); |
|
330 }); |
|
331 } catch(e) { |
|
332 // If anything goes wrong, just send it back. |
|
333 self._sendError(deferred, e.toString(), aId); |
|
334 } |
|
335 } |
|
336 } |
|
337 |
|
338 Services.tm.currentThread.dispatch(runnable, |
|
339 Ci.nsIThread.DISPATCH_NORMAL); |
|
340 return deferred.promise; |
|
341 }, |
|
342 |
|
343 installPackagedApp: function wa_actorInstallPackaged(aDir, aId, aReceipts) { |
|
344 debug("installPackagedApp"); |
|
345 let self = this; |
|
346 let deferred = promise.defer(); |
|
347 |
|
348 let runnable = { |
|
349 run: function run() { |
|
350 try { |
|
351 // Open the app zip package |
|
352 let zipFile = aDir.clone(); |
|
353 zipFile.append("application.zip"); |
|
354 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] |
|
355 .createInstance(Ci.nsIZipReader); |
|
356 zipReader.open(zipFile); |
|
357 |
|
358 // Read app manifest `manifest.webapp` from `application.zip` |
|
359 let istream = zipReader.getInputStream("manifest.webapp"); |
|
360 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
361 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
362 converter.charset = "UTF-8"; |
|
363 let jsonString = converter.ConvertToUnicode( |
|
364 NetUtil.readInputStreamToString(istream, istream.available()) |
|
365 ); |
|
366 |
|
367 let manifest; |
|
368 try { |
|
369 manifest = JSON.parse(jsonString); |
|
370 } catch(e) { |
|
371 self._sendError(deferred, "Error Parsing manifest.webapp: " + e, aId); |
|
372 } |
|
373 |
|
374 let appType = self._getAppType(manifest.type); |
|
375 |
|
376 // Privileged and certified packaged apps can setup a custom origin |
|
377 // via `origin` manifest property |
|
378 let id = aId; |
|
379 if (appType >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED && |
|
380 manifest.origin !== undefined) { |
|
381 let uri; |
|
382 try { |
|
383 uri = Services.io.newURI(manifest.origin, null, null); |
|
384 } catch(e) { |
|
385 self._sendError(deferred, "Invalid origin in webapp's manifest", aId); |
|
386 } |
|
387 |
|
388 if (uri.scheme != "app") { |
|
389 self._sendError(deferred, "Invalid origin in webapp's manifest", aId); |
|
390 } |
|
391 id = uri.prePath.substring(6); |
|
392 } |
|
393 |
|
394 // Only after security checks are made and after final app id is computed |
|
395 // we can move application.zip to the destination directory, and |
|
396 // extract manifest.webapp there. |
|
397 let installDir = DOMApplicationRegistry._getAppDir(id); |
|
398 let manFile = installDir.clone(); |
|
399 manFile.append("manifest.webapp"); |
|
400 zipReader.extract("manifest.webapp", manFile); |
|
401 zipReader.close(); |
|
402 zipFile.moveTo(installDir, "application.zip"); |
|
403 |
|
404 let origin = "app://" + id; |
|
405 let manifestURL = origin + "/manifest.webapp"; |
|
406 |
|
407 // Refresh application.zip content (e.g. reinstall app), as done here: |
|
408 // http://hg.mozilla.org/mozilla-central/annotate/aaefec5d34f8/dom/apps/src/Webapps.jsm#l1125 |
|
409 // Do it in parent process for the simulator |
|
410 let jar = installDir.clone(); |
|
411 jar.append("application.zip"); |
|
412 Services.obs.notifyObservers(jar, "flush-cache-entry", null); |
|
413 |
|
414 // And then in app content process |
|
415 // This function will be evaluated in the scope of the content process |
|
416 // frame script. That will flush the jar cache for this app and allow |
|
417 // loading fresh updated resources if we reload its document. |
|
418 let FlushFrameScript = function (path) { |
|
419 let jar = Components.classes["@mozilla.org/file/local;1"] |
|
420 .createInstance(Components.interfaces.nsILocalFile); |
|
421 jar.initWithPath(path); |
|
422 let obs = Components.classes["@mozilla.org/observer-service;1"] |
|
423 .getService(Components.interfaces.nsIObserverService); |
|
424 obs.notifyObservers(jar, "flush-cache-entry", null); |
|
425 }; |
|
426 for each (let frame in self._appFrames()) { |
|
427 if (frame.getAttribute("mozapp") == manifestURL) { |
|
428 let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; |
|
429 mm.loadFrameScript("data:," + |
|
430 encodeURIComponent("(" + FlushFrameScript.toString() + ")" + |
|
431 "('" + jar.path + "')"), false); |
|
432 } |
|
433 } |
|
434 |
|
435 // Create a fake app object with the minimum set of properties we need. |
|
436 let app = { |
|
437 origin: origin, |
|
438 installOrigin: origin, |
|
439 manifestURL: manifestURL, |
|
440 appStatus: appType, |
|
441 receipts: aReceipts, |
|
442 } |
|
443 |
|
444 self._registerApp(deferred, app, id, aDir); |
|
445 } catch(e) { |
|
446 // If anything goes wrong, just send it back. |
|
447 self._sendError(deferred, e.toString(), aId); |
|
448 } |
|
449 } |
|
450 } |
|
451 |
|
452 Services.tm.currentThread.dispatch(runnable, |
|
453 Ci.nsIThread.DISPATCH_NORMAL); |
|
454 return deferred.promise; |
|
455 }, |
|
456 |
|
457 /** |
|
458 * @param appId : The id of the app we want to install. We will look for |
|
459 * the files for the app in $TMP/b2g/$appId : |
|
460 * For packaged apps: application.zip |
|
461 * For hosted apps: metadata.json and manifest.webapp |
|
462 */ |
|
463 install: function wa_actorInstall(aRequest) { |
|
464 debug("install"); |
|
465 |
|
466 let appId = aRequest.appId; |
|
467 let reg = DOMApplicationRegistry; |
|
468 if (!appId) { |
|
469 appId = reg.makeAppId(); |
|
470 } |
|
471 |
|
472 // Check that we are not overriding a preinstalled application. |
|
473 if (appId in reg.webapps && reg.webapps[appId].removable === false) { |
|
474 return { error: "badParameterType", |
|
475 message: "The application " + appId + " can't be overriden." |
|
476 } |
|
477 } |
|
478 |
|
479 let appDir = FileUtils.getDir("TmpD", ["b2g", appId], false, false); |
|
480 |
|
481 if (aRequest.upload) { |
|
482 // Ensure creating the directory (recursively) |
|
483 appDir = FileUtils.getDir("TmpD", ["b2g", appId], true, false); |
|
484 let actor = this.conn.getActor(aRequest.upload); |
|
485 if (!actor) { |
|
486 return { error: "badParameter", |
|
487 message: "Unable to find upload actor '" + aRequest.upload |
|
488 + "'" }; |
|
489 } |
|
490 let appFile = FileUtils.File(actor.getFilePath()); |
|
491 if (!appFile.exists()) { |
|
492 return { error: "badParameter", |
|
493 message: "The uploaded file doesn't exist on device" }; |
|
494 } |
|
495 appFile.moveTo(appDir, "application.zip"); |
|
496 } else if ((!appDir || !appDir.exists()) && |
|
497 !aRequest.manifest && !aRequest.metadata) { |
|
498 return { error: "badParameterType", |
|
499 message: "missing directory " + appDir.path |
|
500 }; |
|
501 } |
|
502 |
|
503 let testFile = appDir.clone(); |
|
504 testFile.append("application.zip"); |
|
505 |
|
506 let receipts = (aRequest.receipts && Array.isArray(aRequest.receipts)) |
|
507 ? aRequest.receipts |
|
508 : []; |
|
509 |
|
510 if (testFile.exists()) { |
|
511 return this.installPackagedApp(appDir, appId, receipts); |
|
512 } |
|
513 |
|
514 let manifest, metadata; |
|
515 let missing = |
|
516 ["manifest.webapp", "metadata.json"] |
|
517 .some(function(aName) { |
|
518 testFile = appDir.clone(); |
|
519 testFile.append(aName); |
|
520 return !testFile.exists(); |
|
521 }); |
|
522 if (missing) { |
|
523 if (aRequest.manifest && aRequest.metadata && |
|
524 aRequest.metadata.origin) { |
|
525 manifest = aRequest.manifest; |
|
526 metadata = aRequest.metadata; |
|
527 } else { |
|
528 try { |
|
529 appDir.remove(true); |
|
530 } catch(e) {} |
|
531 return { error: "badParameterType", |
|
532 message: "hosted app file and manifest/metadata fields " + |
|
533 "are missing" |
|
534 }; |
|
535 } |
|
536 } |
|
537 |
|
538 return this.installHostedApp(appDir, appId, receipts, manifest, metadata); |
|
539 }, |
|
540 |
|
541 getAll: function wa_actorGetAll(aRequest) { |
|
542 debug("getAll"); |
|
543 |
|
544 let deferred = promise.defer(); |
|
545 let reg = DOMApplicationRegistry; |
|
546 reg.getAll(apps => { |
|
547 deferred.resolve({ apps: this._filterAllowedApps(apps) }); |
|
548 }); |
|
549 |
|
550 return deferred.promise; |
|
551 }, |
|
552 |
|
553 getApp: function wa_actorGetApp(aRequest) { |
|
554 debug("getApp"); |
|
555 |
|
556 let manifestURL = aRequest.manifestURL; |
|
557 if (!manifestURL) { |
|
558 return { error: "missingParameter", |
|
559 message: "missing parameter manifestURL" }; |
|
560 } |
|
561 |
|
562 let reg = DOMApplicationRegistry; |
|
563 let app = reg.getAppByManifestURL(manifestURL); |
|
564 if (!app) { |
|
565 return { error: "appNotFound" }; |
|
566 } |
|
567 |
|
568 return this._isAppAllowedForURL(app.manifestURL).then(allowed => { |
|
569 if (!allowed) { |
|
570 return { error: "forbidden" }; |
|
571 } |
|
572 return reg.getManifestFor(manifestURL).then(function (manifest) { |
|
573 app.manifest = manifest; |
|
574 return { app: app }; |
|
575 }); |
|
576 }); |
|
577 }, |
|
578 |
|
579 _areCertifiedAppsAllowed: function wa__areCertifiedAppsAllowed() { |
|
580 let pref = "devtools.debugger.forbid-certified-apps"; |
|
581 return !Services.prefs.getBoolPref(pref); |
|
582 }, |
|
583 |
|
584 _isAppAllowedForManifest: function wa__isAppAllowedForManifest(aManifest) { |
|
585 if (this._areCertifiedAppsAllowed()) { |
|
586 return true; |
|
587 } |
|
588 let type = this._getAppType(aManifest.type); |
|
589 return type !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED; |
|
590 }, |
|
591 |
|
592 _filterAllowedApps: function wa__filterAllowedApps(aApps) { |
|
593 return aApps.filter(app => this._isAppAllowedForManifest(app.manifest)); |
|
594 }, |
|
595 |
|
596 _isAppAllowedForURL: function wa__isAppAllowedForURL(aManifestURL) { |
|
597 return this._findManifestByURL(aManifestURL).then(manifest => { |
|
598 return this._isAppAllowedForManifest(manifest); |
|
599 }); |
|
600 }, |
|
601 |
|
602 uninstall: function wa_actorUninstall(aRequest) { |
|
603 debug("uninstall"); |
|
604 |
|
605 let manifestURL = aRequest.manifestURL; |
|
606 if (!manifestURL) { |
|
607 return { error: "missingParameter", |
|
608 message: "missing parameter manifestURL" }; |
|
609 } |
|
610 |
|
611 let deferred = promise.defer(); |
|
612 let reg = DOMApplicationRegistry; |
|
613 reg.uninstall( |
|
614 manifestURL, |
|
615 function onsuccess() { |
|
616 deferred.resolve({}); |
|
617 }, |
|
618 function onfailure(reason) { |
|
619 deferred.resolve({ error: reason }); |
|
620 } |
|
621 ); |
|
622 |
|
623 return deferred.promise; |
|
624 }, |
|
625 |
|
626 _findManifestByURL: function wa__findManifestByURL(aManifestURL) { |
|
627 let deferred = promise.defer(); |
|
628 |
|
629 let reg = DOMApplicationRegistry; |
|
630 let id = reg._appIdForManifestURL(aManifestURL); |
|
631 |
|
632 reg._readManifests([{ id: id }]).then((aResults) => { |
|
633 deferred.resolve(aResults[0].manifest); |
|
634 }); |
|
635 |
|
636 return deferred.promise; |
|
637 }, |
|
638 |
|
639 getIconAsDataURL: function (aRequest) { |
|
640 debug("getIconAsDataURL"); |
|
641 |
|
642 let manifestURL = aRequest.manifestURL; |
|
643 if (!manifestURL) { |
|
644 return { error: "missingParameter", |
|
645 message: "missing parameter manifestURL" }; |
|
646 } |
|
647 |
|
648 let reg = DOMApplicationRegistry; |
|
649 let app = reg.getAppByManifestURL(manifestURL); |
|
650 if (!app) { |
|
651 return { error: "wrongParameter", |
|
652 message: "No application for " + manifestURL }; |
|
653 } |
|
654 |
|
655 let deferred = promise.defer(); |
|
656 |
|
657 this._findManifestByURL(manifestURL).then(jsonManifest => { |
|
658 let manifest = new ManifestHelper(jsonManifest, app.origin); |
|
659 let iconURL = manifest.iconURLForSize(aRequest.size || 128); |
|
660 if (!iconURL) { |
|
661 deferred.resolve({ |
|
662 error: "noIcon", |
|
663 message: "This app has no icon" |
|
664 }); |
|
665 return; |
|
666 } |
|
667 |
|
668 // Download the URL as a blob |
|
669 // bug 899177: there is a bug with xhr and app:// and jar:// uris |
|
670 // that ends up forcing the content type to application/xml. |
|
671 let req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1'] |
|
672 .createInstance(Ci.nsIXMLHttpRequest); |
|
673 req.open("GET", iconURL, false); |
|
674 req.responseType = "blob"; |
|
675 |
|
676 try { |
|
677 req.send(null); |
|
678 } catch(e) { |
|
679 deferred.resolve({ |
|
680 error: "noIcon", |
|
681 message: "The icon file '" + iconURL + "' doesn't exist" |
|
682 }); |
|
683 return; |
|
684 } |
|
685 |
|
686 // Convert the blog to a base64 encoded data URI |
|
687 let reader = Cc["@mozilla.org/files/filereader;1"] |
|
688 .createInstance(Ci.nsIDOMFileReader); |
|
689 reader.onload = function () { |
|
690 deferred.resolve({ |
|
691 url: reader.result |
|
692 }); |
|
693 }; |
|
694 reader.onerror = function () { |
|
695 deferred.resolve({ |
|
696 error: reader.error.name, |
|
697 message: String(reader.error) |
|
698 }); |
|
699 }; |
|
700 reader.readAsDataURL(req.response); |
|
701 }); |
|
702 |
|
703 return deferred.promise; |
|
704 }, |
|
705 |
|
706 launch: function wa_actorLaunch(aRequest) { |
|
707 debug("launch"); |
|
708 |
|
709 let manifestURL = aRequest.manifestURL; |
|
710 if (!manifestURL) { |
|
711 return { error: "missingParameter", |
|
712 message: "missing parameter manifestURL" }; |
|
713 } |
|
714 |
|
715 let deferred = promise.defer(); |
|
716 |
|
717 DOMApplicationRegistry.launch( |
|
718 aRequest.manifestURL, |
|
719 aRequest.startPoint || "", |
|
720 Date.now(), |
|
721 function onsuccess() { |
|
722 deferred.resolve({}); |
|
723 }, |
|
724 function onfailure(reason) { |
|
725 deferred.resolve({ error: reason }); |
|
726 }); |
|
727 |
|
728 return deferred.promise; |
|
729 }, |
|
730 |
|
731 close: function wa_actorLaunch(aRequest) { |
|
732 debug("close"); |
|
733 |
|
734 let manifestURL = aRequest.manifestURL; |
|
735 if (!manifestURL) { |
|
736 return { error: "missingParameter", |
|
737 message: "missing parameter manifestURL" }; |
|
738 } |
|
739 |
|
740 let reg = DOMApplicationRegistry; |
|
741 let app = reg.getAppByManifestURL(manifestURL); |
|
742 if (!app) { |
|
743 return { error: "missingParameter", |
|
744 message: "No application for " + manifestURL }; |
|
745 } |
|
746 |
|
747 reg.close(app); |
|
748 |
|
749 return {}; |
|
750 }, |
|
751 |
|
752 _appFrames: function () { |
|
753 // For now, we only support app frames on b2g |
|
754 if (Services.appinfo.ID != "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { |
|
755 return; |
|
756 } |
|
757 // Register the system app |
|
758 let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); |
|
759 let systemAppFrame = chromeWindow.shell.contentBrowser; |
|
760 yield systemAppFrame; |
|
761 |
|
762 // Register apps hosted in the system app. i.e. the homescreen, all regular |
|
763 // apps and the keyboard. |
|
764 // Bookmark apps and other system app internal frames like captive portal |
|
765 // are also hosted in system app, but they are not using mozapp attribute. |
|
766 let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]"); |
|
767 for (let i = 0; i < frames.length; i++) { |
|
768 yield frames[i]; |
|
769 } |
|
770 }, |
|
771 |
|
772 listRunningApps: function (aRequest) { |
|
773 debug("listRunningApps\n"); |
|
774 |
|
775 let appPromises = []; |
|
776 let apps = []; |
|
777 |
|
778 for each (let frame in this._appFrames()) { |
|
779 let manifestURL = frame.getAttribute("mozapp"); |
|
780 |
|
781 appPromises.push(this._isAppAllowedForURL(manifestURL).then(allowed => { |
|
782 if (allowed) { |
|
783 apps.push(manifestURL); |
|
784 } |
|
785 })); |
|
786 } |
|
787 |
|
788 return promise.all(appPromises).then(() => { |
|
789 return { apps: apps }; |
|
790 }); |
|
791 }, |
|
792 |
|
793 getAppActor: function ({ manifestURL }) { |
|
794 debug("getAppActor\n"); |
|
795 |
|
796 let appFrame = null; |
|
797 for each (let frame in this._appFrames()) { |
|
798 if (frame.getAttribute("mozapp") == manifestURL) { |
|
799 appFrame = frame; |
|
800 break; |
|
801 } |
|
802 } |
|
803 |
|
804 let notFoundError = { |
|
805 error: "appNotFound", |
|
806 message: "Unable to find any opened app whose manifest " + |
|
807 "is '" + manifestURL + "'" |
|
808 }; |
|
809 |
|
810 if (!appFrame) { |
|
811 return notFoundError; |
|
812 } |
|
813 |
|
814 return this._isAppAllowedForURL(manifestURL).then(allowed => { |
|
815 if (!allowed) { |
|
816 return notFoundError; |
|
817 } |
|
818 |
|
819 // Only create a new actor, if we haven't already |
|
820 // instanciated one for this connection. |
|
821 let map = this._appActorsMap; |
|
822 let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner) |
|
823 .frameLoader |
|
824 .messageManager; |
|
825 let actor = map.get(mm); |
|
826 if (!actor) { |
|
827 let onConnect = actor => { |
|
828 map.set(mm, actor); |
|
829 return { actor: actor }; |
|
830 }; |
|
831 let onDisconnect = mm => { |
|
832 map.delete(mm); |
|
833 }; |
|
834 return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect) |
|
835 .then(onConnect); |
|
836 } |
|
837 |
|
838 return { actor: actor }; |
|
839 }); |
|
840 }, |
|
841 |
|
842 watchApps: function () { |
|
843 this._openedApps = new Set(); |
|
844 // For now, app open/close events are only implement on b2g |
|
845 if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { |
|
846 let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); |
|
847 let systemAppFrame = chromeWindow.getContentWindow(); |
|
848 systemAppFrame.addEventListener("appwillopen", this); |
|
849 systemAppFrame.addEventListener("appterminated", this); |
|
850 } |
|
851 Services.obs.addObserver(this, "webapps-installed", false); |
|
852 Services.obs.addObserver(this, "webapps-uninstall", false); |
|
853 |
|
854 return {}; |
|
855 }, |
|
856 |
|
857 unwatchApps: function () { |
|
858 this._openedApps = null; |
|
859 if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") { |
|
860 let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser'); |
|
861 let systemAppFrame = chromeWindow.getContentWindow(); |
|
862 systemAppFrame.removeEventListener("appwillopen", this); |
|
863 systemAppFrame.removeEventListener("appterminated", this); |
|
864 } |
|
865 Services.obs.removeObserver(this, "webapps-installed", false); |
|
866 Services.obs.removeObserver(this, "webapps-uninstall", false); |
|
867 |
|
868 return {}; |
|
869 }, |
|
870 |
|
871 handleEvent: function (event) { |
|
872 let manifestURL; |
|
873 switch(event.type) { |
|
874 case "appwillopen": |
|
875 manifestURL = event.detail.manifestURL; |
|
876 |
|
877 // Ignore the event if we already received an appwillopen for this app |
|
878 // (appwillopen is also fired when the app has been moved to background |
|
879 // and get back to foreground) |
|
880 if (this._openedApps.has(manifestURL)) { |
|
881 return; |
|
882 } |
|
883 this._openedApps.add(manifestURL); |
|
884 |
|
885 this._isAppAllowedForURL(manifestURL).then(allowed => { |
|
886 if (allowed) { |
|
887 this.conn.send({ from: this.actorID, |
|
888 type: "appOpen", |
|
889 manifestURL: manifestURL |
|
890 }); |
|
891 } |
|
892 }); |
|
893 |
|
894 break; |
|
895 |
|
896 case "appterminated": |
|
897 manifestURL = event.detail.manifestURL; |
|
898 this._openedApps.delete(manifestURL); |
|
899 |
|
900 this._isAppAllowedForURL(manifestURL).then(allowed => { |
|
901 if (allowed) { |
|
902 this.conn.send({ from: this.actorID, |
|
903 type: "appClose", |
|
904 manifestURL: manifestURL |
|
905 }); |
|
906 } |
|
907 }); |
|
908 |
|
909 break; |
|
910 } |
|
911 }, |
|
912 |
|
913 observe: function (subject, topic, data) { |
|
914 let app = JSON.parse(data); |
|
915 if (topic == "webapps-installed") { |
|
916 this.conn.send({ from: this.actorID, |
|
917 type: "appInstall", |
|
918 manifestURL: app.manifestURL |
|
919 }); |
|
920 } else if (topic == "webapps-uninstall") { |
|
921 this.conn.send({ from: this.actorID, |
|
922 type: "appUninstall", |
|
923 manifestURL: app.manifestURL |
|
924 }); |
|
925 } |
|
926 } |
|
927 }; |
|
928 |
|
929 /** |
|
930 * The request types this actor can handle. |
|
931 */ |
|
932 WebappsActor.prototype.requestTypes = { |
|
933 "install": WebappsActor.prototype.install, |
|
934 "uploadPackage": WebappsActor.prototype.uploadPackage, |
|
935 "getAll": WebappsActor.prototype.getAll, |
|
936 "getApp": WebappsActor.prototype.getApp, |
|
937 "launch": WebappsActor.prototype.launch, |
|
938 "close": WebappsActor.prototype.close, |
|
939 "uninstall": WebappsActor.prototype.uninstall, |
|
940 "listRunningApps": WebappsActor.prototype.listRunningApps, |
|
941 "getAppActor": WebappsActor.prototype.getAppActor, |
|
942 "watchApps": WebappsActor.prototype.watchApps, |
|
943 "unwatchApps": WebappsActor.prototype.unwatchApps, |
|
944 "getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL |
|
945 }; |
|
946 |
|
947 DebuggerServer.addGlobalActor(WebappsActor, "webappsActor"); |