Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 "use strict";
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10 const Cr = Components.results;
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);
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 }
32 const PLATFORM_BUILD_ID_TIME = buildIDToTime();
34 this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
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");
46 XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate",
47 "resource://gre/modules/StoreTrustAnchor.jsm");
49 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
50 "resource://gre/modules/PermissionsInstaller.jsm");
52 XPCOMUtils.defineLazyModuleGetter(this, "OfflineCacheInstaller",
53 "resource://gre/modules/OfflineCacheInstaller.jsm");
55 XPCOMUtils.defineLazyModuleGetter(this, "SystemMessagePermissionsChecker",
56 "resource://gre/modules/SystemMessagePermissionsChecker.jsm");
58 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
59 "resource://gre/modules/WebappOSUtils.jsm");
61 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
62 "resource://gre/modules/NetUtil.jsm");
64 XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
65 "resource://gre/modules/ScriptPreloader.jsm");
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
74 function debug(aMsg) {
75 #ifdef DEBUG
76 dump("-*- Webapps.jsm : " + aMsg + "\n");
77 #endif
78 }
80 function getNSPRErrorCode(err) {
81 return -1 * ((err) & 0xffff);
82 }
84 function supportUseCurrentProfile() {
85 return Services.prefs.getBoolPref("dom.webapps.useCurrentProfile");
86 }
88 function supportSystemMessages() {
89 return Services.prefs.getBoolPref("dom.sysmsg.enabled");
90 }
92 // Minimum delay between two progress events while downloading, in ms.
93 const MIN_PROGRESS_EVENT_DELAY = 1500;
95 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
97 const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser";
99 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
100 "@mozilla.org/parentprocessmessagemanager;1",
101 "nsIMessageBroadcaster");
103 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
104 "@mozilla.org/childprocessmessagemanager;1",
105 "nsIMessageSender");
107 XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() {
108 return Cc["@mozilla.org/inter-app-communication-service;1"]
109 .getService(Ci.nsIInterAppCommService);
110 });
112 XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService",
113 "@mozilla.org/datastore-service;1",
114 "nsIDataStoreService");
116 XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
117 return Cc["@mozilla.org/system-message-internal;1"]
118 .getService(Ci.nsISystemMessagesInternal);
119 });
121 XPCOMUtils.defineLazyGetter(this, "updateSvc", function() {
122 return Cc["@mozilla.org/offlinecacheupdate-service;1"]
123 .getService(Ci.nsIOfflineCacheUpdateService);
124 });
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
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#";
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: [ ],
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"];
166 this.frameMessages = ["Webapps:ClearBrowserData"];
168 this.messages.forEach((function(msgName) {
169 ppmm.addMessageListener(msgName, this);
170 }).bind(this));
172 cpmm.addMessageListener("Activities:Register:OK", this);
174 Services.obs.addObserver(this, "xpcom-shutdown", false);
175 Services.obs.addObserver(this, "memory-pressure", false);
177 AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
179 this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
180 ["webapps", "webapps.json"], true).path;
182 this.loadAndUpdateApps();
183 },
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 }
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 }
201 app.id = id;
203 // Make sure we have a localId
204 if (app.localId === undefined) {
205 app.localId = this._nextLocalId();
206 }
208 if (app.basePath === undefined) {
209 app.basePath = appDir;
210 }
212 // Default to removable apps.
213 if (app.removable === undefined) {
214 app.removable = true;
215 }
217 // Default to a non privileged status.
218 if (app.appStatus === undefined) {
219 app.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
220 }
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 }
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 }
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 }
245 // Default role to "".
246 if (this.webapps[id].role === undefined) {
247 this.webapps[id].role = "";
248 }
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 },
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 },
265 get registryStarted() {
266 return this._registryStarted.promise;
267 },
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 },
277 get registryReady() {
278 return this._registryReady.promise;
279 },
281 // Ensure that the .to property in redirects is a relative URL.
282 sanitizeRedirects: function sanitizeRedirects(aSource) {
283 if (!aSource) {
284 return null;
285 }
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 },
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 });
329 // Nothing else to do but notifying we're ready.
330 this.notifyAppsRegistryReady();
331 }
332 },
334 updateDataStoreForApp: function(aId) {
335 if (!this.webapps[aId]) {
336 return;
337 }
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 },
347 updatePermissionsForApp: function(aId) {
348 if (!this.webapps[aId]) {
349 return;
350 }
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 },
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 },
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 }
401 let filesToMove;
402 let isPackage;
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 }
415 isPackage = false;
416 filesToMove = ["manifest.webapp"];
417 } else {
418 isPackage = true;
419 filesToMove = ["application.zip", "update.webapp"];
420 }
422 debug("Installing 3rd party app : " + aId +
423 " from " + baseDir.path);
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);
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 });
438 app.installState = "installed";
439 app.cachePath = app.basePath;
440 app.basePath = OS.Path.dirname(this.appsFile);
442 if (!isPackage) {
443 return;
444 }
446 app.origin = "app://" + aId;
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;
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 },
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 }
487 let httpsManifestURL =
488 "https://" + app.manifestURL.substring("http://".length);
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 },
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) { }
517 if (!file || !file.exists()) {
518 return;
519 }
521 // a
522 let data = yield AppsUtils.loadJSONAsync(file.path);
523 if (!data) {
524 return;
525 }
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 }
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;
550 this.webapps[id].id = id;
552 // Create a new localId.
553 this.webapps[id].localId = this._nextLocalId();
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 },
570 loadAndUpdateApps: function() {
571 return Task.spawn(function() {
572 let runUpdate = AppsUtils.isFirstRun(Services.prefs);
574 yield this.loadCurrentRegistry();
576 if (runUpdate) {
577 #ifdef MOZ_WIDGET_GONK
578 yield this.installSystemApps();
579 #endif
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 }
596 // DataStores must be initialized at startup.
597 for (let id in this.webapps) {
598 this.updateDataStoreForApp(id);
599 }
601 this.registerAppsHandlers(runUpdate);
602 }.bind(this)).then(null, Cu.reportError);
603 },
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 }
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;
620 dataStoreService.installDataStore(aId, name, aOrigin, aManifestURL,
621 readonly);
622 }
623 }
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;
631 dataStoreService.installAccessDataStore(aId, name, aOrigin,
632 aManifestURL, readonly);
633 }
634 }
635 },
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 }
647 if (!root.messages || !Array.isArray(root.messages) ||
648 root.messages.length == 0) {
649 return;
650 }
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 }
673 if (SystemMessagePermissionsChecker
674 .isSystemMessagePermittedToRegister(messageName,
675 aApp.origin,
676 aManifest)) {
677 msgmgr.registerPage(messageName, href, manifestURL);
678 }
679 });
680 },
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 }
693 let connections = root.connections;
694 if (!connections) {
695 return;
696 }
698 if ((typeof connections) !== "object") {
699 debug("|connections| is not an object. Skipping: " + connections);
700 return;
701 }
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);
708 for (let keyword in connections) {
709 let connection = connections[keyword];
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;
728 if (SystemMessagePermissionsChecker
729 .isSystemMessagePermittedToRegister("connection",
730 aApp.origin,
731 aManifest)) {
732 msgmgr.registerPage("connection", handlerPageURI, manifestURI);
733 }
735 interAppCommService.
736 registerConnection(keyword,
737 handlerPageURI,
738 manifestURI,
739 connection.description,
740 connection.rules);
741 }
742 },
744 _registerSystemMessages: function(aManifest, aApp) {
745 this._registerSystemMessagesForEntryPoint(aManifest, aApp, null);
747 if (!aManifest.entry_points) {
748 return;
749 }
751 for (let entryPoint in aManifest.entry_points) {
752 this._registerSystemMessagesForEntryPoint(aManifest, aApp, entryPoint);
753 }
754 },
756 _registerInterAppConnections: function(aManifest, aApp) {
757 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp, null);
759 if (!aManifest.entry_points) {
760 return;
761 }
763 for (let entryPoint in aManifest.entry_points) {
764 this._registerInterAppConnectionsForEntryPoint(aManifest, aApp,
765 entryPoint);
766 }
767 },
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 }
778 if (!root.activities) {
779 return activitiesToRegister;
780 }
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 }
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 }
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;
806 debug('_createActivitiesToRegister: ' + aApp.manifestURL + ', activity ' +
807 activity + ', description.href is ' + newDesc.href);
809 if (aRunUpdate) {
810 activitiesToRegister.push({ "manifest": aApp.manifestURL,
811 "name": activity,
812 "icon": manifest.iconURLForSize(128),
813 "description": newDesc });
814 }
816 let launchPath = Services.io.newURI(href, null, null);
817 let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
819 if (SystemMessagePermissionsChecker
820 .isSystemMessagePermittedToRegister("activity",
821 aApp.origin,
822 aManifest)) {
823 msgmgr.registerPage("activity", launchPath, manifestURL);
824 }
825 }
826 return activitiesToRegister;
827 },
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));
840 if (!manifest.entry_points) {
841 return;
842 }
844 for (let entryPoint in manifest.entry_points) {
845 activitiesToRegister.push.apply(activitiesToRegister,
846 this._createActivitiesToRegister(manifest, app, entryPoint, aRunUpdate));
847 }
848 }, this);
850 if (!aRunUpdate || activitiesToRegister.length == 0) {
851 this.notifyAppsRegistryReady();
852 return;
853 }
855 // Send the array carrying all the activities to be registered.
856 cpmm.sendAsyncMessage("Activities:Register", activitiesToRegister);
857 },
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 },
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 }
874 if (!root.activities) {
875 return activitiesToUnregister;
876 }
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 },
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));
898 if (!manifest.entry_points) {
899 return;
900 }
902 for (let entryPoint in manifest.entry_points) {
903 activitiesToUnregister.push.apply(activitiesToUnregister,
904 this._createActivitiesToUnregister(manifest, app, entryPoint));
905 }
906 }, this);
908 // Send the array carrying all the activities to be unregistered.
909 cpmm.sendAsyncMessage("Activities:Unregister", activitiesToUnregister);
910 },
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 },
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 },
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 },
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 }
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 });
973 if (!mmFound) {
974 this.children[aMsgName].push({
975 mm: aMm,
976 refCount: 1
977 });
978 }
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 },
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];
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 }
1015 if (msg.length === 0) {
1016 delete this.children[msgName];
1017 }
1018 }
1019 return;
1020 }
1022 aMsgNames.forEach(function(aMsgName) {
1023 if (!(aMsgName in this.children)) {
1024 return;
1025 }
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 });
1039 if (removeIndex) {
1040 this.children[aMsgName].splice(removeIndex, 1);
1041 }
1042 }, this);
1043 },
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);
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 }
1063 let msg = aMessage.data || {};
1064 let mm = aMessage.target;
1065 msg.mm = mm;
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 },
1147 getAppInfo: function getAppInfo(aAppId) {
1148 return AppsUtils.getAppInfo(this.webapps, aAppId);
1149 },
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 },
1169 registerUpdateHandler: function(aHandler) {
1170 this._updateHandlers.push(aHandler);
1171 },
1173 unregisterUpdateHandler: function(aHandler) {
1174 let index = this._updateHandlers.indexOf(aHandler);
1175 if (index != -1) {
1176 this._updateHandlers.splice(index, 1);
1177 }
1178 },
1180 notifyUpdateHandlers: function(aApp, aManifest, aZipPath) {
1181 for (let updateHandler of this._updateHandlers) {
1182 updateHandler(aApp, aManifest, aZipPath);
1183 }
1184 },
1186 _getAppDir: function(aId) {
1187 return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
1188 },
1190 _writeFile: function(aPath, aData) {
1191 debug("Saving " + aPath);
1193 let deferred = Promise.defer();
1195 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
1196 file.initWithPath(aPath);
1198 // Initialize the file output stream
1199 let ostream = FileUtils.openSafeFileOutputStream(file);
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";
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 });
1216 return deferred.promise;
1217 },
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 },
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 }
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 }
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 },
1256 close: function close(aApp) {
1257 debug("close");
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 },
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 }
1274 let app = this.webapps[download.appId];
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 }
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 },
1308 startDownload: Task.async(function*(aManifestURL) {
1309 debug("startDownload for " + aManifestURL);
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 }
1318 if (app.downloading) {
1319 debug("app is already downloading. Ignoring.");
1320 return;
1321 }
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 }
1337 // First of all, we check if the download is supposed to update an
1338 // already installed application.
1339 let isUpdate = (app.installState == "installed");
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;
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);
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 }]);
1359 let jsonManifest = results[0].manifest;
1360 let manifest = new ManifestHelper(jsonManifest, app.origin);
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;
1371 yield this._saveApps();
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 }
1384 return;
1385 }
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 }
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);
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));
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();
1416 yield this._saveApps();
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 }),
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 }
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");
1449 let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
1450 appFile.moveTo(dir, "application.zip");
1451 manFile.moveTo(dir, "manifest.webapp");
1453 // Move the staged update manifest to a non staged one.
1454 let staged = dir.clone();
1455 staged.append("staged-update.webapp");
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 }
1463 try {
1464 tmpDir.remove(true);
1465 } catch(e) { }
1467 // Clean up the deprecated manifest cache if needed.
1468 if (id in this._manifestCache) {
1469 delete this._manifestCache[id];
1470 }
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);
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;
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 }
1494 delete app.retryingDownload;
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);
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 });
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 },
1531 startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
1532 if (!aManifest.appcache_path) {
1533 return;
1534 }
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);
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 }
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);
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);
1574 cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
1576 });
1577 },
1579 // Returns the MD5 hash of the manifest.
1580 computeManifestHash: function(aManifest) {
1581 return AppsUtils.computeHash(JSON.stringify(aManifest));
1582 },
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 }
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 },
1606 checkForUpdate: function(aData, aMm) {
1607 debug("checkForUpdate for " + aData.manifestURL);
1609 function sendError(aError) {
1610 aData.error = aError;
1611 aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
1612 }
1614 let id = this._appIdForManifestURL(aData.manifestURL);
1615 let app = this.webapps[id];
1617 // We cannot update an app that does not exists.
1618 if (!app) {
1619 sendError("NO_SUCH_APP");
1620 return;
1621 }
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 }
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 }
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 }
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;
1649 #ifdef MOZ_WIDGET_GONK
1650 let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
1651 onlyCheckAppCache = (app.basePath == appDir.path);
1652 #endif
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 }
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 }
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 }
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://");
1712 if (xhr.status == 200) {
1713 let manifest = xhr.response;
1714 if (manifest == null) {
1715 sendError("MANIFEST_PARSE_ERROR");
1716 return;
1717 }
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);
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 }
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 }
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);
1817 xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
1818 xhr.addEventListener("error", (function() {
1819 sendError("NETWORK_ERROR");
1820 }).bind(this), false);
1822 debug("Checking manifest at " + aData.manifestURL);
1823 xhr.send(null);
1824 }
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) { }
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 },
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,
1856 isAppOfType: function(appType) {
1857 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
1858 },
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 },
1871 updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
1872 debug("updatePackagedApp");
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));
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();
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 }),
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);
1907 // Clean up the deprecated manifest cache if needed.
1908 if (aId in this._manifestCache) {
1909 delete this._manifestCache[aId];
1910 }
1912 aApp.manifest = aNewManifest || aOldManifest;
1914 let manifest;
1915 if (aNewManifest) {
1916 this.updateAppHandlers(aOldManifest, aNewManifest, aApp);
1918 this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest);
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));
1925 manifest = new ManifestHelper(aNewManifest, aApp.origin);
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 }
1936 this.updateDataStore(this.webapps[aId].localId, aApp.origin,
1937 aApp.manifestURL, aApp.manifest, aApp.appStatus);
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 }
1947 // Update the registry.
1948 this.webapps[aId] = aApp;
1949 yield this._saveApps();
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());
1968 let updateDeferred = Promise.defer();
1970 updateSvc.checkForUpdate(Services.io.newURI(manifest.fullAppcachePath(), null, null),
1971 aApp.localId, false,
1972 (aSubject, aTopic, aData) => updateDeferred.resolve(aTopic));
1974 let topic = yield updateDeferred.promise;
1976 debug("updateHostedApp: updateSvc.checkForUpdate return for " +
1977 aApp.manifestURL + " - event is " + topic);
1979 let eventType =
1980 topic == "offline-cache-update-available" ? "downloadavailable"
1981 : "downloadapplied";
1983 aApp.downloadAvailable = (eventType == "downloadavailable");
1984 yield this._saveApps();
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 }
1998 delete aApp.manifest;
1999 }),
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;
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);
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 }
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 }
2030 let checkManifest = (function() {
2031 if (!app.manifest) {
2032 sendError("MANIFEST_PARSE_ERROR");
2033 return false;
2034 }
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 }
2048 if (!AppsUtils.checkManifest(app.manifest, app)) {
2049 sendError("INVALID_MANIFEST");
2050 return false;
2051 }
2053 if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) {
2054 sendError("INSTALL_FROM_DENIED");
2055 return false;
2056 }
2058 if (!checkAppStatus(app.manifest)) {
2059 sendError("INVALID_SECURITY_LEVEL");
2060 return false;
2061 }
2063 return true;
2064 }).bind(this);
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);
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 }
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";
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 }
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);
2115 xhr.addEventListener("error", (function() {
2116 sendError("NETWORK_ERROR");
2117 }).bind(this), false);
2119 xhr.send(null);
2120 },
2122 doInstallPackage: function doInstallPackage(aData, aMm) {
2123 let app = aData.app;
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);
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 }
2142 let checkUpdateManifest = (function() {
2143 let manifest = app.updateManifest;
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 }
2152 if (!(AppsUtils.checkManifest(manifest, app) && manifest.package_path)) {
2153 sendError("INVALID_MANIFEST");
2154 return false;
2155 }
2157 if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
2158 sendError("INSTALL_FROM_DENIED");
2159 return false;
2160 }
2162 return true;
2163 }).bind(this);
2165 let installApp = (function() {
2166 app.manifestHash = this.computeManifestHash(app.updateManifest);
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);
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 }
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";
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 }
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);
2221 xhr.addEventListener("error", (function() {
2222 sendError("NETWORK_ERROR");
2223 }).bind(this), false);
2225 xhr.send(null);
2226 },
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 },
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: {},
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 }
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];
2270 return;
2271 }
2273 let packageDownload = this.queuedPackageDownload[aManifestURL];
2274 if (packageDownload) {
2275 let manifest = packageDownload.manifest;
2276 let newApp = packageDownload.app;
2277 let installSuccessCallback = packageDownload.callback;
2279 delete this.queuedPackageDownload[aManifestURL];
2281 this.downloadPackage(manifest, newApp, false).then(
2282 this._onDownloadPackage.bind(this, newApp, installSuccessCallback)
2283 );
2284 }
2285 },
2287 _setupApp: function(aData, aId) {
2288 let app = aData.app;
2290 // app can be uninstalled
2291 app.removable = true;
2293 if (aData.isPackage) {
2294 // Override the origin with the correct id.
2295 app.origin = "app://" + aId;
2296 }
2298 app.id = aId;
2299 app.installTime = Date.now();
2300 app.lastUpdateCheck = Date.now();
2302 return app;
2303 },
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;
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 }
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;
2337 return appObject;
2338 },
2340 _writeManifestFile: function(aId, aIsPackage, aJsonManifest) {
2341 debug("_writeManifestFile");
2343 // For packaged apps, keep the update manifest distinct from the app manifest.
2344 let manifestName = aIsPackage ? "update.webapp" : "manifest.webapp";
2346 let dir = this._getAppDir(aId).path;
2347 let manFile = OS.Path.join(dir, manifestName);
2348 this._writeFile(manFile, JSON.stringify(aJsonManifest));
2349 },
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 }
2358 let app = AppsUtils.cloneAppObject(aApp);
2360 if (!AppsUtils.checkManifest(aManifest, app) ||
2361 (aUpdateManifest && !AppsUtils.checkManifest(aUpdateManifest, app))) {
2362 return;
2363 }
2365 app.name = aManifest.name;
2367 app.csp = aManifest.csp || "";
2369 app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
2371 app.removable = true;
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 }
2381 app.localId = this._nextLocalId();
2383 app.basePath = OS.Path.dirname(this.appsFile);
2385 app.progress = 0.0;
2386 app.installState = "installed";
2387 app.downloadAvailable = false;
2388 app.downloading = false;
2389 app.readyToApplyDownload = false;
2391 if (aUpdateManifest && aUpdateManifest.size) {
2392 app.downloadSize = aUpdateManifest.size;
2393 }
2395 app.manifestHash = AppsUtils.computeHash(JSON.stringify(aUpdateManifest ||
2396 aManifest));
2398 let zipFile = WebappOSUtils.getPackagePath(app);
2399 app.packageHash = yield this._computeFileHash(zipFile);
2401 app.role = aManifest.role || "";
2403 app.redirects = this.sanitizeRedirects(aManifest.redirects);
2405 this.webapps[app.id] = app;
2407 // Store the manifest in the manifest cache, so we don't need to re-read it
2408 this._manifestCache[app.id] = app.manifest;
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 }
2416 this._saveApps().then(() => {
2417 this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app });
2418 });
2419 }),
2421 confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) {
2422 debug("confirmInstall");
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);
2429 let isReinstall = false;
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 }
2443 let app = this._setupApp(aData, id);
2445 let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
2446 this._writeManifestFile(id, aData.isPackage, jsonManifest);
2448 debug("app.origin: " + app.origin);
2449 let manifest = new ManifestHelper(jsonManifest, app.origin);
2451 let appObject = this._cloneApp(aData, app, manifest, id, localId);
2453 this.webapps[id] = appObject;
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 }
2470 this.updateDataStore(this.webapps[id].localId, this.webapps[id].origin,
2471 this.webapps[id].manifestURL, jsonManifest,
2472 this.webapps[id].appStatus);
2473 }
2475 for each (let prop in ["installState", "downloadAvailable", "downloading",
2476 "downloadSize", "readyToApplyDownload"]) {
2477 aData.app[prop] = appObject[prop];
2478 }
2480 if (manifest.appcache_path) {
2481 this.queuedDownload[app.manifestURL] = {
2482 manifest: manifest,
2483 app: appObject,
2484 profileDir: aProfileDir
2485 }
2486 }
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 });
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
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);
2533 this.queuedPackageDownload[app.manifestURL] = {
2534 manifest: manifest,
2535 app: appObject,
2536 callback: aInstallSuccessCallback
2537 };
2538 }
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 },
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) { }
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;
2583 yield this._saveApps();
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 }
2591 this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp });
2592 Services.obs.notifyObservers(null, "webapps-installed",
2593 JSON.stringify({ manifestURL: aNewApp.manifestURL }));
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 }
2604 this.updateDataStore(this.webapps[aId].localId, aNewApp.origin,
2605 aNewApp.manifestURL, aManifest, aNewApp.appStatus);
2607 this.broadcastMessage("Webapps:UpdateState", {
2608 app: app,
2609 manifest: aManifest,
2610 manifestURL: aNewApp.manifestURL
2611 });
2613 // Check if we have asm.js code to preload for this application.
2614 yield ScriptPreloader.preload(aNewApp, aManifest);
2616 this.broadcastMessage("Webapps:FireEvent", {
2617 eventType: ["downloadsuccess", "downloadapplied"],
2618 manifestURL: aNewApp.manifestURL
2619 });
2621 if (aInstallSuccessCallback) {
2622 aInstallSuccessCallback(aManifest, zipFile.path);
2623 }
2624 }),
2626 _nextLocalId: function() {
2627 let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
2629 while (this.getManifestURLByLocalId(id)) {
2630 id++;
2631 }
2633 Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
2634 Services.prefs.savePrefFile(null);
2635 return id;
2636 },
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 },
2646 makeAppId: function() {
2647 let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
2648 return uuidGenerator.generateUUID().toString();
2649 },
2651 _saveApps: function() {
2652 return this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2));
2653 },
2655 /**
2656 * Asynchronously reads a list of manifests
2657 */
2659 _manifestCache: {},
2661 _readManifests: function(aData) {
2662 return Task.spawn(function*() {
2663 for (let elem of aData) {
2664 let id = elem.id;
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;
2671 let dir = FileUtils.getDir(baseDir, ["webapps", id], false, true);
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 }
2682 elem.manifest = this._manifestCache[id];
2683 }
2685 return aData;
2686 }.bind(this)).then(null, Cu.reportError);
2687 },
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.
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];
2703 return Task.spawn((function*() {
2704 yield this._ensureSufficientStorage(aNewApp);
2706 let fullPackagePath = aManifest.fullPackagePath();
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';
2717 debug("About to download " + fullPackagePath);
2719 let requestChannel = this._getRequestChannel(fullPackagePath,
2720 isLocalFileInstall,
2721 oldApp,
2722 aNewApp);
2724 AppDownloadManager.add(
2725 aNewApp.manifestURL,
2726 {
2727 channel: requestChannel,
2728 appId: id,
2729 previousState: aIsUpdate ? "installed" : "pending"
2730 }
2731 );
2733 // We set the 'downloading' flag to true right before starting the fetch.
2734 oldApp.downloading = true;
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";
2741 // initialize the progress to 0 right now
2742 oldApp.progress = 0;
2744 let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp);
2745 let hash = yield this._computeFileHash(zipFile.path);
2747 let responseStatus = requestChannel.responseStatus;
2748 let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash);
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 }
2758 let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp,
2759 isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash);
2761 AppDownloadManager.remove(aNewApp.manifestURL);
2763 return [oldApp.id, newManifest];
2765 }).bind(this)).then(
2766 aOnSuccess,
2767 this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate)
2768 );
2769 },
2771 _ensureSufficientStorage: function(aNewApp) {
2772 let deferred = Promise.defer();
2774 let navigator = Services.wm.getMostRecentWindow(chromeWindowType)
2775 .navigator;
2776 let deviceStorage = null;
2778 if (navigator.getDeviceStorage) {
2779 deviceStorage = navigator.getDeviceStorage("apps");
2780 }
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 }
2815 return deferred.promise;
2816 },
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 },
2830 _getRequestChannel: function(aFullPackagePath, aIsLocalFileInstall, aOldApp,
2831 aNewApp) {
2832 let requestChannel;
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 }
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 }
2849 let lastProgressTime = 0;
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) { },
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 };
2886 return requestChannel;
2887 },
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 },
2902 _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
2903 let deferred = Promise.defer();
2905 // Staging the zip in TmpD until all the checks are done.
2906 let zipFile =
2907 FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
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);
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);
2923 listener.init(bufferedOutputStream, {
2924 onStartRequest: function(aRequest, aContext) {
2925 // Nothing to do there anymore.
2926 },
2928 onStopRequest: function(aRequest, aContext, aStatusCode) {
2929 bufferedOutputStream.close();
2930 outputStream.close();
2932 if (!Components.isSuccessCode(aStatusCode)) {
2933 deferred.reject("NETWORK_ERROR");
2934 return;
2935 }
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 }
2947 deferred.resolve(zipFile);
2948 }
2949 });
2950 aRequestChannel.asyncOpen(listener, null);
2952 // send a first progress event to correctly set the DOM object's properties
2953 this._sendDownloadProgressEvent(aNewApp, 0);
2955 return deferred.promise;
2956 },
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();
2968 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
2969 file.initWithPath(aFilePath);
2971 NetUtil.asyncFetch(file, function(inputStream, status) {
2972 if (!Components.isSuccessCode(status)) {
2973 debug("Error reading " + aFilePath + ": " + e);
2974 deferred.reject();
2975 return;
2976 }
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);
2983 const PR_UINT32_MAX = 0xffffffff;
2984 hasher.updateFromStream(inputStream, PR_UINT32_MAX);
2986 // Return the two-digit hexadecimal code for a byte.
2987 function toHexString(charCode) {
2988 return ("0" + charCode.toString(16)).slice(-2);
2989 }
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);
2997 deferred.resolve(hash);
2998 });
3000 return deferred.promise;
3001 },
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 = {};
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 }
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 },
3056 _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall,
3057 aIsUpdate, aManifest, aRequestChannel, aHash) {
3058 return Task.spawn((function*() {
3059 let zipReader, isSigned, newManifest;
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 }
3089 return newManifest;
3091 }).bind(this));
3092 },
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 }
3107 let [result, zipReader] = yield this._openSignedPackage(aApp.installOrigin,
3108 aApp.manifestURL,
3109 aZipFile,
3110 certDb);
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;
3118 let isSigned;
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));
3143 zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
3144 .createInstance(Ci.nsIZipReader);
3145 zipReader.open(aZipFile);
3146 }
3148 return [zipReader, isSigned];
3150 }).bind(this));
3151 },
3153 _openSignedPackage: function(aInstallOrigin, aManifestURL, aZipFile, aCertDb) {
3154 let deferred = Promise.defer();
3156 let root = TrustedRootCertificate.index;
3158 let useReviewerCerts = false;
3159 try {
3160 useReviewerCerts = Services.prefs.
3161 getBoolPref("dom.mozApps.use_reviewer_certs");
3162 } catch (ex) { }
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;
3169 switch (aInstallOrigin) {
3170 case "https://marketplace.firefox.com":
3171 root = manifestPath.startsWith("/reviewers/")
3172 ? Ci.nsIX509CertDB.AppMarketplaceProdReviewersRoot
3173 : Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot;
3174 break;
3176 case "https://marketplace-dev.allizom.org":
3177 root = manifestPath.startsWith("/reviewers/")
3178 ? Ci.nsIX509CertDB.AppMarketplaceDevReviewersRoot
3179 : Ci.nsIX509CertDB.AppMarketplaceDevPublicRoot;
3180 break;
3181 }
3182 }
3184 aCertDb.openSignedAppFileAsync(
3185 root, aZipFile,
3186 function(aRv, aZipReader) {
3187 deferred.resolve([aRv, aZipReader]);
3188 }
3189 );
3191 return deferred.promise;
3192 },
3194 _readPackage: function(aOldApp, aNewApp, aIsLocalFileInstall, aIsUpdate,
3195 aManifest, aRequestChannel, aHash, aZipReader,
3196 aIsSigned) {
3197 this._checkSignature(aNewApp, aIsSigned, aIsLocalFileInstall);
3199 if (!aZipReader.hasEntry("manifest.webapp")) {
3200 throw "MISSING_MANIFEST";
3201 }
3203 let istream = aZipReader.getInputStream("manifest.webapp");
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";
3210 let newManifest = JSON.parse(converter.ConvertToUnicode(
3211 NetUtil.readInputStreamToString(istream, istream.available()) || ""));
3213 if (!AppsUtils.checkManifest(newManifest, aOldApp)) {
3214 throw "INVALID_MANIFEST";
3215 }
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 }
3227 if (!AppsUtils.compareManifests(newManifest, aManifest._manifest)) {
3228 throw "MANIFEST_MISMATCH";
3229 }
3231 if (!AppsUtils.checkInstallAllowed(newManifest, aNewApp.installOrigin)) {
3232 throw "INSTALL_FROM_DENIED";
3233 }
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;
3240 if (AppsUtils.getAppManifestStatus(newManifest) > maxStatus) {
3241 throw "INVALID_SECURITY_LEVEL";
3242 }
3244 aOldApp.appStatus = AppsUtils.getAppManifestStatus(newManifest);
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);
3251 return newManifest;
3252 },
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 },
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 },
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 }
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 },
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 }
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 },
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.
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;
3412 if ((!isInstalled && !isPending) || (isInstalled && isDifferent)) {
3413 throw "WRONG_APP_STORE_ID";
3414 }
3416 if (!isPending && (aNewApp.storeVersion >= aStoreVersion)) {
3417 throw "APP_STORE_VERSION_ROLLBACK";
3418 }
3420 } else if (isInstalled) {
3421 throw "WRONG_APP_STORE_ID";
3422 }
3423 },
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) { }
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 }
3441 let download = AppDownloadManager.get(aNewApp.manifestURL);
3442 aOldApp.downloading = false;
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";
3452 if (aOldApp.staged) {
3453 delete aOldApp.staged;
3454 }
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 },
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 },
3484 uninstall: function(aManifestURL, aOnSuccess, aOnFailure) {
3485 debug("uninstall " + aManifestURL);
3487 let app = this.getAppByManifestURL(aManifestURL);
3488 if (!app) {
3489 aOnFailure("NO_SUCH_APP");
3490 return;
3491 }
3492 let id = app.id;
3494 if (!app.removable) {
3495 debug("Error: cannot uninstall a non-removable app.");
3496 aOnFailure("NON_REMOVABLE_APP");
3497 return;
3498 }
3500 // Check if we are downloading something for this app, and cancel the
3501 // download if needed.
3502 this.cancelDownload(app.manifestURL);
3504 // Clean up the deprecated manifest cache if needed.
3505 if (id in this._manifestCache) {
3506 delete this._manifestCache[id];
3507 }
3509 // Clear private data first.
3510 this._clearPrivateData(app.localId, false);
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));
3518 if (supportSystemMessages()) {
3519 this._readManifests([{ id: id }]).then((aResult) => {
3520 this._unregisterActivities(aResult[0].manifest, app);
3521 });
3522 }
3524 let dir = this._getAppDir(id);
3525 try {
3526 dir.remove(true);
3527 } catch (e) {}
3529 delete this.webapps[id];
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 },
3546 getSelf: function(aData, aMm) {
3547 aData.apps = [];
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 }
3555 let tmp = [];
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 }
3568 if (!aData.apps.length) {
3569 aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
3570 return;
3571 }
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 },
3580 checkInstalled: function(aData, aMm) {
3581 aData.app = null;
3582 let tmp = [];
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 }
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 },
3602 getInstalled: function(aData, aMm) {
3603 aData.apps = [];
3604 let tmp = [];
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 }
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 },
3621 getNotInstalled: function(aData, aMm) {
3622 aData.apps = [];
3623 let tmp = [];
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 }
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 },
3639 doGetAll: function(aData, aMm) {
3640 this.getAll(function (apps) {
3641 aData.apps = apps;
3642 aMm.sendAsyncMessage("Webapps:GetAll:Return:OK", aData);
3643 });
3644 },
3646 getAll: function(aCallback) {
3647 debug("getAll");
3648 let apps = [];
3649 let tmp = [];
3651 for (let id in this.webapps) {
3652 let app = AppsUtils.cloneAppObject(this.webapps[id]);
3653 if (!this._isLaunchable(app))
3654 continue;
3656 apps.push(app);
3657 tmp.push({ id: id });
3658 }
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 },
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;
3673 if (data.length > MAX_RECEIPT_SIZE) {
3674 return "RECEIPT_TOO_BIG";
3675 }
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 }
3687 let segments = jwtData.split('.');
3688 if (segments.length != 3) {
3689 return "INVALID_SEGMENTS_NUMBER";
3690 }
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 }
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 }
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 }
3729 return null;
3730 },
3732 addReceipt: function(aData, aMm) {
3733 debug("addReceipt " + aData.manifestURL);
3735 let receipt = aData.receipt;
3737 if (!receipt) {
3738 aData.error = "INVALID_PARAMETERS";
3739 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
3740 return;
3741 }
3743 let error = this.isReceipt(receipt);
3744 if (error) {
3745 aData.error = error;
3746 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:KO", aData);
3747 return;
3748 }
3750 let id = this._appIdForManifestURL(aData.manifestURL);
3751 let app = this.webapps[id];
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 }
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 }
3768 app.receipts.push(receipt);
3770 this._saveApps().then(() => {
3771 aData.receipts = app.receipts;
3772 aMm.sendAsyncMessage("Webapps:AddReceipt:Return:OK", aData);
3773 });
3774 },
3776 removeReceipt: function(aData, aMm) {
3777 debug("removeReceipt " + aData.manifestURL);
3779 let receipt = aData.receipt;
3781 if (!receipt) {
3782 aData.error = "INVALID_PARAMETERS";
3783 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
3784 return;
3785 }
3787 let id = this._appIdForManifestURL(aData.manifestURL);
3788 let app = this.webapps[id];
3790 if (!app.receipts) {
3791 aData.error = "NO_SUCH_RECEIPT";
3792 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
3793 return;
3794 }
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 }
3803 app.receipts.splice(index, 1);
3805 this._saveApps().then(() => {
3806 aData.receipts = app.receipts;
3807 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:OK", aData);
3808 });
3809 },
3811 replaceReceipt: function(aData, aMm) {
3812 debug("replaceReceipt " + aData.manifestURL);
3814 let oldReceipt = aData.oldReceipt;
3815 let newReceipt = aData.newReceipt;
3817 if (!oldReceipt || !newReceipt) {
3818 aData.error = "INVALID_PARAMETERS";
3819 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
3820 return;
3821 }
3823 let error = this.isReceipt(newReceipt);
3824 if (error) {
3825 aData.error = error;
3826 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:KO", aData);
3827 return;
3828 }
3830 let id = this._appIdForManifestURL(aData.manifestURL);
3831 let app = this.webapps[id];
3833 if (!app.receipts) {
3834 aData.error = "NO_SUCH_RECEIPT";
3835 aMm.sendAsyncMessage("Webapps:RemoveReceipt:Return:KO", aData);
3836 return;
3837 }
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 }
3846 app.receipts[oldIndex] = newReceipt;
3848 this._saveApps().then(() => {
3849 aData.receipts = app.receipts;
3850 aMm.sendAsyncMessage("Webapps:ReplaceReceipt:Return:OK", aData);
3851 });
3852 },
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 }
3861 return this._readManifests([{ id: id }]).then((aResult) => {
3862 return aResult[0].manifest;
3863 });
3864 },
3866 getAppByManifestURL: function(aManifestURL) {
3867 return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
3868 },
3870 getCSPByLocalId: function(aLocalId) {
3871 debug("getCSPByLocalId:" + aLocalId);
3872 return AppsUtils.getCSPByLocalId(this.webapps, aLocalId);
3873 },
3875 getAppLocalIdByStoreId: function(aStoreId) {
3876 debug("getAppLocalIdByStoreId:" + aStoreId);
3877 return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
3878 },
3880 getAppByLocalId: function(aLocalId) {
3881 return AppsUtils.getAppByLocalId(this.webapps, aLocalId);
3882 },
3884 getManifestURLByLocalId: function(aLocalId) {
3885 return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
3886 },
3888 getAppLocalIdByManifestURL: function(aManifestURL) {
3889 return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
3890 },
3892 getCoreAppsBasePath: function() {
3893 return AppsUtils.getCoreAppsBasePath();
3894 },
3896 getWebAppsBasePath: function() {
3897 return OS.Path.dirname(this.appsFile);
3898 },
3900 _isLaunchable: function(aApp) {
3901 if (this.allAppsLaunchable)
3902 return true;
3904 return WebappOSUtils.isLaunchable(aApp);
3905 },
3907 _notifyCategoryAndObservers: function(subject, topic, data, msg) {
3908 const serviceMarker = "service,";
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);
3915 let observers = [];
3917 while (enumerator.hasMoreElements()) {
3918 let entry =
3919 enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
3920 let contractID = cm.getCategoryEntry(topic, entry);
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 }
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 }
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 }
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 },
3962 registerBrowserElementParentForApp: function(bep, appId) {
3963 let mm = bep._mm;
3965 // Make a listener function that holds on to this appId.
3966 let listener = this.receiveAppMessage.bind(this, appId);
3968 this.frameMessages.forEach(function(msgName) {
3969 mm.addMessageListener(msgName, listener);
3970 });
3971 },
3973 receiveAppMessage: function(appId, message) {
3974 switch (message.name) {
3975 case "Webapps:ClearBrowserData":
3976 this._clearPrivateData(appId, true, message.data);
3977 break;
3978 }
3979 },
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 };
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 };
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 },
4018 updateStateChanged: function appObs_Update(aUpdate, aState) {
4019 let mustSave = false;
4020 let app = this.app;
4022 debug("Offline cache state change for " + app.origin + " : " + aState);
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);
4030 app.installState = aStatus;
4031 app.progress = aProgress;
4032 if (aStatus != "installed") {
4033 self._sendProgressEvent();
4034 return;
4035 }
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 }
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 }
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 }
4090 // Status changed, update the stored version.
4091 if (mustSave) {
4092 DOMApplicationRegistry._saveApps();
4093 }
4094 },
4096 applicationCacheAvailable: function appObs_CacheAvail(aApplicationCache) {
4097 // Nothing to do.
4098 }
4099 };
4101 DOMApplicationRegistry.init();