|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const Cu = Components.utils; |
|
8 const Cc = Components.classes; |
|
9 const Ci = Components.interfaces; |
|
10 const Cr = Components.results; |
|
11 |
|
12 // Possible errors thrown by the signature verifier. |
|
13 const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; |
|
14 const SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11); |
|
15 |
|
16 // We need this to decide if we should accept or not files signed with expired |
|
17 // certificates. |
|
18 function buildIDToTime() { |
|
19 let platformBuildID = |
|
20 Cc["@mozilla.org/xre/app-info;1"] |
|
21 .getService(Ci.nsIXULAppInfo).platformBuildID; |
|
22 let platformBuildIDDate = new Date(); |
|
23 platformBuildIDDate.setUTCFullYear(platformBuildID.substr(0,4), |
|
24 platformBuildID.substr(4,2) - 1, |
|
25 platformBuildID.substr(6,2)); |
|
26 platformBuildIDDate.setUTCHours(platformBuildID.substr(8,2), |
|
27 platformBuildID.substr(10,2), |
|
28 platformBuildID.substr(12,2)); |
|
29 return platformBuildIDDate.getTime(); |
|
30 } |
|
31 |
|
32 const PLATFORM_BUILD_ID_TIME = buildIDToTime(); |
|
33 |
|
34 this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"]; |
|
35 |
|
36 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
37 Cu.import("resource://gre/modules/Services.jsm"); |
|
38 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
39 Cu.import('resource://gre/modules/ActivitiesService.jsm'); |
|
40 Cu.import("resource://gre/modules/AppsUtils.jsm"); |
|
41 Cu.import("resource://gre/modules/AppDownloadManager.jsm"); |
|
42 Cu.import("resource://gre/modules/osfile.jsm"); |
|
43 Cu.import("resource://gre/modules/Task.jsm"); |
|
44 Cu.import("resource://gre/modules/Promise.jsm"); |
|
45 |
|
46 XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate", |
|
47 "resource://gre/modules/StoreTrustAnchor.jsm"); |
|
48 |
|
49 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller", |
|
50 "resource://gre/modules/PermissionsInstaller.jsm"); |
|
51 |
|
52 XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller", |
|
53 "resource://gre/modules/OfflineCacheInstaller.jsm"); |
|
54 |
|
55 XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker", |
|
56 "resource://gre/modules/SystemMessagePermissionsChecker.jsm"); |
|
57 |
|
58 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils", |
|
59 "resource://gre/modules/WebappOSUtils.jsm"); |
|
60 |
|
61 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
62 "resource://gre/modules/NetUtil.jsm"); |
|
63 |
|
64 XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader", |
|
65 "resource://gre/modules/ScriptPreloader.jsm"); |
|
66 |
|
67 #ifdef MOZ_WIDGET_GONK |
|
68 XPCOMUtils.defineLazyGetter(this, "libcutils", function() { |
|
69 Cu.import("resource://gre/modules/systemlibs.js"); |
|
70 return libcutils; |
|
71 }); |
|
72 #endif |
|
73 |
|
74 function debug(aMsg) { |
|
75 #ifdef DEBUG |
|
76 dump("-*- Webapps.jsm : " + aMsg + "\n"); |
|
77 #endif |
|
78 } |
|
79 |
|
80 function getNSPRErrorCode(err) { |
|
81 return -1 * ((err) & 0xffff); |
|
82 } |
|
83 |
|
84 function supportUseCurrentProfile() { |
|
85 return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile"); |
|
86 } |
|
87 |
|
88 function supportSystemMessages() { |
|
89 return Services.prefs.getBoolPref("dom.sysmsg.enabled"); |
|
90 } |
|
91 |
|
92 // Minimum delay between two progress events while downloading, in ms. |
|
93 const MIN_PROGRESS_EVENT_DELAY = 1500; |
|
94 |
|
95 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org"; |
|
96 |
|
97 const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser"; |
|
98 |
|
99 XPCOMUtils.defineLazyServiceGetter(this, "ppmm", |
|
100 "@mozilla.org/parentprocessmessagemanager;1", |
|
101 "nsIMessageBroadcaster"); |
|
102 |
|
103 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
104 "@mozilla.org/childprocessmessagemanager;1", |
|
105 "nsIMessageSender"); |
|
106 |
|
107 XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() { |
|
108 return Cc["@mozilla.org/inter-app-communication-service;1"] |
|
109 .getService(Ci.nsIInterAppCommService); |
|
110 }); |
|
111 |
|
112 XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService", |
|
113 "@mozilla.org/datastore-service;1", |
|
114 "nsIDataStoreService"); |
|
115 |
|
116 XPCOMUtils.defineLazyGetter(this, "msgmgr", function() { |
|
117 return Cc["@mozilla.org/system-message-internal;1"] |
|
118 .getService(Ci.nsISystemMessagesInternal); |
|
119 }); |
|
120 |
|
121 XPCOMUtils.defineLazyGetter(this, "updateSvc", function() { |
|
122 return Cc["@mozilla.org/offlinecacheupdate-service;1"] |
|
123 .getService(Ci.nsIOfflineCacheUpdateService); |
|
124 }); |
|
125 |
|
126 #ifdef MOZ_WIDGET_GONK |
|
127 const DIRECTORY_NAME = "webappsDir"; |
|
128 #elifdef ANDROID |
|
129 const DIRECTORY_NAME = "webappsDir"; |
|
130 #else |
|
131 // If we're executing in the context of the webapp runtime, the data files |
|
132 // are in a different directory (currently the Firefox profile that installed |
|
133 // the webapp); otherwise, they're in the current profile. |
|
134 const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD"; |
|
135 #endif |
|
136 |
|
137 // We'll use this to identify privileged apps that have been preinstalled |
|
138 // For those apps we'll set |
|
139 // STORE_ID_PENDING_PREFIX + installOrigin |
|
140 // as the storeID. This ensures it's unique and can't be set from a legit |
|
141 // store even by error. |
|
142 const STORE_ID_PENDING_PREFIX = "#unknownID#"; |
|
143 |
|
144 this.DOMApplicationRegistry = { |
|
145 // Path to the webapps.json file where we store the registry data. |
|
146 appsFile: null, |
|
147 webapps: { }, |
|
148 children: [ ], |
|
149 allAppsLaunchable: false, |
|
150 _updateHandlers: [ ], |
|
151 |
|
152 init: function() { |
|
153 this.messages = ["Webapps:Install", "Webapps:Uninstall", |
|
154 "Webapps:GetSelf", "Webapps:CheckInstalled", |
|
155 "Webapps:GetInstalled", "Webapps:GetNotInstalled", |
|
156 "Webapps:Launch", "Webapps:GetAll", |
|
157 "Webapps:InstallPackage", |
|
158 "Webapps:GetList", "Webapps:RegisterForMessages", |
|
159 "Webapps:UnregisterForMessages", |
|
160 "Webapps:CancelDownload", "Webapps:CheckForUpdate", |
|
161 "Webapps:Download", "Webapps:ApplyDownload", |
|
162 "Webapps:Install:Return:Ack", "Webapps:AddReceipt", |
|
163 "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt", |
|
164 "child-process-shutdown"]; |
|
165 |
|
166 this.frameMessages = ["Webapps:ClearBrowserData"]; |
|
167 |
|
168 this.messages.forEach((function(msgName) { |
|
169 ppmm.addMessageListener(msgName, this); |
|
170 }).bind(this)); |
|
171 |
|
172 cpmm.addMessageListener("Activities:Register:OK", this); |
|
173 |
|
174 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
175 Services.obs.addObserver(this, "memory-pressure", false); |
|
176 |
|
177 AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this)); |
|
178 |
|
179 this.appsFile = FileUtils.getFile(DIRECTORY_NAME, |
|
180 ["webapps", "webapps.json"], true).path; |
|
181 |
|
182 this.loadAndUpdateApps(); |
|
183 }, |
|
184 |
|
185 // loads the current registry, that could be empty on first run. |
|
186 loadCurrentRegistry: function() { |
|
187 return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => { |
|
188 if (!aData) { |
|
189 return; |
|
190 } |
|
191 |
|
192 this.webapps = aData; |
|
193 let appDir = OS.Path.dirname(this.appsFile); |
|
194 for (let id in this.webapps) { |
|
195 let app = this.webapps[id]; |
|
196 if (!app) { |
|
197 delete this.webapps[id]; |
|
198 continue; |
|
199 } |
|
200 |
|
201 app.id = id; |
|
202 |
|
203 // Make sure we have a localId |
|
204 if (app.localId === undefined) { |
|
205 app.localId = this._nextLocalId(); |
|
206 } |
|
207 |
|
208 if (app.basePath === undefined) { |
|
209 app.basePath = appDir; |
|
210 } |
|
211 |
|
212 // Default to removable apps. |
|
213 if (app.removable === undefined) { |
|
214 app.removable = true; |
|
215 } |
|
216 |
|
217 // Default to a non privileged status. |
|
218 if (app.appStatus === undefined) { |
|
219 app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED; |
|
220 } |
|
221 |
|
222 // Default to NO_APP_ID and not in browser. |
|
223 if (app.installerAppId === undefined) { |
|
224 app.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID; |
|
225 } |
|
226 if (app.installerIsBrowser === undefined) { |
|
227 app.installerIsBrowser = false; |
|
228 } |
|
229 |
|
230 // Default installState to "installed", and reset if we shutdown |
|
231 // during an update. |
|
232 if (app.installState === undefined || |
|
233 app.installState === "updating") { |
|
234 app.installState = "installed"; |
|
235 } |
|
236 |
|
237 // Default storeId to "" and storeVersion to 0 |
|
238 if (this.webapps[id].storeId === undefined) { |
|
239 this.webapps[id].storeId = ""; |
|
240 } |
|
241 if (this.webapps[id].storeVersion === undefined) { |
|
242 this.webapps[id].storeVersion = 0; |
|
243 } |
|
244 |
|
245 // Default role to "". |
|
246 if (this.webapps[id].role === undefined) { |
|
247 this.webapps[id].role = ""; |
|
248 } |
|
249 |
|
250 // At startup we can't be downloading, and the $TMP directory |
|
251 // will be empty so we can't just apply a staged update. |
|
252 app.downloading = false; |
|
253 app.readyToApplyDownload = false; |
|
254 } |
|
255 }); |
|
256 }, |
|
257 |
|
258 // Notify we are starting with registering apps. |
|
259 _registryStarted: Promise.defer(), |
|
260 notifyAppsRegistryStart: function notifyAppsRegistryStart() { |
|
261 Services.obs.notifyObservers(this, "webapps-registry-start", null); |
|
262 this._registryStarted.resolve(); |
|
263 }, |
|
264 |
|
265 get registryStarted() { |
|
266 return this._registryStarted.promise; |
|
267 }, |
|
268 |
|
269 // Notify we are done with registering apps and save a copy of the registry. |
|
270 _registryReady: Promise.defer(), |
|
271 notifyAppsRegistryReady: function notifyAppsRegistryReady() { |
|
272 this._registryReady.resolve(); |
|
273 Services.obs.notifyObservers(this, "webapps-registry-ready", null); |
|
274 this._saveApps(); |
|
275 }, |
|
276 |
|
277 get registryReady() { |
|
278 return this._registryReady.promise; |
|
279 }, |
|
280 |
|
281 // Ensure that the .to property in redirects is a relative URL. |
|
282 sanitizeRedirects: function sanitizeRedirects(aSource) { |
|
283 if (!aSource) { |
|
284 return null; |
|
285 } |
|
286 |
|
287 let res = []; |
|
288 for (let i = 0; i < aSource.length; i++) { |
|
289 let redirect = aSource[i]; |
|
290 if (redirect.from && redirect.to && |
|
291 isAbsoluteURI(redirect.from) && |
|
292 !isAbsoluteURI(redirect.to)) { |
|
293 res.push(redirect); |
|
294 } |
|
295 } |
|
296 return res.length > 0 ? res : null; |
|
297 }, |
|
298 |
|
299 // Registers all the activities and system messages. |
|
300 registerAppsHandlers: function(aRunUpdate) { |
|
301 this.notifyAppsRegistryStart(); |
|
302 let ids = []; |
|
303 for (let id in this.webapps) { |
|
304 ids.push({ id: id }); |
|
305 } |
|
306 if (supportSystemMessages()) { |
|
307 this._processManifestForIds(ids, aRunUpdate); |
|
308 } else { |
|
309 // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on |
|
310 // _processManifestForIds so as to not reading the manifests |
|
311 // twice |
|
312 this._readManifests(ids).then((aResults) => { |
|
313 aResults.forEach((aResult) => { |
|
314 if (!aResult.manifest) { |
|
315 // If we can't load the manifest, we probably have a corrupted |
|
316 // registry. We delete the app since we can't do anything with it. |
|
317 delete this.webapps[aResult.id]; |
|
318 return; |
|
319 } |
|
320 let app = this.webapps[aResult.id]; |
|
321 app.csp = aResult.manifest.csp || ""; |
|
322 app.role = aResult.manifest.role || ""; |
|
323 if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { |
|
324 app.redirects = this.sanitizeRedirects(aResult.redirects); |
|
325 } |
|
326 }); |
|
327 }); |
|
328 |
|
329 // Nothing else to do but notifying we're ready. |
|
330 this.notifyAppsRegistryReady(); |
|
331 } |
|
332 }, |
|
333 |
|
334 updateDataStoreForApp: function(aId) { |
|
335 if (!this.webapps[aId]) { |
|
336 return; |
|
337 } |
|
338 |
|
339 // Create or Update the DataStore for this app |
|
340 this._readManifests([{ id: aId }]).then((aResult) => { |
|
341 let app = this.webapps[aId]; |
|
342 this.updateDataStore(app.localId, app.origin, app.manifestURL, |
|
343 aResult[0].manifest, app.appStatus); |
|
344 }); |
|
345 }, |
|
346 |
|
347 updatePermissionsForApp: function(aId) { |
|
348 if (!this.webapps[aId]) { |
|
349 return; |
|
350 } |
|
351 |
|
352 // Install the permissions for this app, as if we were updating |
|
353 // to cleanup the old ones if needed. |
|
354 // TODO It's not clear what this should do when there are multiple profiles. |
|
355 if (supportUseCurrentProfile()) { |
|
356 this._readManifests([{ id: aId }]).then((aResult) => { |
|
357 let data = aResult[0]; |
|
358 PermissionsInstaller.installPermissions({ |
|
359 manifest: data.manifest, |
|
360 manifestURL: this.webapps[aId].manifestURL, |
|
361 origin: this.webapps[aId].origin |
|
362 }, true, function() { |
|
363 debug("Error installing permissions for " + aId); |
|
364 }); |
|
365 }); |
|
366 } |
|
367 }, |
|
368 |
|
369 updateOfflineCacheForApp: function(aId) { |
|
370 let app = this.webapps[aId]; |
|
371 this._readManifests([{ id: aId }]).then((aResult) => { |
|
372 let manifest = new ManifestHelper(aResult[0].manifest, app.origin); |
|
373 OfflineCacheInstaller.installCache({ |
|
374 cachePath: app.cachePath, |
|
375 appId: aId, |
|
376 origin: Services.io.newURI(app.origin, null, null), |
|
377 localId: app.localId, |
|
378 appcache_path: manifest.fullAppcachePath() |
|
379 }); |
|
380 }); |
|
381 }, |
|
382 |
|
383 // Installs a 3rd party app. |
|
384 installPreinstalledApp: function installPreinstalledApp(aId) { |
|
385 #ifdef MOZ_WIDGET_GONK |
|
386 let app = this.webapps[aId]; |
|
387 let baseDir; |
|
388 try { |
|
389 baseDir = FileUtils.getDir("coreAppsDir", ["webapps", aId], false); |
|
390 if (!baseDir.exists()) { |
|
391 return; |
|
392 } else if (!baseDir.directoryEntries.hasMoreElements()) { |
|
393 debug("Error: Core app in " + baseDir.path + " is empty"); |
|
394 return; |
|
395 } |
|
396 } catch(e) { |
|
397 // In ENG builds, we don't have apps in coreAppsDir. |
|
398 return; |
|
399 } |
|
400 |
|
401 let filesToMove; |
|
402 let isPackage; |
|
403 |
|
404 let updateFile = baseDir.clone(); |
|
405 updateFile.append("update.webapp"); |
|
406 if (!updateFile.exists()) { |
|
407 // The update manifest is missing, this is a hosted app only if there is |
|
408 // no application.zip |
|
409 let appFile = baseDir.clone(); |
|
410 appFile.append("application.zip"); |
|
411 if (appFile.exists()) { |
|
412 return; |
|
413 } |
|
414 |
|
415 isPackage = false; |
|
416 filesToMove = ["manifest.webapp"]; |
|
417 } else { |
|
418 isPackage = true; |
|
419 filesToMove = ["application.zip", "update.webapp"]; |
|
420 } |
|
421 |
|
422 debug("Installing 3rd party app : " + aId + |
|
423 " from " + baseDir.path); |
|
424 |
|
425 // We copy this app to DIRECTORY_NAME/$aId, and set the base path as needed. |
|
426 let destDir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); |
|
427 |
|
428 filesToMove.forEach(function(aFile) { |
|
429 let file = baseDir.clone(); |
|
430 file.append(aFile); |
|
431 try { |
|
432 file.copyTo(destDir, aFile); |
|
433 } catch(e) { |
|
434 debug("Error: Failed to copy " + file.path + " to " + destDir.path); |
|
435 } |
|
436 }); |
|
437 |
|
438 app.installState = "installed"; |
|
439 app.cachePath = app.basePath; |
|
440 app.basePath = OS.Path.dirname(this.appsFile); |
|
441 |
|
442 if (!isPackage) { |
|
443 return; |
|
444 } |
|
445 |
|
446 app.origin = "app://" + aId; |
|
447 |
|
448 // Do this for all preinstalled apps... we can't know at this |
|
449 // point if the updates will be signed or not and it doesn't |
|
450 // hurt to have it always. |
|
451 app.storeId = STORE_ID_PENDING_PREFIX + app.installOrigin; |
|
452 |
|
453 // Extract the manifest.webapp file from application.zip. |
|
454 let zipFile = baseDir.clone(); |
|
455 zipFile.append("application.zip"); |
|
456 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] |
|
457 .createInstance(Ci.nsIZipReader); |
|
458 try { |
|
459 debug("Opening " + zipFile.path); |
|
460 zipReader.open(zipFile); |
|
461 if (!zipReader.hasEntry("manifest.webapp")) { |
|
462 throw "MISSING_MANIFEST"; |
|
463 } |
|
464 let manifestFile = destDir.clone(); |
|
465 manifestFile.append("manifest.webapp"); |
|
466 zipReader.extract("manifest.webapp", manifestFile); |
|
467 } catch(e) { |
|
468 // If we are unable to extract the manifest, cleanup and remove this app. |
|
469 debug("Cleaning up: " + e); |
|
470 destDir.remove(true); |
|
471 delete this.webapps[aId]; |
|
472 } finally { |
|
473 zipReader.close(); |
|
474 } |
|
475 #endif |
|
476 }, |
|
477 |
|
478 // For hosted apps, uninstall an app served from http:// if we have |
|
479 // one installed from the same url with an https:// scheme. |
|
480 removeIfHttpsDuplicate: function(aId) { |
|
481 #ifdef MOZ_WIDGET_GONK |
|
482 let app = this.webapps[aId]; |
|
483 if (!app || !app.origin.startsWith("http://")) { |
|
484 return; |
|
485 } |
|
486 |
|
487 let httpsManifestURL = |
|
488 "https://" + app.manifestURL.substring("http://".length); |
|
489 |
|
490 // This will uninstall the http apps and remove any data hold by this |
|
491 // app. Bug 948105 tracks data migration from http to https apps. |
|
492 for (let id in this.webapps) { |
|
493 if (this.webapps[id].manifestURL === httpsManifestURL) { |
|
494 debug("Found a http/https match: " + app.manifestURL + " / " + |
|
495 this.webapps[id].manifestURL); |
|
496 this.uninstall(app.manifestURL, function() {}, function() {}); |
|
497 return; |
|
498 } |
|
499 } |
|
500 #endif |
|
501 }, |
|
502 |
|
503 // Implements the core of bug 787439 |
|
504 // if at first run, go through these steps: |
|
505 // a. load the core apps registry. |
|
506 // b. uninstall any core app from the current registry but not in the |
|
507 // new core apps registry. |
|
508 // c. for all apps in the new core registry, install them if they are not |
|
509 // yet in the current registry, and run installPermissions() |
|
510 installSystemApps: function() { |
|
511 return Task.spawn(function() { |
|
512 let file; |
|
513 try { |
|
514 file = FileUtils.getFile("coreAppsDir", ["webapps", "webapps.json"], false); |
|
515 } catch(e) { } |
|
516 |
|
517 if (!file || !file.exists()) { |
|
518 return; |
|
519 } |
|
520 |
|
521 // a |
|
522 let data = yield AppsUtils.loadJSONAsync(file.path); |
|
523 if (!data) { |
|
524 return; |
|
525 } |
|
526 |
|
527 // b : core apps are not removable. |
|
528 for (let id in this.webapps) { |
|
529 if (id in data || this.webapps[id].removable) |
|
530 continue; |
|
531 // Remove the permissions, cookies and private data for this app. |
|
532 let localId = this.webapps[id].localId; |
|
533 let permMgr = Cc["@mozilla.org/permissionmanager;1"] |
|
534 .getService(Ci.nsIPermissionManager); |
|
535 permMgr.removePermissionsForApp(localId, false); |
|
536 Services.cookies.removeCookiesForApp(localId, false); |
|
537 this._clearPrivateData(localId, false); |
|
538 delete this.webapps[id]; |
|
539 } |
|
540 |
|
541 let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); |
|
542 // c |
|
543 for (let id in data) { |
|
544 // Core apps have ids matching their domain name (eg: dialer.gaiamobile.org) |
|
545 // Use that property to check if they are new or not. |
|
546 if (!(id in this.webapps)) { |
|
547 this.webapps[id] = data[id]; |
|
548 this.webapps[id].basePath = appDir.path; |
|
549 |
|
550 this.webapps[id].id = id; |
|
551 |
|
552 // Create a new localId. |
|
553 this.webapps[id].localId = this._nextLocalId(); |
|
554 |
|
555 // Core apps are not removable. |
|
556 if (this.webapps[id].removable === undefined) { |
|
557 this.webapps[id].removable = false; |
|
558 } |
|
559 } else { |
|
560 // we fall into this case if the app is present in /system/b2g/webapps/webapps.json |
|
561 // and in /data/local/webapps/webapps.json: this happens when updating gaia apps |
|
562 // Confere bug 989876 |
|
563 this.webapps[id].updateTime = data[id].updateTime; |
|
564 this.webapps[id].lastUpdateCheck = data[id].updateTime; |
|
565 } |
|
566 } |
|
567 }.bind(this)).then(null, Cu.reportError); |
|
568 }, |
|
569 |
|
570 loadAndUpdateApps: function() { |
|
571 return Task.spawn(function() { |
|
572 let runUpdate = AppsUtils.isFirstRun(Services.prefs); |
|
573 |
|
574 yield this.loadCurrentRegistry(); |
|
575 |
|
576 if (runUpdate) { |
|
577 #ifdef MOZ_WIDGET_GONK |
|
578 yield this.installSystemApps(); |
|
579 #endif |
|
580 |
|
581 // At first run, install preloaded apps and set up their permissions. |
|
582 for (let id in this.webapps) { |
|
583 this.installPreinstalledApp(id); |
|
584 this.removeIfHttpsDuplicate(id); |
|
585 if (!this.webapps[id]) { |
|
586 continue; |
|
587 } |
|
588 this.updateOfflineCacheForApp(id); |
|
589 this.updatePermissionsForApp(id); |
|
590 } |
|
591 // Need to update the persisted list of apps since |
|
592 // installPreinstalledApp() removes the ones failing to install. |
|
593 this._saveApps(); |
|
594 } |
|
595 |
|
596 // DataStores must be initialized at startup. |
|
597 for (let id in this.webapps) { |
|
598 this.updateDataStoreForApp(id); |
|
599 } |
|
600 |
|
601 this.registerAppsHandlers(runUpdate); |
|
602 }.bind(this)).then(null, Cu.reportError); |
|
603 }, |
|
604 |
|
605 updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) { |
|
606 // Just Certified Apps can use DataStores |
|
607 let prefName = "dom.testing.datastore_enabled_for_hosted_apps"; |
|
608 if (aAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED && |
|
609 (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID || |
|
610 !Services.prefs.getBoolPref(prefName))) { |
|
611 return; |
|
612 } |
|
613 |
|
614 if ('datastores-owned' in aManifest) { |
|
615 for (let name in aManifest['datastores-owned']) { |
|
616 let readonly = "access" in aManifest['datastores-owned'][name] |
|
617 ? aManifest['datastores-owned'][name].access == 'readonly' |
|
618 : false; |
|
619 |
|
620 dataStoreService.installDataStore(aId, name, aOrigin, aManifestURL, |
|
621 readonly); |
|
622 } |
|
623 } |
|
624 |
|
625 if ('datastores-access' in aManifest) { |
|
626 for (let name in aManifest['datastores-access']) { |
|
627 let readonly = ("readonly" in aManifest['datastores-access'][name]) && |
|
628 !aManifest['datastores-access'][name].readonly |
|
629 ? false : true; |
|
630 |
|
631 dataStoreService.installAccessDataStore(aId, name, aOrigin, |
|
632 aManifestURL, readonly); |
|
633 } |
|
634 } |
|
635 }, |
|
636 |
|
637 // |aEntryPoint| is either the entry_point name or the null in which case we |
|
638 // use the root of the manifest. |
|
639 // |
|
640 // TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...). |
|
641 _registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) { |
|
642 let root = aManifest; |
|
643 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { |
|
644 root = aManifest.entry_points[aEntryPoint]; |
|
645 } |
|
646 |
|
647 if (!root.messages || !Array.isArray(root.messages) || |
|
648 root.messages.length == 0) { |
|
649 return; |
|
650 } |
|
651 |
|
652 let manifest = new ManifestHelper(aManifest, aApp.origin); |
|
653 let launchPath = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), null, null); |
|
654 let manifestURL = Services.io.newURI(aApp.manifestURL, null, null); |
|
655 root.messages.forEach(function registerPages(aMessage) { |
|
656 let href = launchPath; |
|
657 let messageName; |
|
658 if (typeof(aMessage) === "object" && Object.keys(aMessage).length === 1) { |
|
659 messageName = Object.keys(aMessage)[0]; |
|
660 let uri; |
|
661 try { |
|
662 uri = manifest.resolveFromOrigin(aMessage[messageName]); |
|
663 } catch(e) { |
|
664 debug("system message url (" + aMessage[messageName] + ") is invalid, skipping. " + |
|
665 "Error is: " + e); |
|
666 return; |
|
667 } |
|
668 href = Services.io.newURI(uri, null, null); |
|
669 } else { |
|
670 messageName = aMessage; |
|
671 } |
|
672 |
|
673 if (SystemMessagePermissionsChecker |
|
674 .isSystemMessagePermittedToRegister(messageName, |
|
675 aApp.origin, |
|
676 aManifest)) { |
|
677 msgmgr.registerPage(messageName, href, manifestURL); |
|
678 } |
|
679 }); |
|
680 }, |
|
681 |
|
682 // |aEntryPoint| is either the entry_point name or the null in which case we |
|
683 // use the root of the manifest. |
|
684 // |
|
685 // TODO Bug 908094 Refine _registerInterAppConnectionsForEntryPoint(...). |
|
686 _registerInterAppConnectionsForEntryPoint: function(aManifest, aApp, |
|
687 aEntryPoint) { |
|
688 let root = aManifest; |
|
689 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { |
|
690 root = aManifest.entry_points[aEntryPoint]; |
|
691 } |
|
692 |
|
693 let connections = root.connections; |
|
694 if (!connections) { |
|
695 return; |
|
696 } |
|
697 |
|
698 if ((typeof connections) !== "object") { |
|
699 debug("|connections| is not an object. Skipping: " + connections); |
|
700 return; |
|
701 } |
|
702 |
|
703 let manifest = new ManifestHelper(aManifest, aApp.origin); |
|
704 let launchPathURI = Services.io.newURI(manifest.fullLaunchPath(aEntryPoint), |
|
705 null, null); |
|
706 let manifestURI = Services.io.newURI(aApp.manifestURL, null, null); |
|
707 |
|
708 for (let keyword in connections) { |
|
709 let connection = connections[keyword]; |
|
710 |
|
711 // Resolve the handler path from origin. If |handler_path| is absent, |
|
712 // use |launch_path| as default. |
|
713 let fullHandlerPath; |
|
714 let handlerPath = connection.handler_path; |
|
715 if (handlerPath) { |
|
716 try { |
|
717 fullHandlerPath = manifest.resolveFromOrigin(handlerPath); |
|
718 } catch(e) { |
|
719 debug("Connection's handler path is invalid. Skipping: keyword: " + |
|
720 keyword + " handler_path: " + handlerPath); |
|
721 continue; |
|
722 } |
|
723 } |
|
724 let handlerPageURI = fullHandlerPath |
|
725 ? Services.io.newURI(fullHandlerPath, null, null) |
|
726 : launchPathURI; |
|
727 |
|
728 if (SystemMessagePermissionsChecker |
|
729 .isSystemMessagePermittedToRegister("connection", |
|
730 aApp.origin, |
|
731 aManifest)) { |
|
732 msgmgr.registerPage("connection", handlerPageURI, manifestURI); |
|
733 } |
|
734 |
|
735 interAppCommService. |
|
736 registerConnection(keyword, |
|
737 handlerPageURI, |
|
738 manifestURI, |
|
739 connection.description, |
|
740 connection.rules); |
|
741 } |
|
742 }, |
|
743 |
|
744 _registerSystemMessages: function(aManifest, aApp) { |
|
745 this._registerSystemMessagesForEntryPoint(aManifest, aApp, null); |
|
746 |
|
747 if (!aManifest.entry_points) { |
|
748 return; |
|
749 } |
|
750 |
|
751 for (let entryPoint in aManifest.entry_points) { |
|
752 this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint); |
|
753 } |
|
754 }, |
|
755 |
|
756 _registerInterAppConnections: function(aManifest, aApp) { |
|
757 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, null); |
|
758 |
|
759 if (!aManifest.entry_points) { |
|
760 return; |
|
761 } |
|
762 |
|
763 for (let entryPoint in aManifest.entry_points) { |
|
764 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, |
|
765 entryPoint); |
|
766 } |
|
767 }, |
|
768 |
|
769 // |aEntryPoint| is either the entry_point name or the null in which case we |
|
770 // use the root of the manifest. |
|
771 _createActivitiesToRegister: function(aManifest, aApp, aEntryPoint, aRunUpdate) { |
|
772 let activitiesToRegister = []; |
|
773 let root = aManifest; |
|
774 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { |
|
775 root = aManifest.entry_points[aEntryPoint]; |
|
776 } |
|
777 |
|
778 if (!root.activities) { |
|
779 return activitiesToRegister; |
|
780 } |
|
781 |
|
782 let manifest = new ManifestHelper(aManifest, aApp.origin); |
|
783 for (let activity in root.activities) { |
|
784 let description = root.activities[activity]; |
|
785 let href = description.href; |
|
786 if (!href) { |
|
787 href = manifest.launch_path; |
|
788 } |
|
789 |
|
790 try { |
|
791 href = manifest.resolveFromOrigin(href); |
|
792 } catch (e) { |
|
793 debug("Activity href (" + href + ") is invalid, skipping. " + |
|
794 "Error is: " + e); |
|
795 continue; |
|
796 } |
|
797 |
|
798 // Make a copy of the description object since we don't want to modify |
|
799 // the manifest itself, but need to register with a resolved URI. |
|
800 let newDesc = {}; |
|
801 for (let prop in description) { |
|
802 newDesc[prop] = description[prop]; |
|
803 } |
|
804 newDesc.href = href; |
|
805 |
|
806 debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' + |
|
807 activity + ', description.href is ' + newDesc.href); |
|
808 |
|
809 if (aRunUpdate) { |
|
810 activitiesToRegister.push({ "manifest": aApp.manifestURL, |
|
811 "name": activity, |
|
812 "icon": manifest.iconURLForSize(128), |
|
813 "description": newDesc }); |
|
814 } |
|
815 |
|
816 let launchPath = Services.io.newURI(href, null, null); |
|
817 let manifestURL = Services.io.newURI(aApp.manifestURL, null, null); |
|
818 |
|
819 if (SystemMessagePermissionsChecker |
|
820 .isSystemMessagePermittedToRegister("activity", |
|
821 aApp.origin, |
|
822 aManifest)) { |
|
823 msgmgr.registerPage("activity", launchPath, manifestURL); |
|
824 } |
|
825 } |
|
826 return activitiesToRegister; |
|
827 }, |
|
828 |
|
829 // |aAppsToRegister| contains an array of apps to be registered, where |
|
830 // each element is an object in the format of {manifest: foo, app: bar}. |
|
831 _registerActivitiesForApps: function(aAppsToRegister, aRunUpdate) { |
|
832 // Collect the activities to be registered for root and entry_points. |
|
833 let activitiesToRegister = []; |
|
834 aAppsToRegister.forEach(function (aApp) { |
|
835 let manifest = aApp.manifest; |
|
836 let app = aApp.app; |
|
837 activitiesToRegister.push.apply(activitiesToRegister, |
|
838 this._createActivitiesToRegister(manifest, app, null, aRunUpdate)); |
|
839 |
|
840 if (!manifest.entry_points) { |
|
841 return; |
|
842 } |
|
843 |
|
844 for (let entryPoint in manifest.entry_points) { |
|
845 activitiesToRegister.push.apply(activitiesToRegister, |
|
846 this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate)); |
|
847 } |
|
848 }, this); |
|
849 |
|
850 if (!aRunUpdate || activitiesToRegister.length == 0) { |
|
851 this.notifyAppsRegistryReady(); |
|
852 return; |
|
853 } |
|
854 |
|
855 // Send the array carrying all the activities to be registered. |
|
856 cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister); |
|
857 }, |
|
858 |
|
859 // Better to directly use |_registerActivitiesForApps()| if we have |
|
860 // multiple apps to be registered for activities. |
|
861 _registerActivities: function(aManifest, aApp, aRunUpdate) { |
|
862 this._registerActivitiesForApps([{ manifest: aManifest, app: aApp }], aRunUpdate); |
|
863 }, |
|
864 |
|
865 // |aEntryPoint| is either the entry_point name or the null in which case we |
|
866 // use the root of the manifest. |
|
867 _createActivitiesToUnregister: function(aManifest, aApp, aEntryPoint) { |
|
868 let activitiesToUnregister = []; |
|
869 let root = aManifest; |
|
870 if (aEntryPoint && aManifest.entry_points[aEntryPoint]) { |
|
871 root = aManifest.entry_points[aEntryPoint]; |
|
872 } |
|
873 |
|
874 if (!root.activities) { |
|
875 return activitiesToUnregister; |
|
876 } |
|
877 |
|
878 for (let activity in root.activities) { |
|
879 let description = root.activities[activity]; |
|
880 activitiesToUnregister.push({ "manifest": aApp.manifestURL, |
|
881 "name": activity, |
|
882 "description": description }); |
|
883 } |
|
884 return activitiesToUnregister; |
|
885 }, |
|
886 |
|
887 // |aAppsToUnregister| contains an array of apps to be unregistered, where |
|
888 // each element is an object in the format of {manifest: foo, app: bar}. |
|
889 _unregisterActivitiesForApps: function(aAppsToUnregister) { |
|
890 // Collect the activities to be unregistered for root and entry_points. |
|
891 let activitiesToUnregister = []; |
|
892 aAppsToUnregister.forEach(function (aApp) { |
|
893 let manifest = aApp.manifest; |
|
894 let app = aApp.app; |
|
895 activitiesToUnregister.push.apply(activitiesToUnregister, |
|
896 this._createActivitiesToUnregister(manifest, app, null)); |
|
897 |
|
898 if (!manifest.entry_points) { |
|
899 return; |
|
900 } |
|
901 |
|
902 for (let entryPoint in manifest.entry_points) { |
|
903 activitiesToUnregister.push.apply(activitiesToUnregister, |
|
904 this._createActivitiesToUnregister(manifest, app, entryPoint)); |
|
905 } |
|
906 }, this); |
|
907 |
|
908 // Send the array carrying all the activities to be unregistered. |
|
909 cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister); |
|
910 }, |
|
911 |
|
912 // Better to directly use |_unregisterActivitiesForApps()| if we have |
|
913 // multiple apps to be unregistered for activities. |
|
914 _unregisterActivities: function(aManifest, aApp) { |
|
915 this._unregisterActivitiesForApps([{ manifest: aManifest, app: aApp }]); |
|
916 }, |
|
917 |
|
918 _processManifestForIds: function(aIds, aRunUpdate) { |
|
919 this._readManifests(aIds).then((aResults) => { |
|
920 let appsToRegister = []; |
|
921 aResults.forEach((aResult) => { |
|
922 let app = this.webapps[aResult.id]; |
|
923 let manifest = aResult.manifest; |
|
924 if (!manifest) { |
|
925 // If we can't load the manifest, we probably have a corrupted |
|
926 // registry. We delete the app since we can't do anything with it. |
|
927 delete this.webapps[aResult.id]; |
|
928 return; |
|
929 } |
|
930 app.name = manifest.name; |
|
931 app.csp = manifest.csp || ""; |
|
932 app.role = manifest.role || ""; |
|
933 if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { |
|
934 app.redirects = this.sanitizeRedirects(manifest.redirects); |
|
935 } |
|
936 this._registerSystemMessages(manifest, app); |
|
937 this._registerInterAppConnections(manifest, app); |
|
938 appsToRegister.push({ manifest: manifest, app: app }); |
|
939 }); |
|
940 this._registerActivitiesForApps(appsToRegister, aRunUpdate); |
|
941 }); |
|
942 }, |
|
943 |
|
944 observe: function(aSubject, aTopic, aData) { |
|
945 if (aTopic == "xpcom-shutdown") { |
|
946 this.messages.forEach((function(msgName) { |
|
947 ppmm.removeMessageListener(msgName, this); |
|
948 }).bind(this)); |
|
949 Services.obs.removeObserver(this, "xpcom-shutdown"); |
|
950 cpmm = null; |
|
951 ppmm = null; |
|
952 } else if (aTopic == "memory-pressure") { |
|
953 // Clear the manifest cache on memory pressure. |
|
954 this._manifestCache = {}; |
|
955 } |
|
956 }, |
|
957 |
|
958 addMessageListener: function(aMsgNames, aApp, aMm) { |
|
959 aMsgNames.forEach(function (aMsgName) { |
|
960 let man = aApp && aApp.manifestURL; |
|
961 if (!(aMsgName in this.children)) { |
|
962 this.children[aMsgName] = []; |
|
963 } |
|
964 |
|
965 let mmFound = this.children[aMsgName].some(function(mmRef) { |
|
966 if (mmRef.mm === aMm) { |
|
967 mmRef.refCount++; |
|
968 return true; |
|
969 } |
|
970 return false; |
|
971 }); |
|
972 |
|
973 if (!mmFound) { |
|
974 this.children[aMsgName].push({ |
|
975 mm: aMm, |
|
976 refCount: 1 |
|
977 }); |
|
978 } |
|
979 |
|
980 // If the state reported by the registration is outdated, update it now. |
|
981 if ((aMsgName === 'Webapps:FireEvent') || |
|
982 (aMsgName === 'Webapps:UpdateState')) { |
|
983 if (man) { |
|
984 let app = this.getAppByManifestURL(aApp.manifestURL); |
|
985 if (app && ((aApp.installState !== app.installState) || |
|
986 (aApp.downloading !== app.downloading))) { |
|
987 debug("Got a registration from an outdated app: " + |
|
988 aApp.manifestURL); |
|
989 let aEvent ={ |
|
990 type: app.installState, |
|
991 app: app, |
|
992 manifestURL: app.manifestURL, |
|
993 manifest: app.manifest |
|
994 }; |
|
995 aMm.sendAsyncMessage(aMsgName, aEvent); |
|
996 } |
|
997 } |
|
998 } |
|
999 }, this); |
|
1000 }, |
|
1001 |
|
1002 removeMessageListener: function(aMsgNames, aMm) { |
|
1003 if (aMsgNames.length === 1 && |
|
1004 aMsgNames[0] === "Webapps:Internal:AllMessages") { |
|
1005 for (let msgName in this.children) { |
|
1006 let msg = this.children[msgName]; |
|
1007 |
|
1008 for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) { |
|
1009 let mmRef = msg[mmI]; |
|
1010 if (mmRef.mm === aMm) { |
|
1011 msg.splice(mmI, 1); |
|
1012 } |
|
1013 } |
|
1014 |
|
1015 if (msg.length === 0) { |
|
1016 delete this.children[msgName]; |
|
1017 } |
|
1018 } |
|
1019 return; |
|
1020 } |
|
1021 |
|
1022 aMsgNames.forEach(function(aMsgName) { |
|
1023 if (!(aMsgName in this.children)) { |
|
1024 return; |
|
1025 } |
|
1026 |
|
1027 let removeIndex; |
|
1028 this.children[aMsgName].some(function(mmRef, index) { |
|
1029 if (mmRef.mm === aMm) { |
|
1030 mmRef.refCount--; |
|
1031 if (mmRef.refCount === 0) { |
|
1032 removeIndex = index; |
|
1033 } |
|
1034 return true; |
|
1035 } |
|
1036 return false; |
|
1037 }); |
|
1038 |
|
1039 if (removeIndex) { |
|
1040 this.children[aMsgName].splice(removeIndex, 1); |
|
1041 } |
|
1042 }, this); |
|
1043 }, |
|
1044 |
|
1045 receiveMessage: function(aMessage) { |
|
1046 // nsIPrefBranch throws if pref does not exist, faster to simply write |
|
1047 // the pref instead of first checking if it is false. |
|
1048 Services.prefs.setBoolPref("dom.mozApps.used", true); |
|
1049 |
|
1050 // We need to check permissions for calls coming from mozApps.mgmt. |
|
1051 // These are: getAll(), getNotInstalled(), applyDownload() and uninstall(). |
|
1052 if (["Webapps:GetAll", |
|
1053 "Webapps:GetNotInstalled", |
|
1054 "Webapps:ApplyDownload", |
|
1055 "Webapps:Uninstall"].indexOf(aMessage.name) != -1) { |
|
1056 if (!aMessage.target.assertPermission("webapps-manage")) { |
|
1057 debug("mozApps message " + aMessage.name + |
|
1058 " from a content process with no 'webapps-manage' privileges."); |
|
1059 return null; |
|
1060 } |
|
1061 } |
|
1062 |
|
1063 let msg = aMessage.data || {}; |
|
1064 let mm = aMessage.target; |
|
1065 msg.mm = mm; |
|
1066 |
|
1067 switch (aMessage.name) { |
|
1068 case "Webapps:Install": { |
|
1069 #ifdef MOZ_ANDROID_SYNTHAPKS |
|
1070 Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg)); |
|
1071 #else |
|
1072 this.doInstall(msg, mm); |
|
1073 #endif |
|
1074 break; |
|
1075 } |
|
1076 case "Webapps:GetSelf": |
|
1077 this.getSelf(msg, mm); |
|
1078 break; |
|
1079 case "Webapps:Uninstall": |
|
1080 this.doUninstall(msg, mm); |
|
1081 break; |
|
1082 case "Webapps:Launch": |
|
1083 this.doLaunch(msg, mm); |
|
1084 break; |
|
1085 case "Webapps:CheckInstalled": |
|
1086 this.checkInstalled(msg, mm); |
|
1087 break; |
|
1088 case "Webapps:GetInstalled": |
|
1089 this.getInstalled(msg, mm); |
|
1090 break; |
|
1091 case "Webapps:GetNotInstalled": |
|
1092 this.getNotInstalled(msg, mm); |
|
1093 break; |
|
1094 case "Webapps:GetAll": |
|
1095 this.doGetAll(msg, mm); |
|
1096 break; |
|
1097 case "Webapps:InstallPackage": { |
|
1098 #ifdef MOZ_ANDROID_SYNTHAPKS |
|
1099 Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg)); |
|
1100 #else |
|
1101 this.doInstallPackage(msg, mm); |
|
1102 #endif |
|
1103 break; |
|
1104 } |
|
1105 case "Webapps:RegisterForMessages": |
|
1106 this.addMessageListener(msg.messages, msg.app, mm); |
|
1107 break; |
|
1108 case "Webapps:UnregisterForMessages": |
|
1109 this.removeMessageListener(msg, mm); |
|
1110 break; |
|
1111 case "child-process-shutdown": |
|
1112 this.removeMessageListener(["Webapps:Internal:AllMessages"], mm); |
|
1113 break; |
|
1114 case "Webapps:GetList": |
|
1115 this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], null, mm); |
|
1116 return this.webapps; |
|
1117 case "Webapps:Download": |
|
1118 this.startDownload(msg.manifestURL); |
|
1119 break; |
|
1120 case "Webapps:CancelDownload": |
|
1121 this.cancelDownload(msg.manifestURL); |
|
1122 break; |
|
1123 case "Webapps:CheckForUpdate": |
|
1124 this.checkForUpdate(msg, mm); |
|
1125 break; |
|
1126 case "Webapps:ApplyDownload": |
|
1127 this.applyDownload(msg.manifestURL); |
|
1128 break; |
|
1129 case "Activities:Register:OK": |
|
1130 this.notifyAppsRegistryReady(); |
|
1131 break; |
|
1132 case "Webapps:Install:Return:Ack": |
|
1133 this.onInstallSuccessAck(msg.manifestURL); |
|
1134 break; |
|
1135 case "Webapps:AddReceipt": |
|
1136 this.addReceipt(msg, mm); |
|
1137 break; |
|
1138 case "Webapps:RemoveReceipt": |
|
1139 this.removeReceipt(msg, mm); |
|
1140 break; |
|
1141 case "Webapps:ReplaceReceipt": |
|
1142 this.replaceReceipt(msg, mm); |
|
1143 break; |
|
1144 } |
|
1145 }, |
|
1146 |
|
1147 getAppInfo: function getAppInfo(aAppId) { |
|
1148 return AppsUtils.getAppInfo(this.webapps, aAppId); |
|
1149 }, |
|
1150 |
|
1151 // Some messages can be listened by several content processes: |
|
1152 // Webapps:AddApp |
|
1153 // Webapps:RemoveApp |
|
1154 // Webapps:Install:Return:OK |
|
1155 // Webapps:Uninstall:Return:OK |
|
1156 // Webapps:Uninstall:Broadcast:Return:OK |
|
1157 // Webapps:FireEvent |
|
1158 // Webapps:checkForUpdate:Return:OK |
|
1159 // Webapps:UpdateState |
|
1160 broadcastMessage: function broadcastMessage(aMsgName, aContent) { |
|
1161 if (!(aMsgName in this.children)) { |
|
1162 return; |
|
1163 } |
|
1164 this.children[aMsgName].forEach(function(mmRef) { |
|
1165 mmRef.mm.sendAsyncMessage(aMsgName, aContent); |
|
1166 }); |
|
1167 }, |
|
1168 |
|
1169 registerUpdateHandler: function(aHandler) { |
|
1170 this._updateHandlers.push(aHandler); |
|
1171 }, |
|
1172 |
|
1173 unregisterUpdateHandler: function(aHandler) { |
|
1174 let index = this._updateHandlers.indexOf(aHandler); |
|
1175 if (index != -1) { |
|
1176 this._updateHandlers.splice(index, 1); |
|
1177 } |
|
1178 }, |
|
1179 |
|
1180 notifyUpdateHandlers: function(aApp, aManifest, aZipPath) { |
|
1181 for (let updateHandler of this._updateHandlers) { |
|
1182 updateHandler(aApp, aManifest, aZipPath); |
|
1183 } |
|
1184 }, |
|
1185 |
|
1186 _getAppDir: function(aId) { |
|
1187 return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true); |
|
1188 }, |
|
1189 |
|
1190 _writeFile: function(aPath, aData) { |
|
1191 debug("Saving " + aPath); |
|
1192 |
|
1193 let deferred = Promise.defer(); |
|
1194 |
|
1195 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
|
1196 file.initWithPath(aPath); |
|
1197 |
|
1198 // Initialize the file output stream |
|
1199 let ostream = FileUtils.openSafeFileOutputStream(file); |
|
1200 |
|
1201 // Obtain a converter to convert our data to a UTF-8 encoded input stream. |
|
1202 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
1203 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
1204 converter.charset = "UTF-8"; |
|
1205 |
|
1206 // Asynchronously copy the data to the file. |
|
1207 let istream = converter.convertToInputStream(aData); |
|
1208 NetUtil.asyncCopy(istream, ostream, function(aResult) { |
|
1209 if (!Components.isSuccessCode(aResult)) { |
|
1210 deferred.reject() |
|
1211 } else { |
|
1212 deferred.resolve(); |
|
1213 } |
|
1214 }); |
|
1215 |
|
1216 return deferred.promise; |
|
1217 }, |
|
1218 |
|
1219 doLaunch: function (aData, aMm) { |
|
1220 this.launch( |
|
1221 aData.manifestURL, |
|
1222 aData.startPoint, |
|
1223 aData.timestamp, |
|
1224 function onsuccess() { |
|
1225 aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData); |
|
1226 }, |
|
1227 function onfailure(reason) { |
|
1228 aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData); |
|
1229 } |
|
1230 ); |
|
1231 }, |
|
1232 |
|
1233 launch: function launch(aManifestURL, aStartPoint, aTimeStamp, aOnSuccess, aOnFailure) { |
|
1234 let app = this.getAppByManifestURL(aManifestURL); |
|
1235 if (!app) { |
|
1236 aOnFailure("NO_SUCH_APP"); |
|
1237 return; |
|
1238 } |
|
1239 |
|
1240 // Fire an error when trying to launch an app that is not |
|
1241 // yet fully installed. |
|
1242 if (app.installState == "pending") { |
|
1243 aOnFailure("PENDING_APP_NOT_LAUNCHABLE"); |
|
1244 return; |
|
1245 } |
|
1246 |
|
1247 // We have to clone the app object as nsIDOMApplication objects are |
|
1248 // stringified as an empty object. (see bug 830376) |
|
1249 let appClone = AppsUtils.cloneAppObject(app); |
|
1250 appClone.startPoint = aStartPoint; |
|
1251 appClone.timestamp = aTimeStamp; |
|
1252 Services.obs.notifyObservers(null, "webapps-launch", JSON.stringify(appClone)); |
|
1253 aOnSuccess(); |
|
1254 }, |
|
1255 |
|
1256 close: function close(aApp) { |
|
1257 debug("close"); |
|
1258 |
|
1259 // We have to clone the app object as nsIDOMApplication objects are |
|
1260 // stringified as an empty object. (see bug 830376) |
|
1261 let appClone = AppsUtils.cloneAppObject(aApp); |
|
1262 Services.obs.notifyObservers(null, "webapps-close", JSON.stringify(appClone)); |
|
1263 }, |
|
1264 |
|
1265 cancelDownload: function cancelDownload(aManifestURL, aError) { |
|
1266 debug("cancelDownload " + aManifestURL); |
|
1267 let error = aError || "DOWNLOAD_CANCELED"; |
|
1268 let download = AppDownloadManager.get(aManifestURL); |
|
1269 if (!download) { |
|
1270 debug("Could not find a download for " + aManifestURL); |
|
1271 return; |
|
1272 } |
|
1273 |
|
1274 let app = this.webapps[download.appId]; |
|
1275 |
|
1276 if (download.cacheUpdate) { |
|
1277 try { |
|
1278 download.cacheUpdate.cancel(); |
|
1279 } catch (e) { |
|
1280 debug (e); |
|
1281 } |
|
1282 } else if (download.channel) { |
|
1283 try { |
|
1284 download.channel.cancel(Cr.NS_BINDING_ABORTED); |
|
1285 } catch(e) { } |
|
1286 } else { |
|
1287 return; |
|
1288 } |
|
1289 |
|
1290 this._saveApps().then(() => { |
|
1291 this.broadcastMessage("Webapps:UpdateState", { |
|
1292 app: { |
|
1293 progress: 0, |
|
1294 installState: download.previousState, |
|
1295 downloading: false |
|
1296 }, |
|
1297 error: error, |
|
1298 manifestURL: app.manifestURL, |
|
1299 }) |
|
1300 this.broadcastMessage("Webapps:FireEvent", { |
|
1301 eventType: "downloaderror", |
|
1302 manifestURL: app.manifestURL |
|
1303 }); |
|
1304 }); |
|
1305 AppDownloadManager.remove(aManifestURL); |
|
1306 }, |
|
1307 |
|
1308 startDownload: Task.async(function*(aManifestURL) { |
|
1309 debug("startDownload for " + aManifestURL); |
|
1310 |
|
1311 let id = this._appIdForManifestURL(aManifestURL); |
|
1312 let app = this.webapps[id]; |
|
1313 if (!app) { |
|
1314 debug("startDownload: No app found for " + aManifestURL); |
|
1315 return; |
|
1316 } |
|
1317 |
|
1318 if (app.downloading) { |
|
1319 debug("app is already downloading. Ignoring."); |
|
1320 return; |
|
1321 } |
|
1322 |
|
1323 // If the caller is trying to start a download but we have nothing to |
|
1324 // download, send an error. |
|
1325 if (!app.downloadAvailable) { |
|
1326 this.broadcastMessage("Webapps:UpdateState", { |
|
1327 error: "NO_DOWNLOAD_AVAILABLE", |
|
1328 manifestURL: app.manifestURL |
|
1329 }); |
|
1330 this.broadcastMessage("Webapps:FireEvent", { |
|
1331 eventType: "downloaderror", |
|
1332 manifestURL: app.manifestURL |
|
1333 }); |
|
1334 return; |
|
1335 } |
|
1336 |
|
1337 // First of all, we check if the download is supposed to update an |
|
1338 // already installed application. |
|
1339 let isUpdate = (app.installState == "installed"); |
|
1340 |
|
1341 // An app download would only be triggered for two reasons: an app |
|
1342 // update or while retrying to download a previously failed or canceled |
|
1343 // instalation. |
|
1344 app.retryingDownload = !isUpdate; |
|
1345 |
|
1346 // We need to get the update manifest here, not the webapp manifest. |
|
1347 // If this is an update, the update manifest is staged. |
|
1348 let file = FileUtils.getFile(DIRECTORY_NAME, |
|
1349 ["webapps", id, |
|
1350 isUpdate ? "staged-update.webapp" |
|
1351 : "update.webapp"], |
|
1352 true); |
|
1353 |
|
1354 if (!file.exists()) { |
|
1355 // This is a hosted app, let's check if it has an appcache |
|
1356 // and download it. |
|
1357 let results = yield this._readManifests([{ id: id }]); |
|
1358 |
|
1359 let jsonManifest = results[0].manifest; |
|
1360 let manifest = new ManifestHelper(jsonManifest, app.origin); |
|
1361 |
|
1362 if (manifest.appcache_path) { |
|
1363 debug("appcache found"); |
|
1364 this.startOfflineCacheDownload(manifest, app, null, isUpdate); |
|
1365 } else { |
|
1366 // Hosted app with no appcache, nothing to do, but we fire a |
|
1367 // downloaded event. |
|
1368 debug("No appcache found, sending 'downloaded' for " + aManifestURL); |
|
1369 app.downloadAvailable = false; |
|
1370 |
|
1371 yield this._saveApps(); |
|
1372 |
|
1373 this.broadcastMessage("Webapps:UpdateState", { |
|
1374 app: app, |
|
1375 manifest: jsonManifest, |
|
1376 manifestURL: aManifestURL |
|
1377 }); |
|
1378 this.broadcastMessage("Webapps:FireEvent", { |
|
1379 eventType: "downloadsuccess", |
|
1380 manifestURL: aManifestURL |
|
1381 }); |
|
1382 } |
|
1383 |
|
1384 return; |
|
1385 } |
|
1386 |
|
1387 let json = yield AppsUtils.loadJSONAsync(file.path); |
|
1388 if (!json) { |
|
1389 debug("startDownload: No update manifest found at " + file.path + " " + |
|
1390 aManifestURL); |
|
1391 return; |
|
1392 } |
|
1393 |
|
1394 let manifest = new ManifestHelper(json, app.manifestURL); |
|
1395 let [aId, aManifest] = yield this.downloadPackage(manifest, { |
|
1396 manifestURL: aManifestURL, |
|
1397 origin: app.origin, |
|
1398 installOrigin: app.installOrigin, |
|
1399 downloadSize: app.downloadSize |
|
1400 }, isUpdate); |
|
1401 |
|
1402 // Success! Keep the zip in of TmpD, we'll move it out when |
|
1403 // applyDownload() will be called. |
|
1404 // Save the manifest in TmpD also |
|
1405 let manFile = OS.Path.join(OS.Constants.Path.tmpDir, "webapps", aId, |
|
1406 "manifest.webapp"); |
|
1407 yield this._writeFile(manFile, JSON.stringify(aManifest)); |
|
1408 |
|
1409 app = this.webapps[aId]; |
|
1410 // Set state and fire events. |
|
1411 app.downloading = false; |
|
1412 app.downloadAvailable = false; |
|
1413 app.readyToApplyDownload = true; |
|
1414 app.updateTime = Date.now(); |
|
1415 |
|
1416 yield this._saveApps(); |
|
1417 |
|
1418 this.broadcastMessage("Webapps:UpdateState", { |
|
1419 app: app, |
|
1420 manifestURL: aManifestURL |
|
1421 }); |
|
1422 this.broadcastMessage("Webapps:FireEvent", { |
|
1423 eventType: "downloadsuccess", |
|
1424 manifestURL: aManifestURL |
|
1425 }); |
|
1426 if (app.installState == "pending") { |
|
1427 // We restarted a failed download, apply it automatically. |
|
1428 this.applyDownload(aManifestURL); |
|
1429 } |
|
1430 }), |
|
1431 |
|
1432 applyDownload: function applyDownload(aManifestURL) { |
|
1433 debug("applyDownload for " + aManifestURL); |
|
1434 let id = this._appIdForManifestURL(aManifestURL); |
|
1435 let app = this.webapps[id]; |
|
1436 if (!app || (app && !app.readyToApplyDownload)) { |
|
1437 return; |
|
1438 } |
|
1439 |
|
1440 // We need to get the old manifest to unregister web activities. |
|
1441 this.getManifestFor(aManifestURL).then((aOldManifest) => { |
|
1442 // Move the application.zip and manifest.webapp files out of TmpD |
|
1443 let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true); |
|
1444 let manFile = tmpDir.clone(); |
|
1445 manFile.append("manifest.webapp"); |
|
1446 let appFile = tmpDir.clone(); |
|
1447 appFile.append("application.zip"); |
|
1448 |
|
1449 let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); |
|
1450 appFile.moveTo(dir, "application.zip"); |
|
1451 manFile.moveTo(dir, "manifest.webapp"); |
|
1452 |
|
1453 // Move the staged update manifest to a non staged one. |
|
1454 let staged = dir.clone(); |
|
1455 staged.append("staged-update.webapp"); |
|
1456 |
|
1457 // If we are applying after a restarted download, we have no |
|
1458 // staged update manifest. |
|
1459 if (staged.exists()) { |
|
1460 staged.moveTo(dir, "update.webapp"); |
|
1461 } |
|
1462 |
|
1463 try { |
|
1464 tmpDir.remove(true); |
|
1465 } catch(e) { } |
|
1466 |
|
1467 // Clean up the deprecated manifest cache if needed. |
|
1468 if (id in this._manifestCache) { |
|
1469 delete this._manifestCache[id]; |
|
1470 } |
|
1471 |
|
1472 // Flush the zip reader cache to make sure we use the new application.zip |
|
1473 // when re-launching the application. |
|
1474 let zipFile = dir.clone(); |
|
1475 zipFile.append("application.zip"); |
|
1476 Services.obs.notifyObservers(zipFile, "flush-cache-entry", null); |
|
1477 |
|
1478 // Get the manifest, and set properties. |
|
1479 this.getManifestFor(aManifestURL).then((aData) => { |
|
1480 app.downloading = false; |
|
1481 app.downloadAvailable = false; |
|
1482 app.downloadSize = 0; |
|
1483 app.installState = "installed"; |
|
1484 app.readyToApplyDownload = false; |
|
1485 |
|
1486 // Update the staged properties. |
|
1487 if (app.staged) { |
|
1488 for (let prop in app.staged) { |
|
1489 app[prop] = app.staged[prop]; |
|
1490 } |
|
1491 delete app.staged; |
|
1492 } |
|
1493 |
|
1494 delete app.retryingDownload; |
|
1495 |
|
1496 // Update the asm.js scripts we need to compile. |
|
1497 ScriptPreloader.preload(app, aData) |
|
1498 .then(() => this._saveApps()).then(() => { |
|
1499 // Update the handlers and permissions for this app. |
|
1500 this.updateAppHandlers(aOldManifest, aData, app); |
|
1501 |
|
1502 AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => { |
|
1503 let appObject = AppsUtils.cloneAppObject(app); |
|
1504 appObject.updateManifest = aUpdateManifest; |
|
1505 this.notifyUpdateHandlers(appObject, aData, appFile.path); |
|
1506 }); |
|
1507 |
|
1508 if (supportUseCurrentProfile()) { |
|
1509 PermissionsInstaller.installPermissions( |
|
1510 { manifest: aData, |
|
1511 origin: app.origin, |
|
1512 manifestURL: app.manifestURL }, |
|
1513 true); |
|
1514 } |
|
1515 this.updateDataStore(this.webapps[id].localId, app.origin, |
|
1516 app.manifestURL, aData, app.appStatus); |
|
1517 this.broadcastMessage("Webapps:UpdateState", { |
|
1518 app: app, |
|
1519 manifest: aData, |
|
1520 manifestURL: app.manifestURL |
|
1521 }); |
|
1522 this.broadcastMessage("Webapps:FireEvent", { |
|
1523 eventType: "downloadapplied", |
|
1524 manifestURL: app.manifestURL |
|
1525 }); |
|
1526 }); |
|
1527 }); |
|
1528 }); |
|
1529 }, |
|
1530 |
|
1531 startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) { |
|
1532 if (!aManifest.appcache_path) { |
|
1533 return; |
|
1534 } |
|
1535 |
|
1536 // If the manifest has an appcache_path property, use it to populate the |
|
1537 // appcache. |
|
1538 let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(), |
|
1539 null, null); |
|
1540 let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null); |
|
1541 |
|
1542 // We determine the app's 'installState' according to its previous |
|
1543 // state. Cancelled downloads should remain as 'pending'. Successfully |
|
1544 // installed apps should morph to 'updating'. |
|
1545 if (aIsUpdate) { |
|
1546 aApp.installState = "updating"; |
|
1547 } |
|
1548 |
|
1549 // We set the 'downloading' flag and update the apps registry right before |
|
1550 // starting the app download/update. |
|
1551 aApp.downloading = true; |
|
1552 aApp.progress = 0; |
|
1553 DOMApplicationRegistry._saveApps().then(() => { |
|
1554 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { |
|
1555 app: { |
|
1556 downloading: true, |
|
1557 installState: aApp.installState, |
|
1558 progress: 0 |
|
1559 }, |
|
1560 manifestURL: aApp.manifestURL |
|
1561 }); |
|
1562 let cacheUpdate = updateSvc.scheduleAppUpdate( |
|
1563 appcacheURI, docURI, aApp.localId, false, aProfileDir); |
|
1564 |
|
1565 // We save the download details for potential further usage like |
|
1566 // cancelling it. |
|
1567 let download = { |
|
1568 cacheUpdate: cacheUpdate, |
|
1569 appId: this._appIdForManifestURL(aApp.manifestURL), |
|
1570 previousState: aIsUpdate ? "installed" : "pending" |
|
1571 }; |
|
1572 AppDownloadManager.add(aApp.manifestURL, download); |
|
1573 |
|
1574 cacheUpdate.addObserver(new AppcacheObserver(aApp), false); |
|
1575 |
|
1576 }); |
|
1577 }, |
|
1578 |
|
1579 // Returns the MD5 hash of the manifest. |
|
1580 computeManifestHash: function(aManifest) { |
|
1581 return AppsUtils.computeHash(JSON.stringify(aManifest)); |
|
1582 }, |
|
1583 |
|
1584 // Updates the redirect mapping, activities and system message handlers. |
|
1585 // aOldManifest can be null if we don't have any handler to unregister. |
|
1586 updateAppHandlers: function(aOldManifest, aNewManifest, aApp) { |
|
1587 debug("updateAppHandlers: old=" + aOldManifest + " new=" + aNewManifest); |
|
1588 this.notifyAppsRegistryStart(); |
|
1589 if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { |
|
1590 aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects); |
|
1591 } |
|
1592 |
|
1593 if (supportSystemMessages()) { |
|
1594 if (aOldManifest) { |
|
1595 this._unregisterActivities(aOldManifest, aApp); |
|
1596 } |
|
1597 this._registerSystemMessages(aNewManifest, aApp); |
|
1598 this._registerActivities(aNewManifest, aApp, true); |
|
1599 this._registerInterAppConnections(aNewManifest, aApp); |
|
1600 } else { |
|
1601 // Nothing else to do but notifying we're ready. |
|
1602 this.notifyAppsRegistryReady(); |
|
1603 } |
|
1604 }, |
|
1605 |
|
1606 checkForUpdate: function(aData, aMm) { |
|
1607 debug("checkForUpdate for " + aData.manifestURL); |
|
1608 |
|
1609 function sendError(aError) { |
|
1610 aData.error = aError; |
|
1611 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); |
|
1612 } |
|
1613 |
|
1614 let id = this._appIdForManifestURL(aData.manifestURL); |
|
1615 let app = this.webapps[id]; |
|
1616 |
|
1617 // We cannot update an app that does not exists. |
|
1618 if (!app) { |
|
1619 sendError("NO_SUCH_APP"); |
|
1620 return; |
|
1621 } |
|
1622 |
|
1623 // We cannot update an app that is not fully installed. |
|
1624 if (app.installState !== "installed") { |
|
1625 sendError("PENDING_APP_NOT_UPDATABLE"); |
|
1626 return; |
|
1627 } |
|
1628 |
|
1629 // We may be able to remove this when Bug 839071 is fixed. |
|
1630 if (app.downloading) { |
|
1631 sendError("APP_IS_DOWNLOADING"); |
|
1632 return; |
|
1633 } |
|
1634 |
|
1635 // If the app is packaged and its manifestURL has an app:// scheme, |
|
1636 // then we can't have an update. |
|
1637 if (app.origin.startsWith("app://") && |
|
1638 app.manifestURL.startsWith("app://")) { |
|
1639 aData.error = "NOT_UPDATABLE"; |
|
1640 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); |
|
1641 return; |
|
1642 } |
|
1643 |
|
1644 // For non-removable hosted apps that lives in the core apps dir we |
|
1645 // only check the appcache because we can't modify the manifest even |
|
1646 // if it has changed. |
|
1647 let onlyCheckAppCache = false; |
|
1648 |
|
1649 #ifdef MOZ_WIDGET_GONK |
|
1650 let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false); |
|
1651 onlyCheckAppCache = (app.basePath == appDir.path); |
|
1652 #endif |
|
1653 |
|
1654 if (onlyCheckAppCache) { |
|
1655 // Bail out for packaged apps. |
|
1656 if (app.origin.startsWith("app://")) { |
|
1657 aData.error = "NOT_UPDATABLE"; |
|
1658 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); |
|
1659 return; |
|
1660 } |
|
1661 |
|
1662 // We need the manifest to check if we have an appcache. |
|
1663 this._readManifests([{ id: id }]).then((aResult) => { |
|
1664 let manifest = aResult[0].manifest; |
|
1665 if (!manifest.appcache_path) { |
|
1666 aData.error = "NOT_UPDATABLE"; |
|
1667 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); |
|
1668 return; |
|
1669 } |
|
1670 |
|
1671 debug("Checking only appcache for " + aData.manifestURL); |
|
1672 // Check if the appcache is updatable, and send "downloadavailable" or |
|
1673 // "downloadapplied". |
|
1674 let updateObserver = { |
|
1675 observe: function(aSubject, aTopic, aObsData) { |
|
1676 debug("onlyCheckAppCache updateSvc.checkForUpdate return for " + |
|
1677 app.manifestURL + " - event is " + aTopic); |
|
1678 if (aTopic == "offline-cache-update-available") { |
|
1679 app.downloadAvailable = true; |
|
1680 this._saveApps().then(() => { |
|
1681 this.broadcastMessage("Webapps:UpdateState", { |
|
1682 app: app, |
|
1683 manifestURL: app.manifestURL |
|
1684 }); |
|
1685 this.broadcastMessage("Webapps:FireEvent", { |
|
1686 eventType: "downloadavailable", |
|
1687 manifestURL: app.manifestURL, |
|
1688 requestID: aData.requestID |
|
1689 }); |
|
1690 }); |
|
1691 } else { |
|
1692 aData.error = "NOT_UPDATABLE"; |
|
1693 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData); |
|
1694 } |
|
1695 } |
|
1696 }; |
|
1697 let helper = new ManifestHelper(manifest, aData.manifestURL); |
|
1698 debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " + |
|
1699 helper.fullAppcachePath()); |
|
1700 updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null), |
|
1701 app.localId, false, updateObserver); |
|
1702 }); |
|
1703 return; |
|
1704 } |
|
1705 |
|
1706 // On xhr load request event |
|
1707 function onload(xhr, oldManifest) { |
|
1708 debug("Got http status=" + xhr.status + " for " + aData.manifestURL); |
|
1709 let oldHash = app.manifestHash; |
|
1710 let isPackage = app.origin.startsWith("app://"); |
|
1711 |
|
1712 if (xhr.status == 200) { |
|
1713 let manifest = xhr.response; |
|
1714 if (manifest == null) { |
|
1715 sendError("MANIFEST_PARSE_ERROR"); |
|
1716 return; |
|
1717 } |
|
1718 |
|
1719 if (!AppsUtils.checkManifest(manifest, app)) { |
|
1720 sendError("INVALID_MANIFEST"); |
|
1721 return; |
|
1722 } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) { |
|
1723 sendError("INSTALL_FROM_DENIED"); |
|
1724 return; |
|
1725 } else { |
|
1726 AppsUtils.ensureSameAppName(oldManifest, manifest, app); |
|
1727 |
|
1728 let hash = this.computeManifestHash(manifest); |
|
1729 debug("Manifest hash = " + hash); |
|
1730 if (isPackage) { |
|
1731 if (!app.staged) { |
|
1732 app.staged = { }; |
|
1733 } |
|
1734 app.staged.manifestHash = hash; |
|
1735 app.staged.etag = xhr.getResponseHeader("Etag"); |
|
1736 } else { |
|
1737 app.manifestHash = hash; |
|
1738 app.etag = xhr.getResponseHeader("Etag"); |
|
1739 } |
|
1740 |
|
1741 app.lastCheckedUpdate = Date.now(); |
|
1742 if (isPackage) { |
|
1743 if (oldHash != hash) { |
|
1744 this.updatePackagedApp(aData, id, app, manifest); |
|
1745 } else { |
|
1746 this._saveApps().then(() => { |
|
1747 // Like if we got a 304, just send a 'downloadapplied' |
|
1748 // or downloadavailable event. |
|
1749 let eventType = app.downloadAvailable ? "downloadavailable" |
|
1750 : "downloadapplied"; |
|
1751 aMm.sendAsyncMessage("Webapps:UpdateState", { |
|
1752 app: app, |
|
1753 manifestURL: app.manifestURL |
|
1754 }); |
|
1755 aMm.sendAsyncMessage("Webapps:FireEvent", { |
|
1756 eventType: eventType, |
|
1757 manifestURL: app.manifestURL, |
|
1758 requestID: aData.requestID |
|
1759 }); |
|
1760 }); |
|
1761 } |
|
1762 } else { |
|
1763 // Update only the appcache if the manifest has not changed |
|
1764 // based on the hash value. |
|
1765 this.updateHostedApp(aData, id, app, oldManifest, |
|
1766 oldHash == hash ? null : manifest); |
|
1767 } |
|
1768 } |
|
1769 } else if (xhr.status == 304) { |
|
1770 // The manifest has not changed. |
|
1771 if (isPackage) { |
|
1772 app.lastCheckedUpdate = Date.now(); |
|
1773 this._saveApps().then(() => { |
|
1774 // If the app is a packaged app, we just send a 'downloadapplied' |
|
1775 // or downloadavailable event. |
|
1776 let eventType = app.downloadAvailable ? "downloadavailable" |
|
1777 : "downloadapplied"; |
|
1778 aMm.sendAsyncMessage("Webapps:UpdateState", { |
|
1779 app: app, |
|
1780 manifestURL: app.manifestURL |
|
1781 }); |
|
1782 aMm.sendAsyncMessage("Webapps:FireEvent", { |
|
1783 eventType: eventType, |
|
1784 manifestURL: app.manifestURL, |
|
1785 requestID: aData.requestID |
|
1786 }); |
|
1787 }); |
|
1788 } else { |
|
1789 // For hosted apps, even if the manifest has not changed, we check |
|
1790 // for offline cache updates. |
|
1791 this.updateHostedApp(aData, id, app, oldManifest, null); |
|
1792 } |
|
1793 } else { |
|
1794 sendError("MANIFEST_URL_ERROR"); |
|
1795 } |
|
1796 } |
|
1797 |
|
1798 // Try to download a new manifest. |
|
1799 function doRequest(oldManifest, headers) { |
|
1800 headers = headers || []; |
|
1801 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] |
|
1802 .createInstance(Ci.nsIXMLHttpRequest); |
|
1803 xhr.open("GET", aData.manifestURL, true); |
|
1804 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
|
1805 headers.forEach(function(aHeader) { |
|
1806 debug("Adding header: " + aHeader.name + ": " + aHeader.value); |
|
1807 xhr.setRequestHeader(aHeader.name, aHeader.value); |
|
1808 }); |
|
1809 xhr.responseType = "json"; |
|
1810 if (app.etag) { |
|
1811 debug("adding manifest etag:" + app.etag); |
|
1812 xhr.setRequestHeader("If-None-Match", app.etag); |
|
1813 } |
|
1814 xhr.channel.notificationCallbacks = |
|
1815 this.createLoadContext(app.installerAppId, app.installerIsBrowser); |
|
1816 |
|
1817 xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false); |
|
1818 xhr.addEventListener("error", (function() { |
|
1819 sendError("NETWORK_ERROR"); |
|
1820 }).bind(this), false); |
|
1821 |
|
1822 debug("Checking manifest at " + aData.manifestURL); |
|
1823 xhr.send(null); |
|
1824 } |
|
1825 |
|
1826 // Read the current app manifest file |
|
1827 this._readManifests([{ id: id }]).then((aResult) => { |
|
1828 let extraHeaders = []; |
|
1829 #ifdef MOZ_WIDGET_GONK |
|
1830 let pingManifestURL; |
|
1831 try { |
|
1832 pingManifestURL = Services.prefs.getCharPref("ping.manifestURL"); |
|
1833 } catch(e) { } |
|
1834 |
|
1835 if (pingManifestURL && pingManifestURL == aData.manifestURL) { |
|
1836 // Get the device info. |
|
1837 let device = libcutils.property_get("ro.product.model"); |
|
1838 extraHeaders.push({ name: "X-MOZ-B2G-DEVICE", |
|
1839 value: device || "unknown" }); |
|
1840 } |
|
1841 #endif |
|
1842 doRequest.call(this, aResult[0].manifest, extraHeaders); |
|
1843 }); |
|
1844 }, |
|
1845 |
|
1846 // Creates a nsILoadContext object with a given appId and isBrowser flag. |
|
1847 createLoadContext: function createLoadContext(aAppId, aIsBrowser) { |
|
1848 return { |
|
1849 associatedWindow: null, |
|
1850 topWindow : null, |
|
1851 appId: aAppId, |
|
1852 isInBrowserElement: aIsBrowser, |
|
1853 usePrivateBrowsing: false, |
|
1854 isContent: false, |
|
1855 |
|
1856 isAppOfType: function(appType) { |
|
1857 throw Cr.NS_ERROR_NOT_IMPLEMENTED; |
|
1858 }, |
|
1859 |
|
1860 QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext, |
|
1861 Ci.nsIInterfaceRequestor, |
|
1862 Ci.nsISupports]), |
|
1863 getInterface: function(iid) { |
|
1864 if (iid.equals(Ci.nsILoadContext)) |
|
1865 return this; |
|
1866 throw Cr.NS_ERROR_NO_INTERFACE; |
|
1867 } |
|
1868 } |
|
1869 }, |
|
1870 |
|
1871 updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) { |
|
1872 debug("updatePackagedApp"); |
|
1873 |
|
1874 // Store the new update manifest. |
|
1875 let dir = this._getAppDir(aId).path; |
|
1876 let manFile = OS.Path.join(dir, "staged-update.webapp"); |
|
1877 yield this._writeFile(manFile, JSON.stringify(aNewManifest)); |
|
1878 |
|
1879 let manifest = new ManifestHelper(aNewManifest, aApp.manifestURL); |
|
1880 // A package is available: set downloadAvailable to fire the matching |
|
1881 // event. |
|
1882 aApp.downloadAvailable = true; |
|
1883 aApp.downloadSize = manifest.size; |
|
1884 aApp.updateManifest = aNewManifest; |
|
1885 yield this._saveApps(); |
|
1886 |
|
1887 this.broadcastMessage("Webapps:UpdateState", { |
|
1888 app: aApp, |
|
1889 manifestURL: aApp.manifestURL |
|
1890 }); |
|
1891 this.broadcastMessage("Webapps:FireEvent", { |
|
1892 eventType: "downloadavailable", |
|
1893 manifestURL: aApp.manifestURL, |
|
1894 requestID: aData.requestID |
|
1895 }); |
|
1896 }), |
|
1897 |
|
1898 // A hosted app is updated if the app manifest or the appcache needs |
|
1899 // updating. Even if the app manifest has not changed, we still check |
|
1900 // for changes in the app cache. |
|
1901 // 'aNewManifest' would contain the updated app manifest if |
|
1902 // it has actually been updated, while 'aOldManifest' contains the |
|
1903 // stored app manifest. |
|
1904 updateHostedApp: Task.async(function*(aData, aId, aApp, aOldManifest, aNewManifest) { |
|
1905 debug("updateHostedApp " + aData.manifestURL); |
|
1906 |
|
1907 // Clean up the deprecated manifest cache if needed. |
|
1908 if (aId in this._manifestCache) { |
|
1909 delete this._manifestCache[aId]; |
|
1910 } |
|
1911 |
|
1912 aApp.manifest = aNewManifest || aOldManifest; |
|
1913 |
|
1914 let manifest; |
|
1915 if (aNewManifest) { |
|
1916 this.updateAppHandlers(aOldManifest, aNewManifest, aApp); |
|
1917 |
|
1918 this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest); |
|
1919 |
|
1920 // Store the new manifest. |
|
1921 let dir = this._getAppDir(aId).path; |
|
1922 let manFile = OS.Path.join(dir, "manifest.webapp"); |
|
1923 yield this._writeFile(manFile, JSON.stringify(aNewManifest)); |
|
1924 |
|
1925 manifest = new ManifestHelper(aNewManifest, aApp.origin); |
|
1926 |
|
1927 if (supportUseCurrentProfile()) { |
|
1928 // Update the permissions for this app. |
|
1929 PermissionsInstaller.installPermissions({ |
|
1930 manifest: aApp.manifest, |
|
1931 origin: aApp.origin, |
|
1932 manifestURL: aData.manifestURL |
|
1933 }, true); |
|
1934 } |
|
1935 |
|
1936 this.updateDataStore(this.webapps[aId].localId, aApp.origin, |
|
1937 aApp.manifestURL, aApp.manifest, aApp.appStatus); |
|
1938 |
|
1939 aApp.name = manifest.name; |
|
1940 aApp.csp = manifest.csp || ""; |
|
1941 aApp.role = manifest.role || ""; |
|
1942 aApp.updateTime = Date.now(); |
|
1943 } else { |
|
1944 manifest = new ManifestHelper(aOldManifest, aApp.origin); |
|
1945 } |
|
1946 |
|
1947 // Update the registry. |
|
1948 this.webapps[aId] = aApp; |
|
1949 yield this._saveApps(); |
|
1950 |
|
1951 if (!manifest.appcache_path) { |
|
1952 this.broadcastMessage("Webapps:UpdateState", { |
|
1953 app: aApp, |
|
1954 manifest: aApp.manifest, |
|
1955 manifestURL: aApp.manifestURL |
|
1956 }); |
|
1957 this.broadcastMessage("Webapps:FireEvent", { |
|
1958 eventType: "downloadapplied", |
|
1959 manifestURL: aApp.manifestURL, |
|
1960 requestID: aData.requestID |
|
1961 }); |
|
1962 } else { |
|
1963 // Check if the appcache is updatable, and send "downloadavailable" or |
|
1964 // "downloadapplied". |
|
1965 debug("updateHostedApp: updateSvc.checkForUpdate for " + |
|
1966 manifest.fullAppcachePath()); |
|
1967 |
|
1968 let updateDeferred = Promise.defer(); |
|
1969 |
|
1970 updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null), |
|
1971 aApp.localId, false, |
|
1972 (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic)); |
|
1973 |
|
1974 let topic = yield updateDeferred.promise; |
|
1975 |
|
1976 debug("updateHostedApp: updateSvc.checkForUpdate return for " + |
|
1977 aApp.manifestURL + " - event is " + topic); |
|
1978 |
|
1979 let eventType = |
|
1980 topic == "offline-cache-update-available" ? "downloadavailable" |
|
1981 : "downloadapplied"; |
|
1982 |
|
1983 aApp.downloadAvailable = (eventType == "downloadavailable"); |
|
1984 yield this._saveApps(); |
|
1985 |
|
1986 this.broadcastMessage("Webapps:UpdateState", { |
|
1987 app: aApp, |
|
1988 manifest: aApp.manifest, |
|
1989 manifestURL: aApp.manifestURL |
|
1990 }); |
|
1991 this.broadcastMessage("Webapps:FireEvent", { |
|
1992 eventType: eventType, |
|
1993 manifestURL: aApp.manifestURL, |
|
1994 requestID: aData.requestID |
|
1995 }); |
|
1996 } |
|
1997 |
|
1998 delete aApp.manifest; |
|
1999 }), |
|
2000 |
|
2001 // Downloads the manifest and run checks, then eventually triggers the |
|
2002 // installation UI. |
|
2003 doInstall: function doInstall(aData, aMm) { |
|
2004 let app = aData.app; |
|
2005 |
|
2006 let sendError = function sendError(aError) { |
|
2007 aData.error = aError; |
|
2008 aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData); |
|
2009 Cu.reportError("Error installing app from: " + app.installOrigin + |
|
2010 ": " + aError); |
|
2011 }.bind(this); |
|
2012 |
|
2013 if (app.receipts.length > 0) { |
|
2014 for (let receipt of app.receipts) { |
|
2015 let error = this.isReceipt(receipt); |
|
2016 if (error) { |
|
2017 sendError(error); |
|
2018 return; |
|
2019 } |
|
2020 } |
|
2021 } |
|
2022 |
|
2023 // Hosted apps can't be trusted or certified, so just check that the |
|
2024 // manifest doesn't ask for those. |
|
2025 function checkAppStatus(aManifest) { |
|
2026 let manifestStatus = aManifest.type || "web"; |
|
2027 return manifestStatus === "web"; |
|
2028 } |
|
2029 |
|
2030 let checkManifest = (function() { |
|
2031 if (!app.manifest) { |
|
2032 sendError("MANIFEST_PARSE_ERROR"); |
|
2033 return false; |
|
2034 } |
|
2035 |
|
2036 // Disallow multiple hosted apps installations from the same origin for now. |
|
2037 // We will remove this code after multiple apps per origin are supported (bug 778277). |
|
2038 // This will also disallow reinstalls from the same origin for now. |
|
2039 for (let id in this.webapps) { |
|
2040 if (this.webapps[id].origin == app.origin && |
|
2041 !this.webapps[id].packageHash && |
|
2042 this._isLaunchable(this.webapps[id])) { |
|
2043 sendError("MULTIPLE_APPS_PER_ORIGIN_FORBIDDEN"); |
|
2044 return false; |
|
2045 } |
|
2046 } |
|
2047 |
|
2048 if (!AppsUtils.checkManifest(app.manifest, app)) { |
|
2049 sendError("INVALID_MANIFEST"); |
|
2050 return false; |
|
2051 } |
|
2052 |
|
2053 if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) { |
|
2054 sendError("INSTALL_FROM_DENIED"); |
|
2055 return false; |
|
2056 } |
|
2057 |
|
2058 if (!checkAppStatus(app.manifest)) { |
|
2059 sendError("INVALID_SECURITY_LEVEL"); |
|
2060 return false; |
|
2061 } |
|
2062 |
|
2063 return true; |
|
2064 }).bind(this); |
|
2065 |
|
2066 let installApp = (function() { |
|
2067 app.manifestHash = this.computeManifestHash(app.manifest); |
|
2068 // We allow bypassing the install confirmation process to facilitate |
|
2069 // automation. |
|
2070 let prefName = "dom.mozApps.auto_confirm_install"; |
|
2071 if (Services.prefs.prefHasUserValue(prefName) && |
|
2072 Services.prefs.getBoolPref(prefName)) { |
|
2073 this.confirmInstall(aData); |
|
2074 } else { |
|
2075 Services.obs.notifyObservers(aMm, "webapps-ask-install", |
|
2076 JSON.stringify(aData)); |
|
2077 } |
|
2078 }).bind(this); |
|
2079 |
|
2080 // We may already have the manifest (e.g. AutoInstall), |
|
2081 // in which case we don't need to load it. |
|
2082 if (app.manifest) { |
|
2083 if (checkManifest()) { |
|
2084 installApp(); |
|
2085 } |
|
2086 return; |
|
2087 } |
|
2088 |
|
2089 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] |
|
2090 .createInstance(Ci.nsIXMLHttpRequest); |
|
2091 xhr.open("GET", app.manifestURL, true); |
|
2092 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
|
2093 xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, |
|
2094 aData.isBrowser); |
|
2095 xhr.responseType = "json"; |
|
2096 |
|
2097 xhr.addEventListener("load", (function() { |
|
2098 if (xhr.status == 200) { |
|
2099 if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin, |
|
2100 xhr.getResponseHeader("content-type"))) { |
|
2101 sendError("INVALID_MANIFEST"); |
|
2102 return; |
|
2103 } |
|
2104 |
|
2105 app.manifest = xhr.response; |
|
2106 if (checkManifest()) { |
|
2107 app.etag = xhr.getResponseHeader("Etag"); |
|
2108 installApp(); |
|
2109 } |
|
2110 } else { |
|
2111 sendError("MANIFEST_URL_ERROR"); |
|
2112 } |
|
2113 }).bind(this), false); |
|
2114 |
|
2115 xhr.addEventListener("error", (function() { |
|
2116 sendError("NETWORK_ERROR"); |
|
2117 }).bind(this), false); |
|
2118 |
|
2119 xhr.send(null); |
|
2120 }, |
|
2121 |
|
2122 doInstallPackage: function doInstallPackage(aData, aMm) { |
|
2123 let app = aData.app; |
|
2124 |
|
2125 let sendError = function sendError(aError) { |
|
2126 aData.error = aError; |
|
2127 aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData); |
|
2128 Cu.reportError("Error installing packaged app from: " + |
|
2129 app.installOrigin + ": " + aError); |
|
2130 }.bind(this); |
|
2131 |
|
2132 if (app.receipts.length > 0) { |
|
2133 for (let receipt of app.receipts) { |
|
2134 let error = this.isReceipt(receipt); |
|
2135 if (error) { |
|
2136 sendError(error); |
|
2137 return; |
|
2138 } |
|
2139 } |
|
2140 } |
|
2141 |
|
2142 let checkUpdateManifest = (function() { |
|
2143 let manifest = app.updateManifest; |
|
2144 |
|
2145 // Disallow reinstalls from the same manifest URL for now. |
|
2146 let id = this._appIdForManifestURL(app.manifestURL); |
|
2147 if (id !== null && this._isLaunchable(this.webapps[id])) { |
|
2148 sendError("REINSTALL_FORBIDDEN"); |
|
2149 return false; |
|
2150 } |
|
2151 |
|
2152 if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) { |
|
2153 sendError("INVALID_MANIFEST"); |
|
2154 return false; |
|
2155 } |
|
2156 |
|
2157 if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) { |
|
2158 sendError("INSTALL_FROM_DENIED"); |
|
2159 return false; |
|
2160 } |
|
2161 |
|
2162 return true; |
|
2163 }).bind(this); |
|
2164 |
|
2165 let installApp = (function() { |
|
2166 app.manifestHash = this.computeManifestHash(app.updateManifest); |
|
2167 |
|
2168 // We allow bypassing the install confirmation process to facilitate |
|
2169 // automation. |
|
2170 let prefName = "dom.mozApps.auto_confirm_install"; |
|
2171 if (Services.prefs.prefHasUserValue(prefName) && |
|
2172 Services.prefs.getBoolPref(prefName)) { |
|
2173 this.confirmInstall(aData); |
|
2174 } else { |
|
2175 Services.obs.notifyObservers(aMm, "webapps-ask-install", |
|
2176 JSON.stringify(aData)); |
|
2177 } |
|
2178 }).bind(this); |
|
2179 |
|
2180 // We may already have the manifest (e.g. AutoInstall), |
|
2181 // in which case we don't need to load it. |
|
2182 if (app.updateManifest) { |
|
2183 if (checkUpdateManifest()) { |
|
2184 installApp(); |
|
2185 } |
|
2186 return; |
|
2187 } |
|
2188 |
|
2189 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] |
|
2190 .createInstance(Ci.nsIXMLHttpRequest); |
|
2191 xhr.open("GET", app.manifestURL, true); |
|
2192 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
|
2193 xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId, |
|
2194 aData.isBrowser); |
|
2195 xhr.responseType = "json"; |
|
2196 |
|
2197 xhr.addEventListener("load", (function() { |
|
2198 if (xhr.status == 200) { |
|
2199 if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin, |
|
2200 xhr.getResponseHeader("content-type"))) { |
|
2201 sendError("INVALID_MANIFEST"); |
|
2202 return; |
|
2203 } |
|
2204 |
|
2205 app.updateManifest = xhr.response; |
|
2206 if (!app.updateManifest) { |
|
2207 sendError("MANIFEST_PARSE_ERROR"); |
|
2208 return; |
|
2209 } |
|
2210 if (checkUpdateManifest()) { |
|
2211 app.etag = xhr.getResponseHeader("Etag"); |
|
2212 debug("at install package got app etag=" + app.etag); |
|
2213 installApp(); |
|
2214 } |
|
2215 } |
|
2216 else { |
|
2217 sendError("MANIFEST_URL_ERROR"); |
|
2218 } |
|
2219 }).bind(this), false); |
|
2220 |
|
2221 xhr.addEventListener("error", (function() { |
|
2222 sendError("NETWORK_ERROR"); |
|
2223 }).bind(this), false); |
|
2224 |
|
2225 xhr.send(null); |
|
2226 }, |
|
2227 |
|
2228 denyInstall: function(aData) { |
|
2229 let packageId = aData.app.packageId; |
|
2230 if (packageId) { |
|
2231 let dir = FileUtils.getDir("TmpD", ["webapps", packageId], |
|
2232 true, true); |
|
2233 try { |
|
2234 dir.remove(true); |
|
2235 } catch(e) { |
|
2236 } |
|
2237 } |
|
2238 aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData); |
|
2239 }, |
|
2240 |
|
2241 // This function is called after we called the onsuccess callback on the |
|
2242 // content side. This let the webpage the opportunity to set event handlers |
|
2243 // on the app before we start firing progress events. |
|
2244 queuedDownload: {}, |
|
2245 queuedPackageDownload: {}, |
|
2246 |
|
2247 onInstallSuccessAck: function onInstallSuccessAck(aManifestURL, |
|
2248 aDontNeedNetwork) { |
|
2249 // If we are offline, register to run when we'll be online. |
|
2250 if ((Services.io.offline) && !aDontNeedNetwork) { |
|
2251 let onlineWrapper = { |
|
2252 observe: function(aSubject, aTopic, aData) { |
|
2253 Services.obs.removeObserver(onlineWrapper, |
|
2254 "network:offline-status-changed"); |
|
2255 DOMApplicationRegistry.onInstallSuccessAck(aManifestURL); |
|
2256 } |
|
2257 }; |
|
2258 Services.obs.addObserver(onlineWrapper, |
|
2259 "network:offline-status-changed", false); |
|
2260 return; |
|
2261 } |
|
2262 |
|
2263 let cacheDownload = this.queuedDownload[aManifestURL]; |
|
2264 if (cacheDownload) { |
|
2265 this.startOfflineCacheDownload(cacheDownload.manifest, |
|
2266 cacheDownload.app, |
|
2267 cacheDownload.profileDir); |
|
2268 delete this.queuedDownload[aManifestURL]; |
|
2269 |
|
2270 return; |
|
2271 } |
|
2272 |
|
2273 let packageDownload = this.queuedPackageDownload[aManifestURL]; |
|
2274 if (packageDownload) { |
|
2275 let manifest = packageDownload.manifest; |
|
2276 let newApp = packageDownload.app; |
|
2277 let installSuccessCallback = packageDownload.callback; |
|
2278 |
|
2279 delete this.queuedPackageDownload[aManifestURL]; |
|
2280 |
|
2281 this.downloadPackage(manifest, newApp, false).then( |
|
2282 this._onDownloadPackage.bind(this, newApp, installSuccessCallback) |
|
2283 ); |
|
2284 } |
|
2285 }, |
|
2286 |
|
2287 _setupApp: function(aData, aId) { |
|
2288 let app = aData.app; |
|
2289 |
|
2290 // app can be uninstalled |
|
2291 app.removable = true; |
|
2292 |
|
2293 if (aData.isPackage) { |
|
2294 // Override the origin with the correct id. |
|
2295 app.origin = "app://" + aId; |
|
2296 } |
|
2297 |
|
2298 app.id = aId; |
|
2299 app.installTime = Date.now(); |
|
2300 app.lastUpdateCheck = Date.now(); |
|
2301 |
|
2302 return app; |
|
2303 }, |
|
2304 |
|
2305 _cloneApp: function(aData, aNewApp, aManifest, aId, aLocalId) { |
|
2306 let appObject = AppsUtils.cloneAppObject(aNewApp); |
|
2307 appObject.appStatus = |
|
2308 aNewApp.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED; |
|
2309 |
|
2310 if (aManifest.appcache_path) { |
|
2311 appObject.installState = "pending"; |
|
2312 appObject.downloadAvailable = true; |
|
2313 appObject.downloading = true; |
|
2314 appObject.downloadSize = 0; |
|
2315 appObject.readyToApplyDownload = false; |
|
2316 } else if (aManifest.package_path) { |
|
2317 appObject.installState = "pending"; |
|
2318 appObject.downloadAvailable = true; |
|
2319 appObject.downloading = true; |
|
2320 appObject.downloadSize = aManifest.size; |
|
2321 appObject.readyToApplyDownload = false; |
|
2322 } else { |
|
2323 appObject.installState = "installed"; |
|
2324 appObject.downloadAvailable = false; |
|
2325 appObject.downloading = false; |
|
2326 appObject.readyToApplyDownload = false; |
|
2327 } |
|
2328 |
|
2329 appObject.localId = aLocalId; |
|
2330 appObject.basePath = OS.Path.dirname(this.appsFile); |
|
2331 appObject.name = aManifest.name; |
|
2332 appObject.csp = aManifest.csp || ""; |
|
2333 appObject.role = aManifest.role || ""; |
|
2334 appObject.installerAppId = aData.appId; |
|
2335 appObject.installerIsBrowser = aData.isBrowser; |
|
2336 |
|
2337 return appObject; |
|
2338 }, |
|
2339 |
|
2340 _writeManifestFile: function(aId, aIsPackage, aJsonManifest) { |
|
2341 debug("_writeManifestFile"); |
|
2342 |
|
2343 // For packaged apps, keep the update manifest distinct from the app manifest. |
|
2344 let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp"; |
|
2345 |
|
2346 let dir = this._getAppDir(aId).path; |
|
2347 let manFile = OS.Path.join(dir, manifestName); |
|
2348 this._writeFile(manFile, JSON.stringify(aJsonManifest)); |
|
2349 }, |
|
2350 |
|
2351 // Add an app that is already installed to the registry. |
|
2352 addInstalledApp: Task.async(function*(aApp, aManifest, aUpdateManifest) { |
|
2353 if (this.getAppLocalIdByManifestURL(aApp.manifestURL) != |
|
2354 Ci.nsIScriptSecurityManager.NO_APP_ID) { |
|
2355 return; |
|
2356 } |
|
2357 |
|
2358 let app = AppsUtils.cloneAppObject(aApp); |
|
2359 |
|
2360 if (!AppsUtils.checkManifest(aManifest, app) || |
|
2361 (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) { |
|
2362 return; |
|
2363 } |
|
2364 |
|
2365 app.name = aManifest.name; |
|
2366 |
|
2367 app.csp = aManifest.csp || ""; |
|
2368 |
|
2369 app.appStatus = AppsUtils.getAppManifestStatus(aManifest); |
|
2370 |
|
2371 app.removable = true; |
|
2372 |
|
2373 // Reuse the app ID if the scheme is "app". |
|
2374 let uri = Services.io.newURI(app.origin, null, null); |
|
2375 if (uri.scheme == "app") { |
|
2376 app.id = uri.host; |
|
2377 } else { |
|
2378 app.id = this.makeAppId(); |
|
2379 } |
|
2380 |
|
2381 app.localId = this._nextLocalId(); |
|
2382 |
|
2383 app.basePath = OS.Path.dirname(this.appsFile); |
|
2384 |
|
2385 app.progress = 0.0; |
|
2386 app.installState = "installed"; |
|
2387 app.downloadAvailable = false; |
|
2388 app.downloading = false; |
|
2389 app.readyToApplyDownload = false; |
|
2390 |
|
2391 if (aUpdateManifest && aUpdateManifest.size) { |
|
2392 app.downloadSize = aUpdateManifest.size; |
|
2393 } |
|
2394 |
|
2395 app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest || |
|
2396 aManifest)); |
|
2397 |
|
2398 let zipFile = WebappOSUtils.getPackagePath(app); |
|
2399 app.packageHash = yield this._computeFileHash(zipFile); |
|
2400 |
|
2401 app.role = aManifest.role || ""; |
|
2402 |
|
2403 app.redirects = this.sanitizeRedirects(aManifest.redirects); |
|
2404 |
|
2405 this.webapps[app.id] = app; |
|
2406 |
|
2407 // Store the manifest in the manifest cache, so we don't need to re-read it |
|
2408 this._manifestCache[app.id] = app.manifest; |
|
2409 |
|
2410 // Store the manifest and the updateManifest. |
|
2411 this._writeManifestFile(app.id, false, aManifest); |
|
2412 if (aUpdateManifest) { |
|
2413 this._writeManifestFile(app.id, true, aUpdateManifest); |
|
2414 } |
|
2415 |
|
2416 this._saveApps().then(() => { |
|
2417 this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app }); |
|
2418 }); |
|
2419 }), |
|
2420 |
|
2421 confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) { |
|
2422 debug("confirmInstall"); |
|
2423 |
|
2424 let origin = Services.io.newURI(aData.app.origin, null, null); |
|
2425 let id = this._appIdForManifestURL(aData.app.manifestURL); |
|
2426 let manifestURL = origin.resolve(aData.app.manifestURL); |
|
2427 let localId = this.getAppLocalIdByManifestURL(manifestURL); |
|
2428 |
|
2429 let isReinstall = false; |
|
2430 |
|
2431 // Installing an application again is considered as an update. |
|
2432 if (id) { |
|
2433 isReinstall = true; |
|
2434 let dir = this._getAppDir(id); |
|
2435 try { |
|
2436 dir.remove(true); |
|
2437 } catch(e) { } |
|
2438 } else { |
|
2439 id = this.makeAppId(); |
|
2440 localId = this._nextLocalId(); |
|
2441 } |
|
2442 |
|
2443 let app = this._setupApp(aData, id); |
|
2444 |
|
2445 let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest; |
|
2446 this._writeManifestFile(id, aData.isPackage, jsonManifest); |
|
2447 |
|
2448 debug("app.origin: " + app.origin); |
|
2449 let manifest = new ManifestHelper(jsonManifest, app.origin); |
|
2450 |
|
2451 let appObject = this._cloneApp(aData, app, manifest, id, localId); |
|
2452 |
|
2453 this.webapps[id] = appObject; |
|
2454 |
|
2455 // For package apps, the permissions are not in the mini-manifest, so |
|
2456 // don't update the permissions yet. |
|
2457 if (!aData.isPackage) { |
|
2458 if (supportUseCurrentProfile()) { |
|
2459 PermissionsInstaller.installPermissions( |
|
2460 { |
|
2461 origin: appObject.origin, |
|
2462 manifestURL: appObject.manifestURL, |
|
2463 manifest: jsonManifest |
|
2464 }, |
|
2465 isReinstall, |
|
2466 this.uninstall.bind(this, aData, aData.mm) |
|
2467 ); |
|
2468 } |
|
2469 |
|
2470 this.updateDataStore(this.webapps[id].localId, this.webapps[id].origin, |
|
2471 this.webapps[id].manifestURL, jsonManifest, |
|
2472 this.webapps[id].appStatus); |
|
2473 } |
|
2474 |
|
2475 for each (let prop in ["installState", "downloadAvailable", "downloading", |
|
2476 "downloadSize", "readyToApplyDownload"]) { |
|
2477 aData.app[prop] = appObject[prop]; |
|
2478 } |
|
2479 |
|
2480 if (manifest.appcache_path) { |
|
2481 this.queuedDownload[app.manifestURL] = { |
|
2482 manifest: manifest, |
|
2483 app: appObject, |
|
2484 profileDir: aProfileDir |
|
2485 } |
|
2486 } |
|
2487 |
|
2488 // We notify about the successful installation via mgmt.oninstall and the |
|
2489 // corresponging DOMRequest.onsuccess event as soon as the app is properly |
|
2490 // saved in the registry. |
|
2491 this._saveApps().then(() => { |
|
2492 this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject }); |
|
2493 if (aData.isPackage && aData.apkInstall && !aData.requestID) { |
|
2494 // Skip directly to onInstallSuccessAck, since there isn't |
|
2495 // a WebappsRegistry to receive Webapps:Install:Return:OK and respond |
|
2496 // Webapps:Install:Return:Ack when an app is being auto-installed. |
|
2497 this.onInstallSuccessAck(app.manifestURL); |
|
2498 } else { |
|
2499 // Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify |
|
2500 // the installing page about the successful install, after which it'll |
|
2501 // respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck. |
|
2502 this.broadcastMessage("Webapps:Install:Return:OK", aData); |
|
2503 } |
|
2504 if (!aData.isPackage) { |
|
2505 this.updateAppHandlers(null, app.manifest, app); |
|
2506 if (aInstallSuccessCallback) { |
|
2507 aInstallSuccessCallback(app.manifest); |
|
2508 } |
|
2509 } |
|
2510 Services.obs.notifyObservers(null, "webapps-installed", |
|
2511 JSON.stringify({ manifestURL: app.manifestURL })); |
|
2512 }); |
|
2513 |
|
2514 let dontNeedNetwork = false; |
|
2515 if (manifest.package_path) { |
|
2516 // If it is a local app then it must been installed from a local file |
|
2517 // instead of web. |
|
2518 #ifdef MOZ_ANDROID_SYNTHAPKS |
|
2519 // In that case, we would already have the manifest, not just the update |
|
2520 // manifest. |
|
2521 dontNeedNetwork = !!aData.app.manifest; |
|
2522 #else |
|
2523 if (aData.app.localInstallPath) { |
|
2524 dontNeedNetwork = true; |
|
2525 jsonManifest.package_path = "file://" + aData.app.localInstallPath; |
|
2526 } |
|
2527 #endif |
|
2528 |
|
2529 // origin for install apps is meaningless here, since it's app:// and this |
|
2530 // can't be used to resolve package paths. |
|
2531 manifest = new ManifestHelper(jsonManifest, app.manifestURL); |
|
2532 |
|
2533 this.queuedPackageDownload[app.manifestURL] = { |
|
2534 manifest: manifest, |
|
2535 app: appObject, |
|
2536 callback: aInstallSuccessCallback |
|
2537 }; |
|
2538 } |
|
2539 |
|
2540 if (aData.forceSuccessAck) { |
|
2541 // If it's a local install, there's no content process so just |
|
2542 // ack the install. |
|
2543 this.onInstallSuccessAck(app.manifestURL, dontNeedNetwork); |
|
2544 } |
|
2545 }, |
|
2546 |
|
2547 /** |
|
2548 * Install the package after successfully downloading it |
|
2549 * |
|
2550 * Bound params: |
|
2551 * |
|
2552 * @param aNewApp {Object} the new app data |
|
2553 * @param aInstallSuccessCallback {Function} |
|
2554 * the callback to call on install success |
|
2555 * |
|
2556 * Passed params: |
|
2557 * |
|
2558 * @param aId {Integer} the unique ID of the application |
|
2559 * @param aManifest {Object} The manifest of the application |
|
2560 */ |
|
2561 _onDownloadPackage: Task.async(function*(aNewApp, aInstallSuccessCallback, |
|
2562 [aId, aManifest]) { |
|
2563 debug("_onDownloadPackage"); |
|
2564 // Success! Move the zip out of TmpD. |
|
2565 let app = this.webapps[aId]; |
|
2566 let zipFile = |
|
2567 FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true); |
|
2568 let dir = this._getAppDir(aId); |
|
2569 zipFile.moveTo(dir, "application.zip"); |
|
2570 let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true); |
|
2571 try { |
|
2572 tmpDir.remove(true); |
|
2573 } catch(e) { } |
|
2574 |
|
2575 // Save the manifest |
|
2576 let manFile = OS.Path.join(dir.path, "manifest.webapp"); |
|
2577 yield this._writeFile(manFile, JSON.stringify(aManifest)); |
|
2578 // Set state and fire events. |
|
2579 app.installState = "installed"; |
|
2580 app.downloading = false; |
|
2581 app.downloadAvailable = false; |
|
2582 |
|
2583 yield this._saveApps(); |
|
2584 |
|
2585 this.updateAppHandlers(null, aManifest, aNewApp); |
|
2586 // Clear the manifest cache in case it holds the update manifest. |
|
2587 if (aId in this._manifestCache) { |
|
2588 delete this._manifestCache[aId]; |
|
2589 } |
|
2590 |
|
2591 this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp }); |
|
2592 Services.obs.notifyObservers(null, "webapps-installed", |
|
2593 JSON.stringify({ manifestURL: aNewApp.manifestURL })); |
|
2594 |
|
2595 if (supportUseCurrentProfile()) { |
|
2596 // Update the permissions for this app. |
|
2597 PermissionsInstaller.installPermissions({ |
|
2598 manifest: aManifest, |
|
2599 origin: aNewApp.origin, |
|
2600 manifestURL: aNewApp.manifestURL |
|
2601 }, true); |
|
2602 } |
|
2603 |
|
2604 this.updateDataStore(this.webapps[aId].localId, aNewApp.origin, |
|
2605 aNewApp.manifestURL, aManifest, aNewApp.appStatus); |
|
2606 |
|
2607 this.broadcastMessage("Webapps:UpdateState", { |
|
2608 app: app, |
|
2609 manifest: aManifest, |
|
2610 manifestURL: aNewApp.manifestURL |
|
2611 }); |
|
2612 |
|
2613 // Check if we have asm.js code to preload for this application. |
|
2614 yield ScriptPreloader.preload(aNewApp, aManifest); |
|
2615 |
|
2616 this.broadcastMessage("Webapps:FireEvent", { |
|
2617 eventType: ["downloadsuccess", "downloadapplied"], |
|
2618 manifestURL: aNewApp.manifestURL |
|
2619 }); |
|
2620 |
|
2621 if (aInstallSuccessCallback) { |
|
2622 aInstallSuccessCallback(aManifest, zipFile.path); |
|
2623 } |
|
2624 }), |
|
2625 |
|
2626 _nextLocalId: function() { |
|
2627 let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1; |
|
2628 |
|
2629 while (this.getManifestURLByLocalId(id)) { |
|
2630 id++; |
|
2631 } |
|
2632 |
|
2633 Services.prefs.setIntPref("dom.mozApps.maxLocalId", id); |
|
2634 Services.prefs.savePrefFile(null); |
|
2635 return id; |
|
2636 }, |
|
2637 |
|
2638 _appIdForManifestURL: function(aURI) { |
|
2639 for (let id in this.webapps) { |
|
2640 if (this.webapps[id].manifestURL == aURI) |
|
2641 return id; |
|
2642 } |
|
2643 return null; |
|
2644 }, |
|
2645 |
|
2646 makeAppId: function() { |
|
2647 let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); |
|
2648 return uuidGenerator.generateUUID().toString(); |
|
2649 }, |
|
2650 |
|
2651 _saveApps: function() { |
|
2652 return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2)); |
|
2653 }, |
|
2654 |
|
2655 /** |
|
2656 * Asynchronously reads a list of manifests |
|
2657 */ |
|
2658 |
|
2659 _manifestCache: {}, |
|
2660 |
|
2661 _readManifests: function(aData) { |
|
2662 return Task.spawn(function*() { |
|
2663 for (let elem of aData) { |
|
2664 let id = elem.id; |
|
2665 |
|
2666 if (!this._manifestCache[id]) { |
|
2667 // the manifest file used to be named manifest.json, so fallback on this. |
|
2668 let baseDir = this.webapps[id].basePath == this.getCoreAppsBasePath() |
|
2669 ? "coreAppsDir" : DIRECTORY_NAME; |
|
2670 |
|
2671 let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true); |
|
2672 |
|
2673 let fileNames = ["manifest.webapp", "update.webapp", "manifest.json"]; |
|
2674 for (let fileName of fileNames) { |
|
2675 this._manifestCache[id] = yield AppsUtils.loadJSONAsync(OS.Path.join(dir.path, fileName)); |
|
2676 if (this._manifestCache[id]) { |
|
2677 break; |
|
2678 } |
|
2679 } |
|
2680 } |
|
2681 |
|
2682 elem.manifest = this._manifestCache[id]; |
|
2683 } |
|
2684 |
|
2685 return aData; |
|
2686 }.bind(this)).then(null, Cu.reportError); |
|
2687 }, |
|
2688 |
|
2689 downloadPackage: function(aManifest, aNewApp, aIsUpdate, aOnSuccess) { |
|
2690 // Here are the steps when installing a package: |
|
2691 // - create a temp directory where to store the app. |
|
2692 // - download the zip in this directory. |
|
2693 // - check the signature on the zip. |
|
2694 // - extract the manifest from the zip and check it. |
|
2695 // - ask confirmation to the user. |
|
2696 // - add the new app to the registry. |
|
2697 // If we fail at any step, we revert the previous ones and return an error. |
|
2698 |
|
2699 // We define these outside the task to use them in its reject handler. |
|
2700 let id = this._appIdForManifestURL(aNewApp.manifestURL); |
|
2701 let oldApp = this.webapps[id]; |
|
2702 |
|
2703 return Task.spawn((function*() { |
|
2704 yield this._ensureSufficientStorage(aNewApp); |
|
2705 |
|
2706 let fullPackagePath = aManifest.fullPackagePath(); |
|
2707 |
|
2708 // Check if it's a local file install (we've downloaded/sideloaded the |
|
2709 // package already, it existed on the build, or it came with an APK). |
|
2710 // Note that this variable also controls whether files signed with expired |
|
2711 // certificates are accepted or not. If isLocalFileInstall is true and the |
|
2712 // device date is earlier than the build generation date, then the signature |
|
2713 // will be accepted even if the certificate is expired. |
|
2714 let isLocalFileInstall = |
|
2715 Services.io.extractScheme(fullPackagePath) === 'file'; |
|
2716 |
|
2717 debug("About to download " + fullPackagePath); |
|
2718 |
|
2719 let requestChannel = this._getRequestChannel(fullPackagePath, |
|
2720 isLocalFileInstall, |
|
2721 oldApp, |
|
2722 aNewApp); |
|
2723 |
|
2724 AppDownloadManager.add( |
|
2725 aNewApp.manifestURL, |
|
2726 { |
|
2727 channel: requestChannel, |
|
2728 appId: id, |
|
2729 previousState: aIsUpdate ? "installed" : "pending" |
|
2730 } |
|
2731 ); |
|
2732 |
|
2733 // We set the 'downloading' flag to true right before starting the fetch. |
|
2734 oldApp.downloading = true; |
|
2735 |
|
2736 // We determine the app's 'installState' according to its previous |
|
2737 // state. Cancelled download should remain as 'pending'. Successfully |
|
2738 // installed apps should morph to 'updating'. |
|
2739 oldApp.installState = aIsUpdate ? "updating" : "pending"; |
|
2740 |
|
2741 // initialize the progress to 0 right now |
|
2742 oldApp.progress = 0; |
|
2743 |
|
2744 let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp); |
|
2745 let hash = yield this._computeFileHash(zipFile.path); |
|
2746 |
|
2747 let responseStatus = requestChannel.responseStatus; |
|
2748 let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash); |
|
2749 |
|
2750 if (oldPackage) { |
|
2751 debug("package's etag or hash unchanged; sending 'applied' event"); |
|
2752 // The package's Etag or hash has not changed. |
|
2753 // We send a "applied" event right away. |
|
2754 this._sendAppliedEvent(aNewApp, oldApp, id); |
|
2755 return; |
|
2756 } |
|
2757 |
|
2758 let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp, |
|
2759 isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash); |
|
2760 |
|
2761 AppDownloadManager.remove(aNewApp.manifestURL); |
|
2762 |
|
2763 return [oldApp.id, newManifest]; |
|
2764 |
|
2765 }).bind(this)).then( |
|
2766 aOnSuccess, |
|
2767 this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate) |
|
2768 ); |
|
2769 }, |
|
2770 |
|
2771 _ensureSufficientStorage: function(aNewApp) { |
|
2772 let deferred = Promise.defer(); |
|
2773 |
|
2774 let navigator = Services.wm.getMostRecentWindow(chromeWindowType) |
|
2775 .navigator; |
|
2776 let deviceStorage = null; |
|
2777 |
|
2778 if (navigator.getDeviceStorage) { |
|
2779 deviceStorage = navigator.getDeviceStorage("apps"); |
|
2780 } |
|
2781 |
|
2782 if (deviceStorage) { |
|
2783 let req = deviceStorage.freeSpace(); |
|
2784 req.onsuccess = req.onerror = e => { |
|
2785 let freeBytes = e.target.result; |
|
2786 let sufficientStorage = this._checkDownloadSize(freeBytes, aNewApp); |
|
2787 if (sufficientStorage) { |
|
2788 deferred.resolve(); |
|
2789 } else { |
|
2790 deferred.reject("INSUFFICIENT_STORAGE"); |
|
2791 } |
|
2792 } |
|
2793 } else { |
|
2794 debug("No deviceStorage"); |
|
2795 // deviceStorage isn't available, so use FileUtils to find the size of |
|
2796 // available storage. |
|
2797 let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true); |
|
2798 try { |
|
2799 let sufficientStorage = this._checkDownloadSize(dir.diskSpaceAvailable, |
|
2800 aNewApp); |
|
2801 if (sufficientStorage) { |
|
2802 deferred.resolve(); |
|
2803 } else { |
|
2804 deferred.reject("INSUFFICIENT_STORAGE"); |
|
2805 } |
|
2806 } catch(ex) { |
|
2807 // If disk space information isn't available, we'll end up here. |
|
2808 // We should proceed anyway, otherwise devices that support neither |
|
2809 // deviceStorage nor diskSpaceAvailable will never be able to install |
|
2810 // packaged apps. |
|
2811 deferred.resolve(); |
|
2812 } |
|
2813 } |
|
2814 |
|
2815 return deferred.promise; |
|
2816 }, |
|
2817 |
|
2818 _checkDownloadSize: function(aFreeBytes, aNewApp) { |
|
2819 if (aFreeBytes) { |
|
2820 debug("Free storage: " + aFreeBytes + ". Download size: " + |
|
2821 aNewApp.downloadSize); |
|
2822 if (aFreeBytes <= |
|
2823 aNewApp.downloadSize + AppDownloadManager.MIN_REMAINING_FREESPACE) { |
|
2824 return false; |
|
2825 } |
|
2826 } |
|
2827 return true; |
|
2828 }, |
|
2829 |
|
2830 _getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp, |
|
2831 aNewApp) { |
|
2832 let requestChannel; |
|
2833 |
|
2834 if (aIsLocalFileInstall) { |
|
2835 requestChannel = NetUtil.newChannel(aFullPackagePath) |
|
2836 .QueryInterface(Ci.nsIFileChannel); |
|
2837 } else { |
|
2838 requestChannel = NetUtil.newChannel(aFullPackagePath) |
|
2839 .QueryInterface(Ci.nsIHttpChannel); |
|
2840 requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
|
2841 } |
|
2842 |
|
2843 if (aOldApp.packageEtag && !aIsLocalFileInstall) { |
|
2844 debug("Add If-None-Match header: " + aOldApp.packageEtag); |
|
2845 requestChannel.setRequestHeader("If-None-Match", aOldApp.packageEtag, |
|
2846 false); |
|
2847 } |
|
2848 |
|
2849 let lastProgressTime = 0; |
|
2850 |
|
2851 requestChannel.notificationCallbacks = { |
|
2852 QueryInterface: function(aIID) { |
|
2853 if (aIID.equals(Ci.nsISupports) || |
|
2854 aIID.equals(Ci.nsIProgressEventSink) || |
|
2855 aIID.equals(Ci.nsILoadContext)) |
|
2856 return this; |
|
2857 throw Cr.NS_ERROR_NO_INTERFACE; |
|
2858 }, |
|
2859 getInterface: function(aIID) { |
|
2860 return this.QueryInterface(aIID); |
|
2861 }, |
|
2862 onProgress: (function(aRequest, aContext, aProgress, aProgressMax) { |
|
2863 aOldApp.progress = aProgress; |
|
2864 let now = Date.now(); |
|
2865 if (now - lastProgressTime > MIN_PROGRESS_EVENT_DELAY) { |
|
2866 debug("onProgress: " + aProgress + "/" + aProgressMax); |
|
2867 this._sendDownloadProgressEvent(aNewApp, aProgress); |
|
2868 lastProgressTime = now; |
|
2869 this._saveApps(); |
|
2870 } |
|
2871 }).bind(this), |
|
2872 onStatus: function(aRequest, aContext, aStatus, aStatusArg) { }, |
|
2873 |
|
2874 // nsILoadContext |
|
2875 appId: aOldApp.installerAppId, |
|
2876 isInBrowserElement: aOldApp.installerIsBrowser, |
|
2877 usePrivateBrowsing: false, |
|
2878 isContent: false, |
|
2879 associatedWindow: null, |
|
2880 topWindow : null, |
|
2881 isAppOfType: function(appType) { |
|
2882 throw Cr.NS_ERROR_NOT_IMPLEMENTED; |
|
2883 } |
|
2884 }; |
|
2885 |
|
2886 return requestChannel; |
|
2887 }, |
|
2888 |
|
2889 _sendDownloadProgressEvent: function(aNewApp, aProgress) { |
|
2890 this.broadcastMessage("Webapps:UpdateState", { |
|
2891 app: { |
|
2892 progress: aProgress |
|
2893 }, |
|
2894 manifestURL: aNewApp.manifestURL |
|
2895 }); |
|
2896 this.broadcastMessage("Webapps:FireEvent", { |
|
2897 eventType: "progress", |
|
2898 manifestURL: aNewApp.manifestURL |
|
2899 }); |
|
2900 }, |
|
2901 |
|
2902 _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) { |
|
2903 let deferred = Promise.defer(); |
|
2904 |
|
2905 // Staging the zip in TmpD until all the checks are done. |
|
2906 let zipFile = |
|
2907 FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true); |
|
2908 |
|
2909 // We need an output stream to write the channel content to the zip file. |
|
2910 let outputStream = Cc["@mozilla.org/network/file-output-stream;1"] |
|
2911 .createInstance(Ci.nsIFileOutputStream); |
|
2912 // write, create, truncate |
|
2913 outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0); |
|
2914 let bufferedOutputStream = |
|
2915 Cc['@mozilla.org/network/buffered-output-stream;1'] |
|
2916 .createInstance(Ci.nsIBufferedOutputStream); |
|
2917 bufferedOutputStream.init(outputStream, 1024); |
|
2918 |
|
2919 // Create a listener that will give data to the file output stream. |
|
2920 let listener = Cc["@mozilla.org/network/simple-stream-listener;1"] |
|
2921 .createInstance(Ci.nsISimpleStreamListener); |
|
2922 |
|
2923 listener.init(bufferedOutputStream, { |
|
2924 onStartRequest: function(aRequest, aContext) { |
|
2925 // Nothing to do there anymore. |
|
2926 }, |
|
2927 |
|
2928 onStopRequest: function(aRequest, aContext, aStatusCode) { |
|
2929 bufferedOutputStream.close(); |
|
2930 outputStream.close(); |
|
2931 |
|
2932 if (!Components.isSuccessCode(aStatusCode)) { |
|
2933 deferred.reject("NETWORK_ERROR"); |
|
2934 return; |
|
2935 } |
|
2936 |
|
2937 // If we get a 4XX or a 5XX http status, bail out like if we had a |
|
2938 // network error. |
|
2939 let responseStatus = aRequestChannel.responseStatus; |
|
2940 if (responseStatus >= 400 && responseStatus <= 599) { |
|
2941 // unrecoverable error, don't bug the user |
|
2942 aOldApp.downloadAvailable = false; |
|
2943 deferred.reject("NETWORK_ERROR"); |
|
2944 return; |
|
2945 } |
|
2946 |
|
2947 deferred.resolve(zipFile); |
|
2948 } |
|
2949 }); |
|
2950 aRequestChannel.asyncOpen(listener, null); |
|
2951 |
|
2952 // send a first progress event to correctly set the DOM object's properties |
|
2953 this._sendDownloadProgressEvent(aNewApp, 0); |
|
2954 |
|
2955 return deferred.promise; |
|
2956 }, |
|
2957 |
|
2958 /** |
|
2959 * Compute the MD5 hash of a file, doing async IO off the main thread. |
|
2960 * |
|
2961 * @param {String} aFilePath |
|
2962 * the path of the file to hash |
|
2963 * @returns {String} the MD5 hash of the file |
|
2964 */ |
|
2965 _computeFileHash: function(aFilePath) { |
|
2966 let deferred = Promise.defer(); |
|
2967 |
|
2968 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
|
2969 file.initWithPath(aFilePath); |
|
2970 |
|
2971 NetUtil.asyncFetch(file, function(inputStream, status) { |
|
2972 if (!Components.isSuccessCode(status)) { |
|
2973 debug("Error reading " + aFilePath + ": " + e); |
|
2974 deferred.reject(); |
|
2975 return; |
|
2976 } |
|
2977 |
|
2978 let hasher = Cc["@mozilla.org/security/hash;1"] |
|
2979 .createInstance(Ci.nsICryptoHash); |
|
2980 // We want to use the MD5 algorithm. |
|
2981 hasher.init(hasher.MD5); |
|
2982 |
|
2983 const PR_UINT32_MAX = 0xffffffff; |
|
2984 hasher.updateFromStream(inputStream, PR_UINT32_MAX); |
|
2985 |
|
2986 // Return the two-digit hexadecimal code for a byte. |
|
2987 function toHexString(charCode) { |
|
2988 return ("0" + charCode.toString(16)).slice(-2); |
|
2989 } |
|
2990 |
|
2991 // We're passing false to get the binary hash and not base64. |
|
2992 let data = hasher.finish(false); |
|
2993 // Convert the binary hash data to a hex string. |
|
2994 let hash = [toHexString(data.charCodeAt(i)) for (i in data)].join(""); |
|
2995 debug("File hash computed: " + hash); |
|
2996 |
|
2997 deferred.resolve(hash); |
|
2998 }); |
|
2999 |
|
3000 return deferred.promise; |
|
3001 }, |
|
3002 |
|
3003 /** |
|
3004 * Send an "applied" event right away for the package being installed. |
|
3005 * |
|
3006 * XXX We use this to exit the app update process early when the downloaded |
|
3007 * package is identical to the last one we installed. Presumably we do |
|
3008 * something similar after updating the app, and we could refactor both cases |
|
3009 * to use the same code to send the "applied" event. |
|
3010 * |
|
3011 * @param aNewApp {Object} the new app data |
|
3012 * @param aOldApp {Object} the currently stored app data |
|
3013 * @param aId {String} the unique id of the app |
|
3014 */ |
|
3015 _sendAppliedEvent: function(aNewApp, aOldApp, aId) { |
|
3016 aOldApp.downloading = false; |
|
3017 aOldApp.downloadAvailable = false; |
|
3018 aOldApp.downloadSize = 0; |
|
3019 aOldApp.installState = "installed"; |
|
3020 aOldApp.readyToApplyDownload = false; |
|
3021 if (aOldApp.staged && aOldApp.staged.manifestHash) { |
|
3022 // If we're here then the manifest has changed but the package |
|
3023 // hasn't. Let's clear this, so we don't keep offering |
|
3024 // a bogus update to the user |
|
3025 aOldApp.manifestHash = aOldApp.staged.manifestHash; |
|
3026 aOldApp.etag = aOldApp.staged.etag || aOldApp.etag; |
|
3027 aOldApp.staged = {}; |
|
3028 |
|
3029 // Move the staged update manifest to a non staged one. |
|
3030 try { |
|
3031 let staged = this._getAppDir(aId); |
|
3032 staged.append("staged-update.webapp"); |
|
3033 staged.moveTo(staged.parent, "update.webapp"); |
|
3034 } catch (ex) { |
|
3035 // We don't really mind much if this fails. |
|
3036 } |
|
3037 } |
|
3038 |
|
3039 // Save the updated registry, and cleanup the tmp directory. |
|
3040 this._saveApps().then(() => { |
|
3041 this.broadcastMessage("Webapps:UpdateState", { |
|
3042 app: aOldApp, |
|
3043 manifestURL: aNewApp.manifestURL |
|
3044 }); |
|
3045 this.broadcastMessage("Webapps:FireEvent", { |
|
3046 manifestURL: aNewApp.manifestURL, |
|
3047 eventType: ["downloadsuccess", "downloadapplied"] |
|
3048 }); |
|
3049 }); |
|
3050 let file = FileUtils.getFile("TmpD", ["webapps", aId], false); |
|
3051 if (file && file.exists()) { |
|
3052 file.remove(true); |
|
3053 } |
|
3054 }, |
|
3055 |
|
3056 _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall, |
|
3057 aIsUpdate, aManifest, aRequestChannel, aHash) { |
|
3058 return Task.spawn((function*() { |
|
3059 let zipReader, isSigned, newManifest; |
|
3060 |
|
3061 try { |
|
3062 [zipReader, isSigned] = yield this._openPackage(aZipFile, aOldApp, |
|
3063 aIsLocalFileInstall); |
|
3064 newManifest = yield this._readPackage(aOldApp, aNewApp, |
|
3065 aIsLocalFileInstall, aIsUpdate, aManifest, aRequestChannel, |
|
3066 aHash, zipReader, isSigned); |
|
3067 } catch (e) { |
|
3068 debug("package open/read error: " + e); |
|
3069 // Something bad happened when opening/reading the package. |
|
3070 // Unrecoverable error, don't bug the user. |
|
3071 // Apps with installState 'pending' does not produce any |
|
3072 // notification, so we are safe with its current |
|
3073 // downloadAvailable state. |
|
3074 if (aOldApp.installState !== "pending") { |
|
3075 aOldApp.downloadAvailable = false; |
|
3076 } |
|
3077 if (typeof e == 'object') { |
|
3078 Cu.reportError("Error while reading package:" + e); |
|
3079 throw "INVALID_PACKAGE"; |
|
3080 } else { |
|
3081 throw e; |
|
3082 } |
|
3083 } finally { |
|
3084 if (zipReader) { |
|
3085 zipReader.close(); |
|
3086 } |
|
3087 } |
|
3088 |
|
3089 return newManifest; |
|
3090 |
|
3091 }).bind(this)); |
|
3092 }, |
|
3093 |
|
3094 _openPackage: function(aZipFile, aApp, aIsLocalFileInstall) { |
|
3095 return Task.spawn((function*() { |
|
3096 let certDb; |
|
3097 try { |
|
3098 certDb = Cc["@mozilla.org/security/x509certdb;1"] |
|
3099 .getService(Ci.nsIX509CertDB); |
|
3100 } catch (e) { |
|
3101 debug("nsIX509CertDB error: " + e); |
|
3102 // unrecoverable error, don't bug the user |
|
3103 aApp.downloadAvailable = false; |
|
3104 throw "CERTDB_ERROR"; |
|
3105 } |
|
3106 |
|
3107 let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin, |
|
3108 aApp.manifestURL, |
|
3109 aZipFile, |
|
3110 certDb); |
|
3111 |
|
3112 // We cannot really know if the system date is correct or |
|
3113 // not. What we can know is if it's after the build date or not, |
|
3114 // and assume the build date is correct (which we cannot |
|
3115 // really know either). |
|
3116 let isLaterThanBuildTime = Date.now() > PLATFORM_BUILD_ID_TIME; |
|
3117 |
|
3118 let isSigned; |
|
3119 |
|
3120 if (Components.isSuccessCode(result)) { |
|
3121 isSigned = true; |
|
3122 } else if (result == Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY || |
|
3123 result == Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY || |
|
3124 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING) { |
|
3125 throw "APP_PACKAGE_CORRUPTED"; |
|
3126 } else if (result == Cr.NS_ERROR_FILE_CORRUPTED || |
|
3127 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE || |
|
3128 result == Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID || |
|
3129 result == Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID) { |
|
3130 throw "APP_PACKAGE_INVALID"; |
|
3131 } else if ((!aIsLocalFileInstall || isLaterThanBuildTime) && |
|
3132 (result != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED)) { |
|
3133 throw "INVALID_SIGNATURE"; |
|
3134 } else { |
|
3135 // If it's a localFileInstall and the validation failed |
|
3136 // because of a expired certificate, just assume it was valid |
|
3137 // and that the error occurred because the system time has not |
|
3138 // been set yet. |
|
3139 isSigned = (aIsLocalFileInstall && |
|
3140 (getNSPRErrorCode(result) == |
|
3141 SEC_ERROR_EXPIRED_CERTIFICATE)); |
|
3142 |
|
3143 zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] |
|
3144 .createInstance(Ci.nsIZipReader); |
|
3145 zipReader.open(aZipFile); |
|
3146 } |
|
3147 |
|
3148 return [zipReader, isSigned]; |
|
3149 |
|
3150 }).bind(this)); |
|
3151 }, |
|
3152 |
|
3153 _openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) { |
|
3154 let deferred = Promise.defer(); |
|
3155 |
|
3156 let root = TrustedRootCertificate.index; |
|
3157 |
|
3158 let useReviewerCerts = false; |
|
3159 try { |
|
3160 useReviewerCerts = Services.prefs. |
|
3161 getBoolPref("dom.mozApps.use_reviewer_certs"); |
|
3162 } catch (ex) { } |
|
3163 |
|
3164 // We'll use the reviewer and dev certificates only if the pref is set to |
|
3165 // true. |
|
3166 if (useReviewerCerts) { |
|
3167 let manifestPath = Services.io.newURI(aManifestURL, null, null).path; |
|
3168 |
|
3169 switch (aInstallOrigin) { |
|
3170 case "https://marketplace.firefox.com": |
|
3171 root = manifestPath.startsWith("/reviewers/") |
|
3172 ? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot |
|
3173 : Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot; |
|
3174 break; |
|
3175 |
|
3176 case "https://marketplace-dev.allizom.org": |
|
3177 root = manifestPath.startsWith("/reviewers/") |
|
3178 ? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot |
|
3179 : Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot; |
|
3180 break; |
|
3181 } |
|
3182 } |
|
3183 |
|
3184 aCertDb.openSignedAppFileAsync( |
|
3185 root, aZipFile, |
|
3186 function(aRv, aZipReader) { |
|
3187 deferred.resolve([aRv, aZipReader]); |
|
3188 } |
|
3189 ); |
|
3190 |
|
3191 return deferred.promise; |
|
3192 }, |
|
3193 |
|
3194 _readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate, |
|
3195 aManifest, aRequestChannel, aHash, aZipReader, |
|
3196 aIsSigned) { |
|
3197 this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall); |
|
3198 |
|
3199 if (!aZipReader.hasEntry("manifest.webapp")) { |
|
3200 throw "MISSING_MANIFEST"; |
|
3201 } |
|
3202 |
|
3203 let istream = aZipReader.getInputStream("manifest.webapp"); |
|
3204 |
|
3205 // Obtain a converter to read from a UTF-8 encoded input stream. |
|
3206 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
3207 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
3208 converter.charset = "UTF-8"; |
|
3209 |
|
3210 let newManifest = JSON.parse(converter.ConvertToUnicode( |
|
3211 NetUtil.readInputStreamToString(istream, istream.available()) || "")); |
|
3212 |
|
3213 if (!AppsUtils.checkManifest(newManifest, aOldApp)) { |
|
3214 throw "INVALID_MANIFEST"; |
|
3215 } |
|
3216 |
|
3217 // For app updates we don't forbid apps to rename themselves but |
|
3218 // we still retain the old name of the app. In the future we |
|
3219 // will use UI to allow updates to rename an app after we check |
|
3220 // with the user that the rename is ok. |
|
3221 if (aIsUpdate) { |
|
3222 // Call ensureSameAppName before compareManifests as `manifest` |
|
3223 // has been normalized to avoid app rename. |
|
3224 AppsUtils.ensureSameAppName(aManifest._manifest, newManifest, aOldApp); |
|
3225 } |
|
3226 |
|
3227 if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) { |
|
3228 throw "MANIFEST_MISMATCH"; |
|
3229 } |
|
3230 |
|
3231 if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) { |
|
3232 throw "INSTALL_FROM_DENIED"; |
|
3233 } |
|
3234 |
|
3235 // Local file installs can be privileged even without the signature. |
|
3236 let maxStatus = aIsSigned || aIsLocalFileInstall |
|
3237 ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED |
|
3238 : Ci.nsIPrincipal.APP_STATUS_INSTALLED; |
|
3239 |
|
3240 if (AppsUtils.getAppManifestStatus(newManifest) > maxStatus) { |
|
3241 throw "INVALID_SECURITY_LEVEL"; |
|
3242 } |
|
3243 |
|
3244 aOldApp.appStatus = AppsUtils.getAppManifestStatus(newManifest); |
|
3245 |
|
3246 this._saveEtag(aIsUpdate, aOldApp, aRequestChannel, aHash, newManifest); |
|
3247 this._checkOrigin(aIsSigned || aIsLocalFileInstall, aOldApp, newManifest, |
|
3248 aIsUpdate); |
|
3249 this._getIds(aIsSigned, aZipReader, converter, aNewApp, aOldApp, aIsUpdate); |
|
3250 |
|
3251 return newManifest; |
|
3252 }, |
|
3253 |
|
3254 _checkSignature: function(aApp, aIsSigned, aIsLocalFileInstall) { |
|
3255 // XXX Security: You CANNOT safely add a new app store for |
|
3256 // installing privileged apps just by modifying this pref and |
|
3257 // adding the signing cert for that store to the cert trust |
|
3258 // database. *Any* origin listed can install apps signed with |
|
3259 // *any* certificate trusted; we don't try to maintain a strong |
|
3260 // association between certificate with installOrign. The |
|
3261 // expectation here is that in production builds the pref will |
|
3262 // contain exactly one origin. However, in custom development |
|
3263 // builds it may contain more than one origin so we can test |
|
3264 // different stages (dev, staging, prod) of the same app store. |
|
3265 // |
|
3266 // Only allow signed apps to be installed from a whitelist of |
|
3267 // domains, and require all packages installed from any of the |
|
3268 // domains on the whitelist to be signed. This is a stopgap until |
|
3269 // we have a real story for handling multiple app stores signing |
|
3270 // apps. |
|
3271 let signedAppOriginsStr = |
|
3272 Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from"); |
|
3273 // If it's a local install and it's signed then we assume |
|
3274 // the app origin is a valid signer. |
|
3275 let isSignedAppOrigin = (aIsSigned && aIsLocalFileInstall) || |
|
3276 signedAppOriginsStr.split(","). |
|
3277 indexOf(aApp.installOrigin) > -1; |
|
3278 if (!aIsSigned && isSignedAppOrigin) { |
|
3279 // Packaged apps installed from these origins must be signed; |
|
3280 // if not, assume somebody stripped the signature. |
|
3281 throw "INVALID_SIGNATURE"; |
|
3282 } else if (aIsSigned && !isSignedAppOrigin) { |
|
3283 // Other origins are *prohibited* from installing signed apps. |
|
3284 // One reason is that our app revocation mechanism requires |
|
3285 // strong cooperation from the host of the mini-manifest, which |
|
3286 // we assume to be under the control of the install origin, |
|
3287 // even if it has a different origin. |
|
3288 throw "INSTALL_FROM_DENIED"; |
|
3289 } |
|
3290 }, |
|
3291 |
|
3292 _saveEtag: function(aIsUpdate, aOldApp, aRequestChannel, aHash, aManifest) { |
|
3293 // Save the new Etag for the package. |
|
3294 if (aIsUpdate) { |
|
3295 if (!aOldApp.staged) { |
|
3296 aOldApp.staged = { }; |
|
3297 } |
|
3298 try { |
|
3299 aOldApp.staged.packageEtag = aRequestChannel.getResponseHeader("Etag"); |
|
3300 } catch(e) { } |
|
3301 aOldApp.staged.packageHash = aHash; |
|
3302 aOldApp.staged.appStatus = AppsUtils.getAppManifestStatus(aManifest); |
|
3303 } else { |
|
3304 try { |
|
3305 aOldApp.packageEtag = aRequestChannel.getResponseHeader("Etag"); |
|
3306 } catch(e) { } |
|
3307 aOldApp.packageHash = aHash; |
|
3308 aOldApp.appStatus = AppsUtils.getAppManifestStatus(aManifest); |
|
3309 } |
|
3310 }, |
|
3311 |
|
3312 _checkOrigin: function(aIsSigned, aOldApp, aManifest, aIsUpdate) { |
|
3313 // Check if the app declares which origin it will use. |
|
3314 if (aIsSigned && |
|
3315 aOldApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED && |
|
3316 aManifest.origin !== undefined) { |
|
3317 let uri; |
|
3318 try { |
|
3319 uri = Services.io.newURI(aManifest.origin, null, null); |
|
3320 } catch(e) { |
|
3321 throw "INVALID_ORIGIN"; |
|
3322 } |
|
3323 if (uri.scheme != "app") { |
|
3324 throw "INVALID_ORIGIN"; |
|
3325 } |
|
3326 |
|
3327 if (aIsUpdate) { |
|
3328 // Changing the origin during an update is not allowed. |
|
3329 if (uri.prePath != aOldApp.origin) { |
|
3330 throw "INVALID_ORIGIN_CHANGE"; |
|
3331 } |
|
3332 // Nothing else to do for an update... since the |
|
3333 // origin can't change we don't need to move the |
|
3334 // app nor can we have a duplicated origin |
|
3335 } else { |
|
3336 debug("Setting origin to " + uri.prePath + |
|
3337 " for " + aOldApp.manifestURL); |
|
3338 let newId = uri.prePath.substring(6); // "app://".length |
|
3339 if (newId in this.webapps) { |
|
3340 throw "DUPLICATE_ORIGIN"; |
|
3341 } |
|
3342 aOldApp.origin = uri.prePath; |
|
3343 // Update the registry. |
|
3344 let oldId = aOldApp.id; |
|
3345 aOldApp.id = newId; |
|
3346 this.webapps[newId] = aOldApp; |
|
3347 delete this.webapps[oldId]; |
|
3348 // Rename the directories where the files are installed. |
|
3349 [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) { |
|
3350 let parent = FileUtils.getDir(aDir, ["webapps"], true, true); |
|
3351 let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true); |
|
3352 dir.moveTo(parent, newId); |
|
3353 }); |
|
3354 // Signals that we need to swap the old id with the new app. |
|
3355 this.broadcastMessage("Webapps:RemoveApp", { id: oldId }); |
|
3356 this.broadcastMessage("Webapps:AddApp", { id: newId, |
|
3357 app: aOldApp }); |
|
3358 } |
|
3359 } |
|
3360 }, |
|
3361 |
|
3362 _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp, |
|
3363 aIsUpdate) { |
|
3364 // Get ids.json if the file is signed |
|
3365 if (aIsSigned) { |
|
3366 let idsStream; |
|
3367 try { |
|
3368 idsStream = aZipReader.getInputStream("META-INF/ids.json"); |
|
3369 } catch (e) { |
|
3370 throw aZipReader.hasEntry("META-INF/ids.json") |
|
3371 ? e |
|
3372 : "MISSING_IDS_JSON"; |
|
3373 } |
|
3374 |
|
3375 let ids = JSON.parse(aConverter.ConvertToUnicode(NetUtil. |
|
3376 readInputStreamToString( idsStream, idsStream.available()) || "")); |
|
3377 if ((!ids.id) || !Number.isInteger(ids.version) || |
|
3378 (ids.version <= 0)) { |
|
3379 throw "INVALID_IDS_JSON"; |
|
3380 } |
|
3381 let storeId = aNewApp.installOrigin + "#" + ids.id; |
|
3382 this._checkForStoreIdMatch(aIsUpdate, aOldApp, storeId, ids.version); |
|
3383 aOldApp.storeId = storeId; |
|
3384 aOldApp.storeVersion = ids.version; |
|
3385 } |
|
3386 }, |
|
3387 |
|
3388 // aStoreId must be a string of the form |
|
3389 // <installOrigin>#<storeId from ids.json> |
|
3390 // aStoreVersion must be a positive integer. |
|
3391 _checkForStoreIdMatch: function(aIsUpdate, aNewApp, aStoreId, aStoreVersion) { |
|
3392 // Things to check: |
|
3393 // 1. if it's a update: |
|
3394 // a. We should already have this storeId, or the original storeId must |
|
3395 // start with STORE_ID_PENDING_PREFIX |
|
3396 // b. The manifestURL for the stored app should be the same one we're |
|
3397 // updating |
|
3398 // c. And finally the version of the update should be higher than the one |
|
3399 // on the already installed package |
|
3400 // 2. else |
|
3401 // a. We should not have this storeId on the list |
|
3402 // We're currently launching WRONG_APP_STORE_ID for all the mismatch kind of |
|
3403 // errors, and APP_STORE_VERSION_ROLLBACK for the version error. |
|
3404 |
|
3405 // Does an app with this storeID exist already? |
|
3406 let appId = this.getAppLocalIdByStoreId(aStoreId); |
|
3407 let isInstalled = appId != Ci.nsIScriptSecurityManager.NO_APP_ID; |
|
3408 if (aIsUpdate) { |
|
3409 let isDifferent = aNewApp.localId !== appId; |
|
3410 let isPending = aNewApp.storeId.indexOf(STORE_ID_PENDING_PREFIX) == 0; |
|
3411 |
|
3412 if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) { |
|
3413 throw "WRONG_APP_STORE_ID"; |
|
3414 } |
|
3415 |
|
3416 if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) { |
|
3417 throw "APP_STORE_VERSION_ROLLBACK"; |
|
3418 } |
|
3419 |
|
3420 } else if (isInstalled) { |
|
3421 throw "WRONG_APP_STORE_ID"; |
|
3422 } |
|
3423 }, |
|
3424 |
|
3425 // Removes the directory we created, and sends an error to the DOM side. |
|
3426 _revertDownloadPackage: function(aId, aOldApp, aNewApp, aIsUpdate, aError) { |
|
3427 debug("Cleanup: " + aError + "\n" + aError.stack); |
|
3428 let dir = FileUtils.getDir("TmpD", ["webapps", aId], true, true); |
|
3429 try { |
|
3430 dir.remove(true); |
|
3431 } catch (e) { } |
|
3432 |
|
3433 // We avoid notifying the error to the DOM side if the app download |
|
3434 // was cancelled via cancelDownload, which already sends its own |
|
3435 // notification. |
|
3436 if (aOldApp.isCanceling) { |
|
3437 delete aOldApp.isCanceling; |
|
3438 return; |
|
3439 } |
|
3440 |
|
3441 let download = AppDownloadManager.get(aNewApp.manifestURL); |
|
3442 aOldApp.downloading = false; |
|
3443 |
|
3444 // If there were not enough storage to download the package we |
|
3445 // won't have a record of the download details, so we just set the |
|
3446 // installState to 'pending' at first download and to 'installed' when |
|
3447 // updating. |
|
3448 aOldApp.installState = download ? download.previousState |
|
3449 : aIsUpdate ? "installed" |
|
3450 : "pending"; |
|
3451 |
|
3452 if (aOldApp.staged) { |
|
3453 delete aOldApp.staged; |
|
3454 } |
|
3455 |
|
3456 this._saveApps().then(() => { |
|
3457 this.broadcastMessage("Webapps:UpdateState", { |
|
3458 app: aOldApp, |
|
3459 error: aError, |
|
3460 manifestURL: aNewApp.manifestURL |
|
3461 }); |
|
3462 this.broadcastMessage("Webapps:FireEvent", { |
|
3463 eventType: "downloaderror", |
|
3464 manifestURL: aNewApp.manifestURL |
|
3465 }); |
|
3466 }); |
|
3467 AppDownloadManager.remove(aNewApp.manifestURL); |
|
3468 }, |
|
3469 |
|
3470 doUninstall: function(aData, aMm) { |
|
3471 this.uninstall(aData.manifestURL, |
|
3472 function onsuccess() { |
|
3473 aMm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData); |
|
3474 }, |
|
3475 function onfailure() { |
|
3476 // Fall-through, fails to uninstall the desired app because: |
|
3477 // - we cannot find the app to be uninstalled. |
|
3478 // - the app to be uninstalled is not removable. |
|
3479 aMm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData); |
|
3480 } |
|
3481 ); |
|
3482 }, |
|
3483 |
|
3484 uninstall: function(aManifestURL, aOnSuccess, aOnFailure) { |
|
3485 debug("uninstall " + aManifestURL); |
|
3486 |
|
3487 let app = this.getAppByManifestURL(aManifestURL); |
|
3488 if (!app) { |
|
3489 aOnFailure("NO_SUCH_APP"); |
|
3490 return; |
|
3491 } |
|
3492 let id = app.id; |
|
3493 |
|
3494 if (!app.removable) { |
|
3495 debug("Error: cannot uninstall a non-removable app."); |
|
3496 aOnFailure("NON_REMOVABLE_APP"); |
|
3497 return; |
|
3498 } |
|
3499 |
|
3500 // Check if we are downloading something for this app, and cancel the |
|
3501 // download if needed. |
|
3502 this.cancelDownload(app.manifestURL); |
|
3503 |
|
3504 // Clean up the deprecated manifest cache if needed. |
|
3505 if (id in this._manifestCache) { |
|
3506 delete this._manifestCache[id]; |
|
3507 } |
|
3508 |
|
3509 // Clear private data first. |
|
3510 this._clearPrivateData(app.localId, false); |
|
3511 |
|
3512 // Then notify observers. |
|
3513 // We have to clone the app object as nsIDOMApplication objects are |
|
3514 // stringified as an empty object. (see bug 830376) |
|
3515 let appClone = AppsUtils.cloneAppObject(app); |
|
3516 Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(appClone)); |
|
3517 |
|
3518 if (supportSystemMessages()) { |
|
3519 this._readManifests([{ id: id }]).then((aResult) => { |
|
3520 this._unregisterActivities(aResult[0].manifest, app); |
|
3521 }); |
|
3522 } |
|
3523 |
|
3524 let dir = this._getAppDir(id); |
|
3525 try { |
|
3526 dir.remove(true); |
|
3527 } catch (e) {} |
|
3528 |
|
3529 delete this.webapps[id]; |
|
3530 |
|
3531 this._saveApps().then(() => { |
|
3532 this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", appClone); |
|
3533 // Catch exception on callback call to ensure notifying observers after |
|
3534 try { |
|
3535 if (aOnSuccess) { |
|
3536 aOnSuccess(); |
|
3537 } |
|
3538 } catch(ex) { |
|
3539 Cu.reportError("DOMApplicationRegistry: Exception on app uninstall: " + |
|
3540 ex + "\n" + ex.stack); |
|
3541 } |
|
3542 this.broadcastMessage("Webapps:RemoveApp", { id: id }); |
|
3543 }); |
|
3544 }, |
|
3545 |
|
3546 getSelf: function(aData, aMm) { |
|
3547 aData.apps = []; |
|
3548 |
|
3549 if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || |
|
3550 aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { |
|
3551 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); |
|
3552 return; |
|
3553 } |
|
3554 |
|
3555 let tmp = []; |
|
3556 |
|
3557 for (let id in this.webapps) { |
|
3558 if (this.webapps[id].origin == aData.origin && |
|
3559 this.webapps[id].localId == aData.appId && |
|
3560 this._isLaunchable(this.webapps[id])) { |
|
3561 let app = AppsUtils.cloneAppObject(this.webapps[id]); |
|
3562 aData.apps.push(app); |
|
3563 tmp.push({ id: id }); |
|
3564 break; |
|
3565 } |
|
3566 } |
|
3567 |
|
3568 if (!aData.apps.length) { |
|
3569 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); |
|
3570 return; |
|
3571 } |
|
3572 |
|
3573 this._readManifests(tmp).then((aResult) => { |
|
3574 for (let i = 0; i < aResult.length; i++) |
|
3575 aData.apps[i].manifest = aResult[i].manifest; |
|
3576 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData); |
|
3577 }); |
|
3578 }, |
|
3579 |
|
3580 checkInstalled: function(aData, aMm) { |
|
3581 aData.app = null; |
|
3582 let tmp = []; |
|
3583 |
|
3584 for (let appId in this.webapps) { |
|
3585 if (this.webapps[appId].manifestURL == aData.manifestURL && |
|
3586 this._isLaunchable(this.webapps[appId])) { |
|
3587 aData.app = AppsUtils.cloneAppObject(this.webapps[appId]); |
|
3588 tmp.push({ id: appId }); |
|
3589 break; |
|
3590 } |
|
3591 } |
|
3592 |
|
3593 this._readManifests(tmp).then((aResult) => { |
|
3594 for (let i = 0; i < aResult.length; i++) { |
|
3595 aData.app.manifest = aResult[i].manifest; |
|
3596 break; |
|
3597 } |
|
3598 aMm.sendAsyncMessage("Webapps:CheckInstalled:Return:OK", aData); |
|
3599 }); |
|
3600 }, |
|
3601 |
|
3602 getInstalled: function(aData, aMm) { |
|
3603 aData.apps = []; |
|
3604 let tmp = []; |
|
3605 |
|
3606 for (let id in this.webapps) { |
|
3607 if (this.webapps[id].installOrigin == aData.origin && |
|
3608 this._isLaunchable(this.webapps[id])) { |
|
3609 aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id])); |
|
3610 tmp.push({ id: id }); |
|
3611 } |
|
3612 } |
|
3613 |
|
3614 this._readManifests(tmp).then((aResult) => { |
|
3615 for (let i = 0; i < aResult.length; i++) |
|
3616 aData.apps[i].manifest = aResult[i].manifest; |
|
3617 aMm.sendAsyncMessage("Webapps:GetInstalled:Return:OK", aData); |
|
3618 }); |
|
3619 }, |
|
3620 |
|
3621 getNotInstalled: function(aData, aMm) { |
|
3622 aData.apps = []; |
|
3623 let tmp = []; |
|
3624 |
|
3625 for (let id in this.webapps) { |
|
3626 if (!this._isLaunchable(this.webapps[id])) { |
|
3627 aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id])); |
|
3628 tmp.push({ id: id }); |
|
3629 } |
|
3630 } |
|
3631 |
|
3632 this._readManifests(tmp).then((aResult) => { |
|
3633 for (let i = 0; i < aResult.length; i++) |
|
3634 aData.apps[i].manifest = aResult[i].manifest; |
|
3635 aMm.sendAsyncMessage("Webapps:GetNotInstalled:Return:OK", aData); |
|
3636 }); |
|
3637 }, |
|
3638 |
|
3639 doGetAll: function(aData, aMm) { |
|
3640 this.getAll(function (apps) { |
|
3641 aData.apps = apps; |
|
3642 aMm.sendAsyncMessage("Webapps:GetAll:Return:OK", aData); |
|
3643 }); |
|
3644 }, |
|
3645 |
|
3646 getAll: function(aCallback) { |
|
3647 debug("getAll"); |
|
3648 let apps = []; |
|
3649 let tmp = []; |
|
3650 |
|
3651 for (let id in this.webapps) { |
|
3652 let app = AppsUtils.cloneAppObject(this.webapps[id]); |
|
3653 if (!this._isLaunchable(app)) |
|
3654 continue; |
|
3655 |
|
3656 apps.push(app); |
|
3657 tmp.push({ id: id }); |
|
3658 } |
|
3659 |
|
3660 this._readManifests(tmp).then((aResult) => { |
|
3661 for (let i = 0; i < aResult.length; i++) |
|
3662 apps[i].manifest = aResult[i].manifest; |
|
3663 aCallback(apps); |
|
3664 }); |
|
3665 }, |
|
3666 |
|
3667 /* Check if |data| is actually a receipt */ |
|
3668 isReceipt: function(data) { |
|
3669 try { |
|
3670 // The receipt data shouldn't be too big (allow up to 1 MiB of data) |
|
3671 const MAX_RECEIPT_SIZE = 1048576; |
|
3672 |
|
3673 if (data.length > MAX_RECEIPT_SIZE) { |
|
3674 return "RECEIPT_TOO_BIG"; |
|
3675 } |
|
3676 |
|
3677 // Marketplace receipts are JWK + "~" + JWT |
|
3678 // Other receipts may contain only the JWT |
|
3679 let receiptParts = data.split('~'); |
|
3680 let jwtData = null; |
|
3681 if (receiptParts.length == 2) { |
|
3682 jwtData = receiptParts[1]; |
|
3683 } else { |
|
3684 jwtData = receiptParts[0]; |
|
3685 } |
|
3686 |
|
3687 let segments = jwtData.split('.'); |
|
3688 if (segments.length != 3) { |
|
3689 return "INVALID_SEGMENTS_NUMBER"; |
|
3690 } |
|
3691 |
|
3692 // We need to translate the base64 alphabet used in JWT to our base64 alphabet |
|
3693 // before calling atob. |
|
3694 let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+') |
|
3695 .replace(/_/g, '/'))); |
|
3696 if (!decodedReceipt) { |
|
3697 return "INVALID_RECEIPT_ENCODING"; |
|
3698 } |
|
3699 |
|
3700 // Required values for a receipt |
|
3701 if (!decodedReceipt.typ) { |
|
3702 return "RECEIPT_TYPE_REQUIRED"; |
|
3703 } |
|
3704 if (!decodedReceipt.product) { |
|
3705 return "RECEIPT_PRODUCT_REQUIRED"; |
|
3706 } |
|
3707 if (!decodedReceipt.user) { |
|
3708 return "RECEIPT_USER_REQUIRED"; |
|
3709 } |
|
3710 if (!decodedReceipt.iss) { |
|
3711 return "RECEIPT_ISS_REQUIRED"; |
|
3712 } |
|
3713 if (!decodedReceipt.nbf) { |
|
3714 return "RECEIPT_NBF_REQUIRED"; |
|
3715 } |
|
3716 if (!decodedReceipt.iat) { |
|
3717 return "RECEIPT_IAT_REQUIRED"; |
|
3718 } |
|
3719 |
|
3720 let allowedTypes = [ "purchase-receipt", "developer-receipt", |
|
3721 "reviewer-receipt", "test-receipt" ]; |
|
3722 if (allowedTypes.indexOf(decodedReceipt.typ) < 0) { |
|
3723 return "RECEIPT_TYPE_UNSUPPORTED"; |
|
3724 } |
|
3725 } catch (e) { |
|
3726 return "RECEIPT_ERROR"; |
|
3727 } |
|
3728 |
|
3729 return null; |
|
3730 }, |
|
3731 |
|
3732 addReceipt: function(aData, aMm) { |
|
3733 debug("addReceipt " + aData.manifestURL); |
|
3734 |
|
3735 let receipt = aData.receipt; |
|
3736 |
|
3737 if (!receipt) { |
|
3738 aData.error = "INVALID_PARAMETERS"; |
|
3739 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); |
|
3740 return; |
|
3741 } |
|
3742 |
|
3743 let error = this.isReceipt(receipt); |
|
3744 if (error) { |
|
3745 aData.error = error; |
|
3746 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); |
|
3747 return; |
|
3748 } |
|
3749 |
|
3750 let id = this._appIdForManifestURL(aData.manifestURL); |
|
3751 let app = this.webapps[id]; |
|
3752 |
|
3753 if (!app.receipts) { |
|
3754 app.receipts = []; |
|
3755 } else if (app.receipts.length > 500) { |
|
3756 aData.error = "TOO_MANY_RECEIPTS"; |
|
3757 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); |
|
3758 return; |
|
3759 } |
|
3760 |
|
3761 let index = app.receipts.indexOf(receipt); |
|
3762 if (index >= 0) { |
|
3763 aData.error = "RECEIPT_ALREADY_EXISTS"; |
|
3764 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData); |
|
3765 return; |
|
3766 } |
|
3767 |
|
3768 app.receipts.push(receipt); |
|
3769 |
|
3770 this._saveApps().then(() => { |
|
3771 aData.receipts = app.receipts; |
|
3772 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData); |
|
3773 }); |
|
3774 }, |
|
3775 |
|
3776 removeReceipt: function(aData, aMm) { |
|
3777 debug("removeReceipt " + aData.manifestURL); |
|
3778 |
|
3779 let receipt = aData.receipt; |
|
3780 |
|
3781 if (!receipt) { |
|
3782 aData.error = "INVALID_PARAMETERS"; |
|
3783 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); |
|
3784 return; |
|
3785 } |
|
3786 |
|
3787 let id = this._appIdForManifestURL(aData.manifestURL); |
|
3788 let app = this.webapps[id]; |
|
3789 |
|
3790 if (!app.receipts) { |
|
3791 aData.error = "NO_SUCH_RECEIPT"; |
|
3792 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); |
|
3793 return; |
|
3794 } |
|
3795 |
|
3796 let index = app.receipts.indexOf(receipt); |
|
3797 if (index == -1) { |
|
3798 aData.error = "NO_SUCH_RECEIPT"; |
|
3799 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); |
|
3800 return; |
|
3801 } |
|
3802 |
|
3803 app.receipts.splice(index, 1); |
|
3804 |
|
3805 this._saveApps().then(() => { |
|
3806 aData.receipts = app.receipts; |
|
3807 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData); |
|
3808 }); |
|
3809 }, |
|
3810 |
|
3811 replaceReceipt: function(aData, aMm) { |
|
3812 debug("replaceReceipt " + aData.manifestURL); |
|
3813 |
|
3814 let oldReceipt = aData.oldReceipt; |
|
3815 let newReceipt = aData.newReceipt; |
|
3816 |
|
3817 if (!oldReceipt || !newReceipt) { |
|
3818 aData.error = "INVALID_PARAMETERS"; |
|
3819 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); |
|
3820 return; |
|
3821 } |
|
3822 |
|
3823 let error = this.isReceipt(newReceipt); |
|
3824 if (error) { |
|
3825 aData.error = error; |
|
3826 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); |
|
3827 return; |
|
3828 } |
|
3829 |
|
3830 let id = this._appIdForManifestURL(aData.manifestURL); |
|
3831 let app = this.webapps[id]; |
|
3832 |
|
3833 if (!app.receipts) { |
|
3834 aData.error = "NO_SUCH_RECEIPT"; |
|
3835 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData); |
|
3836 return; |
|
3837 } |
|
3838 |
|
3839 let oldIndex = app.receipts.indexOf(oldReceipt); |
|
3840 if (oldIndex == -1) { |
|
3841 aData.error = "NO_SUCH_RECEIPT"; |
|
3842 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData); |
|
3843 return; |
|
3844 } |
|
3845 |
|
3846 app.receipts[oldIndex] = newReceipt; |
|
3847 |
|
3848 this._saveApps().then(() => { |
|
3849 aData.receipts = app.receipts; |
|
3850 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData); |
|
3851 }); |
|
3852 }, |
|
3853 |
|
3854 getManifestFor: function(aManifestURL) { |
|
3855 let id = this._appIdForManifestURL(aManifestURL); |
|
3856 let app = this.webapps[id]; |
|
3857 if (!id || (app.installState == "pending" && !app.retryingDownload)) { |
|
3858 return Promise.resolve(null); |
|
3859 } |
|
3860 |
|
3861 return this._readManifests([{ id: id }]).then((aResult) => { |
|
3862 return aResult[0].manifest; |
|
3863 }); |
|
3864 }, |
|
3865 |
|
3866 getAppByManifestURL: function(aManifestURL) { |
|
3867 return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL); |
|
3868 }, |
|
3869 |
|
3870 getCSPByLocalId: function(aLocalId) { |
|
3871 debug("getCSPByLocalId:" + aLocalId); |
|
3872 return AppsUtils.getCSPByLocalId(this.webapps, aLocalId); |
|
3873 }, |
|
3874 |
|
3875 getAppLocalIdByStoreId: function(aStoreId) { |
|
3876 debug("getAppLocalIdByStoreId:" + aStoreId); |
|
3877 return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId); |
|
3878 }, |
|
3879 |
|
3880 getAppByLocalId: function(aLocalId) { |
|
3881 return AppsUtils.getAppByLocalId(this.webapps, aLocalId); |
|
3882 }, |
|
3883 |
|
3884 getManifestURLByLocalId: function(aLocalId) { |
|
3885 return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId); |
|
3886 }, |
|
3887 |
|
3888 getAppLocalIdByManifestURL: function(aManifestURL) { |
|
3889 return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL); |
|
3890 }, |
|
3891 |
|
3892 getCoreAppsBasePath: function() { |
|
3893 return AppsUtils.getCoreAppsBasePath(); |
|
3894 }, |
|
3895 |
|
3896 getWebAppsBasePath: function() { |
|
3897 return OS.Path.dirname(this.appsFile); |
|
3898 }, |
|
3899 |
|
3900 _isLaunchable: function(aApp) { |
|
3901 if (this.allAppsLaunchable) |
|
3902 return true; |
|
3903 |
|
3904 return WebappOSUtils.isLaunchable(aApp); |
|
3905 }, |
|
3906 |
|
3907 _notifyCategoryAndObservers: function(subject, topic, data, msg) { |
|
3908 const serviceMarker = "service,"; |
|
3909 |
|
3910 // First create observers from the category manager. |
|
3911 let cm = |
|
3912 Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); |
|
3913 let enumerator = cm.enumerateCategory(topic); |
|
3914 |
|
3915 let observers = []; |
|
3916 |
|
3917 while (enumerator.hasMoreElements()) { |
|
3918 let entry = |
|
3919 enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data; |
|
3920 let contractID = cm.getCategoryEntry(topic, entry); |
|
3921 |
|
3922 let factoryFunction; |
|
3923 if (contractID.substring(0, serviceMarker.length) == serviceMarker) { |
|
3924 contractID = contractID.substring(serviceMarker.length); |
|
3925 factoryFunction = "getService"; |
|
3926 } |
|
3927 else { |
|
3928 factoryFunction = "createInstance"; |
|
3929 } |
|
3930 |
|
3931 try { |
|
3932 let handler = Cc[contractID][factoryFunction](); |
|
3933 if (handler) { |
|
3934 let observer = handler.QueryInterface(Ci.nsIObserver); |
|
3935 observers.push(observer); |
|
3936 } |
|
3937 } catch(e) { } |
|
3938 } |
|
3939 |
|
3940 // Next enumerate the registered observers. |
|
3941 enumerator = Services.obs.enumerateObservers(topic); |
|
3942 while (enumerator.hasMoreElements()) { |
|
3943 try { |
|
3944 let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver); |
|
3945 if (observers.indexOf(observer) == -1) { |
|
3946 observers.push(observer); |
|
3947 } |
|
3948 } catch (e) { } |
|
3949 } |
|
3950 |
|
3951 observers.forEach(function (observer) { |
|
3952 try { |
|
3953 observer.observe(subject, topic, data); |
|
3954 } catch(e) { } |
|
3955 }); |
|
3956 // Send back an answer to the child. |
|
3957 if (msg) { |
|
3958 ppmm.broadcastAsyncMessage("Webapps:ClearBrowserData:Return", msg); |
|
3959 } |
|
3960 }, |
|
3961 |
|
3962 registerBrowserElementParentForApp: function(bep, appId) { |
|
3963 let mm = bep._mm; |
|
3964 |
|
3965 // Make a listener function that holds on to this appId. |
|
3966 let listener = this.receiveAppMessage.bind(this, appId); |
|
3967 |
|
3968 this.frameMessages.forEach(function(msgName) { |
|
3969 mm.addMessageListener(msgName, listener); |
|
3970 }); |
|
3971 }, |
|
3972 |
|
3973 receiveAppMessage: function(appId, message) { |
|
3974 switch (message.name) { |
|
3975 case "Webapps:ClearBrowserData": |
|
3976 this._clearPrivateData(appId, true, message.data); |
|
3977 break; |
|
3978 } |
|
3979 }, |
|
3980 |
|
3981 _clearPrivateData: function(appId, browserOnly, msg) { |
|
3982 let subject = { |
|
3983 appId: appId, |
|
3984 browserOnly: browserOnly, |
|
3985 QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams]) |
|
3986 }; |
|
3987 this._notifyCategoryAndObservers(subject, "webapps-clear-data", null, msg); |
|
3988 } |
|
3989 }; |
|
3990 |
|
3991 /** |
|
3992 * Appcache download observer |
|
3993 */ |
|
3994 let AppcacheObserver = function(aApp) { |
|
3995 debug("Creating AppcacheObserver for " + aApp.origin + |
|
3996 " - " + aApp.installState); |
|
3997 this.app = aApp; |
|
3998 this.startStatus = aApp.installState; |
|
3999 this.lastProgressTime = 0; |
|
4000 // Send a first progress event to correctly set the DOM object's properties. |
|
4001 this._sendProgressEvent(); |
|
4002 }; |
|
4003 |
|
4004 AppcacheObserver.prototype = { |
|
4005 // nsIOfflineCacheUpdateObserver implementation |
|
4006 _sendProgressEvent: function() { |
|
4007 let app = this.app; |
|
4008 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { |
|
4009 app: app, |
|
4010 manifestURL: app.manifestURL |
|
4011 }); |
|
4012 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { |
|
4013 eventType: "progress", |
|
4014 manifestURL: app.manifestURL |
|
4015 }); |
|
4016 }, |
|
4017 |
|
4018 updateStateChanged: function appObs_Update(aUpdate, aState) { |
|
4019 let mustSave = false; |
|
4020 let app = this.app; |
|
4021 |
|
4022 debug("Offline cache state change for " + app.origin + " : " + aState); |
|
4023 |
|
4024 var self = this; |
|
4025 let setStatus = function appObs_setStatus(aStatus, aProgress) { |
|
4026 debug("Offlinecache setStatus to " + aStatus + " with progress " + |
|
4027 aProgress + " for " + app.origin); |
|
4028 mustSave = (app.installState != aStatus); |
|
4029 |
|
4030 app.installState = aStatus; |
|
4031 app.progress = aProgress; |
|
4032 if (aStatus != "installed") { |
|
4033 self._sendProgressEvent(); |
|
4034 return; |
|
4035 } |
|
4036 |
|
4037 app.updateTime = Date.now(); |
|
4038 app.downloading = false; |
|
4039 app.downloadAvailable = false; |
|
4040 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { |
|
4041 app: app, |
|
4042 manifestURL: app.manifestURL |
|
4043 }); |
|
4044 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { |
|
4045 eventType: ["downloadsuccess", "downloadapplied"], |
|
4046 manifestURL: app.manifestURL |
|
4047 }); |
|
4048 } |
|
4049 |
|
4050 let setError = function appObs_setError(aError) { |
|
4051 debug("Offlinecache setError to " + aError); |
|
4052 app.downloading = false; |
|
4053 DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", { |
|
4054 app: app, |
|
4055 manifestURL: app.manifestURL |
|
4056 }); |
|
4057 DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", { |
|
4058 error: aError, |
|
4059 eventType: "downloaderror", |
|
4060 manifestURL: app.manifestURL |
|
4061 }); |
|
4062 mustSave = true; |
|
4063 } |
|
4064 |
|
4065 switch (aState) { |
|
4066 case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR: |
|
4067 aUpdate.removeObserver(this); |
|
4068 AppDownloadManager.remove(app.manifestURL); |
|
4069 setError("APP_CACHE_DOWNLOAD_ERROR"); |
|
4070 break; |
|
4071 case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE: |
|
4072 case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED: |
|
4073 aUpdate.removeObserver(this); |
|
4074 AppDownloadManager.remove(app.manifestURL); |
|
4075 setStatus("installed", aUpdate.byteProgress); |
|
4076 break; |
|
4077 case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING: |
|
4078 setStatus(this.startStatus, aUpdate.byteProgress); |
|
4079 break; |
|
4080 case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED: |
|
4081 case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS: |
|
4082 let now = Date.now(); |
|
4083 if (now - this.lastProgressTime > MIN_PROGRESS_EVENT_DELAY) { |
|
4084 setStatus(this.startStatus, aUpdate.byteProgress); |
|
4085 this.lastProgressTime = now; |
|
4086 } |
|
4087 break; |
|
4088 } |
|
4089 |
|
4090 // Status changed, update the stored version. |
|
4091 if (mustSave) { |
|
4092 DOMApplicationRegistry._saveApps(); |
|
4093 } |
|
4094 }, |
|
4095 |
|
4096 applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) { |
|
4097 // Nothing to do. |
|
4098 } |
|
4099 }; |
|
4100 |
|
4101 DOMApplicationRegistry.init(); |