Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / AppsUtils.jsm
blobbe732faa6b43ccdd447407eb4d4aed0fd306a928
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 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14 Cu.import("resource://gre/modules/Promise.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
17   "resource://gre/modules/FileUtils.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
20   "resource://gre/modules/WebappOSUtils.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
23   "resource://gre/modules/NetUtil.jsm");
25 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
26                                    "@mozilla.org/AppsService;1",
27                                    "nsIAppsService");
29 // Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm,
30 // Webapps.jsm and Webapps.js
32 this.EXPORTED_SYMBOLS =
33   ["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
35 function debug(s) {
36   //dump("-*- AppsUtils.jsm: " + s + "\n");
39 this.isAbsoluteURI = function(aURI) {
40   let foo = Services.io.newURI("http://foo", null, null);
41   let bar = Services.io.newURI("http://bar", null, null);
42   return Services.io.newURI(aURI, null, foo).prePath != foo.prePath ||
43          Services.io.newURI(aURI, null, bar).prePath != bar.prePath;
46 this.mozIApplication = function(aApp) {
47   _setAppProperties(this, aApp);
50 mozIApplication.prototype = {
51   hasPermission: function(aPermission) {
52     let uri = Services.io.newURI(this.origin, null, null);
53     let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
54                    .getService(Ci.nsIScriptSecurityManager);
55     // This helper checks an URI inside |aApp|'s origin and part of |aApp| has a
56     // specific permission. It is not checking if browsers inside |aApp| have such
57     // permission.
58     let principal = secMan.getAppCodebasePrincipal(uri, this.localId,
59                                                    /*mozbrowser*/false);
60     let perm = Services.perms.testExactPermissionFromPrincipal(principal,
61                                                                aPermission);
62     return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
63   },
65   hasWidgetPage: function(aPageURL) {
66     return this.widgetPages.indexOf(aPageURL) != -1;
67   },
69   QueryInterface: function(aIID) {
70     if (aIID.equals(Ci.mozIApplication) ||
71         aIID.equals(Ci.nsISupports))
72       return this;
73     throw Cr.NS_ERROR_NO_INTERFACE;
74   }
77 function _setAppProperties(aObj, aApp) {
78   aObj.name = aApp.name;
79   aObj.csp = aApp.csp;
80   aObj.installOrigin = aApp.installOrigin;
81   aObj.origin = aApp.origin;
82 #ifdef MOZ_WIDGET_ANDROID
83   aObj.apkPackageName = aApp.apkPackageName;
84 #endif
85   aObj.receipts = aApp.receipts ? JSON.parse(JSON.stringify(aApp.receipts)) : null;
86   aObj.installTime = aApp.installTime;
87   aObj.manifestURL = aApp.manifestURL;
88   aObj.appStatus = aApp.appStatus;
89   aObj.removable = aApp.removable;
90   aObj.id = aApp.id;
91   aObj.localId = aApp.localId;
92   aObj.basePath = aApp.basePath;
93   aObj.progress = aApp.progress || 0.0;
94   aObj.installState = aApp.installState || "installed";
95   aObj.downloadAvailable = aApp.downloadAvailable;
96   aObj.downloading = aApp.downloading;
97   aObj.readyToApplyDownload = aApp.readyToApplyDownload;
98   aObj.downloadSize = aApp.downloadSize || 0;
99   aObj.lastUpdateCheck = aApp.lastUpdateCheck;
100   aObj.updateTime = aApp.updateTime;
101   aObj.etag = aApp.etag;
102   aObj.packageEtag = aApp.packageEtag;
103   aObj.manifestHash = aApp.manifestHash;
104   aObj.packageHash = aApp.packageHash;
105   aObj.staged = aApp.staged;
106   aObj.installerAppId = aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID;
107   aObj.installerIsBrowser = !!aApp.installerIsBrowser;
108   aObj.storeId = aApp.storeId || "";
109   aObj.storeVersion = aApp.storeVersion || 0;
110   aObj.role = aApp.role || "";
111   aObj.redirects = aApp.redirects;
112   aObj.widgetPages = aApp.widgetPages || [];
113   aObj.kind = aApp.kind;
114   aObj.enabled = aApp.enabled !== undefined ? aApp.enabled : true;
115   aObj.sideloaded = aApp.sideloaded;
118 this.AppsUtils = {
119   // Clones a app, without the manifest.
120   cloneAppObject: function(aApp) {
121     let obj = {};
122     _setAppProperties(obj, aApp);
123     return obj;
124   },
126   // Creates a nsILoadContext object with a given appId and isBrowser flag.
127   createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
128     return {
129        associatedWindow: null,
130        topWindow : null,
131        appId: aAppId,
132        isInBrowserElement: aIsBrowser,
133        usePrivateBrowsing: false,
134        isContent: false,
136        isAppOfType: function(appType) {
137          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
138        },
140        QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
141                                               Ci.nsIInterfaceRequestor,
142                                               Ci.nsISupports]),
143        getInterface: function(iid) {
144          if (iid.equals(Ci.nsILoadContext))
145            return this;
146          throw Cr.NS_ERROR_NO_INTERFACE;
147        }
148      }
149   },
151   // Sends data downloaded from aRequestChannel to a file
152   // identified by aId and aFileName.
153   getFile: function(aRequestChannel, aId, aFileName) {
154     let deferred = Promise.defer();
156     // Staging the file in TmpD until all the checks are done.
157     let file = FileUtils.getFile("TmpD", ["webapps", aId, aFileName], true);
159     // We need an output stream to write the channel content to the out file.
160     let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
161                          .createInstance(Ci.nsIFileOutputStream);
162     // write, create, truncate
163     outputStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
164     let bufferedOutputStream =
165       Cc['@mozilla.org/network/buffered-output-stream;1']
166         .createInstance(Ci.nsIBufferedOutputStream);
167     bufferedOutputStream.init(outputStream, 1024);
169     // Create a listener that will give data to the file output stream.
170     let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
171                      .createInstance(Ci.nsISimpleStreamListener);
173     listener.init(bufferedOutputStream, {
174       onStartRequest: function(aRequest, aContext) {
175         // Nothing to do there anymore.
176       },
178       onStopRequest: function(aRequest, aContext, aStatusCode) {
179         bufferedOutputStream.close();
180         outputStream.close();
182         if (!Components.isSuccessCode(aStatusCode)) {
183           deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: true});
184           return;
185         }
187         // If we get a 4XX or a 5XX http status, bail out like if we had a
188         // network error.
189         let responseStatus = aRequestChannel.responseStatus;
190         if (responseStatus >= 400 && responseStatus <= 599) {
191           // unrecoverable error, don't bug the user
192           deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: false});
193           return;
194         }
196         deferred.resolve(file);
197       }
198     });
199     aRequestChannel.asyncOpen(listener, null);
201     return deferred.promise;
202   },
204   getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) {
205     debug("getAppByManifestURL " + aManifestURL);
206     // This could be O(1) if |webapps| was a dictionary indexed on manifestURL
207     // which should be the unique app identifier.
208     // It's currently O(n).
209     for (let id in aApps) {
210       let app = aApps[id];
211       if (app.manifestURL == aManifestURL) {
212         return new mozIApplication(app);
213       }
214     }
216     return null;
217   },
219   getManifestFor: function getManifestFor(aManifestURL) {
220     debug("getManifestFor(" + aManifestURL + ")");
221     return DOMApplicationRegistry.getManifestFor(aManifestURL);
222   },
224   getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aApps, aManifestURL) {
225     debug("getAppLocalIdByManifestURL " + aManifestURL);
226     for (let id in aApps) {
227       if (aApps[id].manifestURL == aManifestURL) {
228         return aApps[id].localId;
229       }
230     }
232     return Ci.nsIScriptSecurityManager.NO_APP_ID;
233   },
235   getAppLocalIdByStoreId: function(aApps, aStoreId) {
236     debug("getAppLocalIdByStoreId:" + aStoreId);
237     for (let id in aApps) {
238       if (aApps[id].storeId == aStoreId) {
239         return aApps[id].localId;
240       }
241     }
243     return Ci.nsIScriptSecurityManager.NO_APP_ID;
244   },
246   getManifestCSPByLocalId: function getManifestCSPByLocalId(aApps, aLocalId) {
247     debug("getManifestCSPByLocalId " + aLocalId);
248     for (let id in aApps) {
249       let app = aApps[id];
250       if (app.localId == aLocalId) {
251         return ( app.csp || "" );
252       }
253     }
255     return "";
256   },
258   getDefaultCSPByLocalId: function(aApps, aLocalId) {
259     debug("getDefaultCSPByLocalId " + aLocalId);
260     for (let id in aApps) {
261       let app = aApps[id];
262       if (app.localId == aLocalId) {
263         // Use the app kind and the app status to choose the right default CSP.
264         try {
265           switch (app.appStatus) {
266             case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
267               return Services.prefs.getCharPref("security.apps.certified.CSP.default");
268               break;
269             case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
270               return Services.prefs.getCharPref("security.apps.privileged.CSP.default");
271               break;
272             case Ci.nsIPrincipal.APP_STATUS_INSTALLED:
273               return app.kind == "hosted-trusted"
274                 ? Services.prefs.getCharPref("security.apps.trusted.CSP.default")
275                 : "";
276               break;
277           }
278         } catch(e) {}
279       }
280     }
282     return "default-src 'self'; object-src 'none'";
283   },
285   getAppByLocalId: function getAppByLocalId(aApps, aLocalId) {
286     debug("getAppByLocalId " + aLocalId);
287     for (let id in aApps) {
288       let app = aApps[id];
289       if (app.localId == aLocalId) {
290         return new mozIApplication(app);
291       }
292     }
294     return null;
295   },
297   getManifestURLByLocalId: function getManifestURLByLocalId(aApps, aLocalId) {
298     debug("getManifestURLByLocalId " + aLocalId);
299     for (let id in aApps) {
300       let app = aApps[id];
301       if (app.localId == aLocalId) {
302         return app.manifestURL;
303       }
304     }
306     return "";
307   },
309   getCoreAppsBasePath: function getCoreAppsBasePath() {
310     debug("getCoreAppsBasePath()");
311     try {
312       return FileUtils.getDir("coreAppsDir", ["webapps"], false).path;
313     } catch(e) {
314       return null;
315     }
316   },
318   getAppInfo: function getAppInfo(aApps, aAppId) {
319     let app = aApps[aAppId];
321     if (!app) {
322       debug("No webapp for " + aAppId);
323       return null;
324     }
326     // We can have 3rd party apps that are non-removable,
327     // so we can't use the 'removable' property for isCoreApp
328     // Instead, we check if the app is installed under /system/b2g
329     let isCoreApp = false;
331 #ifdef MOZ_WIDGET_GONK
332     isCoreApp = app.basePath == this.getCoreAppsBasePath();
333 #endif
334     debug(app.basePath + " isCoreApp: " + isCoreApp);
336     // Before bug 910473, this is a temporary workaround to get correct path
337     // from child process in mochitest.
338     let prefName = "dom.mozApps.auto_confirm_install";
339     if (Services.prefs.prefHasUserValue(prefName) &&
340         Services.prefs.getBoolPref(prefName)) {
341       return { "path": app.basePath + "/" + app.id,
342                "isCoreApp": isCoreApp };
343     }
345     return { "path": WebappOSUtils.getPackagePath(app),
346              "isCoreApp": isCoreApp };
347   },
349   /**
350     * Remove potential HTML tags from displayable fields in the manifest.
351     * We check name, description, developer name, and permission description
352     */
353   sanitizeManifest: function(aManifest) {
354     let sanitizer = Cc["@mozilla.org/parserutils;1"]
355                       .getService(Ci.nsIParserUtils);
356     if (!sanitizer) {
357       return;
358     }
360     function sanitize(aStr) {
361       return sanitizer.convertToPlainText(aStr,
362                Ci.nsIDocumentEncoder.OutputRaw, 0);
363     }
365     function sanitizeEntryPoint(aRoot) {
366       aRoot.name = sanitize(aRoot.name);
368       if (aRoot.description) {
369         aRoot.description = sanitize(aRoot.description);
370       }
372       if (aRoot.developer && aRoot.developer.name) {
373         aRoot.developer.name = sanitize(aRoot.developer.name);
374       }
376       if (aRoot.permissions) {
377         for (let permission in aRoot.permissions) {
378           if (aRoot.permissions[permission].description) {
379             aRoot.permissions[permission].description =
380              sanitize(aRoot.permissions[permission].description);
381           }
382         }
383       }
384     }
386     // First process the main section, then the entry points.
387     sanitizeEntryPoint(aManifest);
389     if (aManifest.entry_points) {
390       for (let entry in aManifest.entry_points) {
391         sanitizeEntryPoint(aManifest.entry_points[entry]);
392       }
393     }
394   },
396   /**
397    * From https://developer.mozilla.org/en/OpenWebApps/The_Manifest
398    * Only the name property is mandatory.
399    */
400   checkManifest: function(aManifest, app) {
401     if (aManifest.name == undefined)
402       return false;
404     this.sanitizeManifest(aManifest);
406     // launch_path, entry_points launch paths, message hrefs, and activity hrefs can't be absolute
407     if (aManifest.launch_path && isAbsoluteURI(aManifest.launch_path))
408       return false;
410     function checkAbsoluteEntryPoints(entryPoints) {
411       for (let name in entryPoints) {
412         if (entryPoints[name].launch_path && isAbsoluteURI(entryPoints[name].launch_path)) {
413           return true;
414         }
415       }
416       return false;
417     }
419     if (checkAbsoluteEntryPoints(aManifest.entry_points))
420       return false;
422     for (let localeName in aManifest.locales) {
423       if (checkAbsoluteEntryPoints(aManifest.locales[localeName].entry_points)) {
424         return false;
425       }
426     }
428     if (aManifest.activities) {
429       for (let activityName in aManifest.activities) {
430         let activity = aManifest.activities[activityName];
431         if (activity.href && isAbsoluteURI(activity.href)) {
432           return false;
433         }
434       }
435     }
437     // |messages| is an array of items, where each item is either a string or
438     // a {name: href} object.
439     let messages = aManifest.messages;
440     if (messages) {
441       if (!Array.isArray(messages)) {
442         return false;
443       }
444       for (let item of aManifest.messages) {
445         if (typeof item == "object") {
446           let keys = Object.keys(item);
447           if (keys.length != 1) {
448             return false;
449           }
450           if (isAbsoluteURI(item[keys[0]])) {
451             return false;
452           }
453         }
454       }
455     }
457     // The 'size' field must be a positive integer.
458     if (aManifest.size) {
459       aManifest.size = parseInt(aManifest.size);
460       if (Number.isNaN(aManifest.size) || aManifest.size < 0) {
461         return false;
462       }
463     }
465     // The 'role' field must be a string.
466     if (aManifest.role && (typeof aManifest.role !== "string")) {
467       return false;
468     }
469     return true;
470   },
472   checkManifestContentType: function
473      checkManifestContentType(aInstallOrigin, aWebappOrigin, aContentType) {
474     let hadCharset = { };
475     let charset = { };
476     let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
477     let contentType = netutil.parseContentType(aContentType, charset, hadCharset);
478     if (aInstallOrigin != aWebappOrigin &&
479         !(contentType == "application/x-web-app-manifest+json" ||
480           contentType == "application/manifest+json")) {
481       return false;
482     }
483     return true;
484   },
486   allowUnsignedAddons: false, // for testing purposes.
488   /**
489    * Checks if the app role is allowed:
490    * Only certified apps can be themes.
491    * Only privileged or certified apps can be addons.
492    * @param aRole   : the role assigned to this app.
493    * @param aStatus : the APP_STATUS_* for this app.
494    */
495   checkAppRole: function(aRole, aStatus) {
496     if (aRole == "theme" && aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
497       return false;
498     }
499     if (!this.allowUnsignedAddons &&
500         (aRole == "addon" &&
501          aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
502          aStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED)) {
503       return false;
504     }
505     return true;
506   },
508   /**
509    * Method to apply modifications to webapp manifests file saved internally.
510    * For now, only ensure app can't rename itself.
511    */
512   ensureSameAppName: function ensureSameAppName(aOldManifest, aNewManifest, aApp) {
513     // Ensure that app name can't be updated
514     aNewManifest.name = aApp.name;
516     let defaultShortName =
517       new ManifestHelper(aOldManifest, aApp.origin, aApp.manifestURL).short_name;
518     aNewManifest.short_name = defaultShortName;
520     // Nor through localized names
521     if ("locales" in aNewManifest) {
522       for (let locale in aNewManifest.locales) {
523         let newLocaleEntry = aNewManifest.locales[locale];
525         let oldLocaleEntry = aOldManifest && "locales" in aOldManifest &&
526             locale in aOldManifest.locales && aOldManifest.locales[locale];
528         if (newLocaleEntry.name) {
529           // In case previous manifest didn't had a name,
530           // we use the default app name
531           newLocaleEntry.name =
532             (oldLocaleEntry && oldLocaleEntry.name) || aApp.name;
533         }
534         if (newLocaleEntry.short_name) {
535           newLocaleEntry.short_name =
536             (oldLocaleEntry && oldLocaleEntry.short_name) || defaultShortName;
537         }
538       }
539     }
540   },
542   /**
543    * Determines whether the manifest allows installs for the given origin.
544    * @param object aManifest
545    * @param string aInstallOrigin
546    * @return boolean
547    **/
548   checkInstallAllowed: function checkInstallAllowed(aManifest, aInstallOrigin) {
549     if (!aManifest.installs_allowed_from) {
550       return true;
551     }
553     function cbCheckAllowedOrigin(aOrigin) {
554       return aOrigin == "*" || aOrigin == aInstallOrigin;
555     }
557     return aManifest.installs_allowed_from.some(cbCheckAllowedOrigin);
558   },
560   /**
561    * Determine the type of app (app, privileged, certified)
562    * that is installed by the manifest
563    * @param object aManifest
564    * @returns integer
565    **/
566   getAppManifestStatus: function getAppManifestStatus(aManifest) {
567     let type = aManifest.type || "web";
569     switch(type) {
570     case "web":
571     case "trusted":
572       return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
573     case "privileged":
574       return Ci.nsIPrincipal.APP_STATUS_PRIVILEGED;
575     case "certified":
576       return Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
577     default:
578       throw new Error("Webapps.jsm: Undetermined app manifest type");
579     }
580   },
582   /**
583    * Determines if an update or a factory reset occured.
584    */
585   isFirstRun: function isFirstRun(aPrefBranch) {
586     let savedmstone = null;
587     try {
588       savedmstone = aPrefBranch.getCharPref("gecko.mstone");
589     } catch (e) {}
591     let mstone = Services.appinfo.platformVersion;
593     let savedBuildID = null;
594     try {
595       savedBuildID = aPrefBranch.getCharPref("gecko.buildID");
596     } catch (e) {}
598     let buildID = Services.appinfo.platformBuildID;
600     aPrefBranch.setCharPref("gecko.mstone", mstone);
601     aPrefBranch.setCharPref("gecko.buildID", buildID);
603     if ((mstone != savedmstone) || (buildID != savedBuildID)) {
604       aPrefBranch.setBoolPref("dom.apps.reset-permissions", false);
605       return true;
606     } else {
607       return false;
608     }
609   },
611   /**
612    * Check if two manifests have the same set of properties and that the
613    * values of these properties are the same, in each locale.
614    * Manifests here are raw json ones.
615    */
616   compareManifests: function compareManifests(aManifest1, aManifest2) {
617     // 1. check if we have the same locales in both manifests.
618     let locales1 = [];
619     let locales2 = [];
620     if (aManifest1.locales) {
621       for (let locale in aManifest1.locales) {
622         locales1.push(locale);
623       }
624     }
625     if (aManifest2.locales) {
626       for (let locale in aManifest2.locales) {
627         locales2.push(locale);
628       }
629     }
630     if (locales1.sort().join() !== locales2.sort().join()) {
631       return false;
632     }
634     // Helper function to check the app name and developer information for
635     // two given roots.
636     let checkNameAndDev = function(aRoot1, aRoot2) {
637       let name1 = aRoot1.name;
638       let name2 = aRoot2.name;
639       if (name1 !== name2) {
640         return false;
641       }
643       let dev1 = aRoot1.developer;
644       let dev2 = aRoot2.developer;
645       if ((dev1 && !dev2) || (dev2 && !dev1)) {
646         return false;
647       }
649       return (!dev1 && !dev2) ||
650              (dev1.name === dev2.name && dev1.url === dev2.url);
651     }
653     // 2. For each locale, check if the name and dev info are the same.
654     if (!checkNameAndDev(aManifest1, aManifest2)) {
655       return false;
656     }
658     for (let locale in aManifest1.locales) {
659       if (!checkNameAndDev(aManifest1.locales[locale],
660                            aManifest2.locales[locale])) {
661         return false;
662       }
663     }
665     // Nothing failed.
666     return true;
667   },
669   // Asynchronously loads a JSON file. aPath is a string representing the path
670   // of the file to be read.
671   loadJSONAsync: function(aPath) {
672     let deferred = Promise.defer();
674     try {
675       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
676       file.initWithPath(aPath);
678       let channel = NetUtil.newChannel(file);
679       channel.contentType = "application/json";
681       NetUtil.asyncFetch(channel, function(aStream, aResult) {
682         if (!Components.isSuccessCode(aResult)) {
683           deferred.resolve(null);
685           if (aResult == Cr.NS_ERROR_FILE_NOT_FOUND) {
686             // We expect this under certain circumstances, like for webapps.json
687             // on firstrun, so we return early without reporting an error.
688             return;
689           }
691           Cu.reportError("AppsUtils: Could not read from json file " + aPath);
692           return;
693         }
695         try {
696           // Obtain a converter to read from a UTF-8 encoded input stream.
697           let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
698                             .createInstance(Ci.nsIScriptableUnicodeConverter);
699           converter.charset = "UTF-8";
701           // Read json file into a string
702           let data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream,
703                                                             aStream.available()) || ""));
704           aStream.close();
706           deferred.resolve(data);
707         } catch (ex) {
708           Cu.reportError("AppsUtils: Could not parse JSON: " +
709                          aPath + " " + ex + "\n" + ex.stack);
710           deferred.resolve(null);
711         }
712       });
713     } catch (ex) {
714       Cu.reportError("AppsUtils: Could not read from " +
715                      aPath + " : " + ex + "\n" + ex.stack);
716       deferred.resolve(null);
717     }
719     return deferred.promise;
720   },
722   // Returns the MD5 hash of a string.
723   computeHash: function(aString) {
724     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
725                       .createInstance(Ci.nsIScriptableUnicodeConverter);
726     converter.charset = "UTF-8";
727     let result = {};
728     // Data is an array of bytes.
729     let data = converter.convertToByteArray(aString, result);
731     let hasher = Cc["@mozilla.org/security/hash;1"]
732                    .createInstance(Ci.nsICryptoHash);
733     hasher.init(hasher.MD5);
734     hasher.update(data, data.length);
735     // We're passing false to get the binary hash and not base64.
736     let hash = hasher.finish(false);
738     function toHexString(charCode) {
739       return ("0" + charCode.toString(16)).slice(-2);
740     }
742     // Convert the binary hash data to a hex string.
743     return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
744   },
746   // Returns the hash for a JS object.
747   computeObjectHash: function(aObject) {
748     return this.computeHash(JSON.stringify(aObject));
749   },
751   getAppManifestURLFromWindow: function(aWindow) {
752     let appId = aWindow.document.nodePrincipal.appId;
753     if (appId === Ci.nsIScriptSecurityManager.NO_APP_ID) {
754       return null;
755     }
757     return appsService.getManifestURLByLocalId(appId);
758   },
762  * Helper object to access manifest information with locale support
763  */
764 this.ManifestHelper = function(aManifest, aOrigin, aManifestURL, aLang) {
765   // If the app is packaged, we resolve uris against the origin.
766   // If it's not, against the manifest url.
768   if (!aOrigin || !aManifestURL) {
769     throw Error("ManifestHelper needs both origin and manifestURL");
770   }
772   this._baseURI = Services.io.newURI(
773     aOrigin.startsWith("app://") ? aOrigin : aManifestURL, null, null);
775   // We keep the manifest url in all cases since we need it to
776   // resolve the package path for packaged apps.
777   this._manifestURL = Services.io.newURI(aManifestURL, null, null);
779   this._manifest = aManifest;
781   let locale = aLang;
782   if (!locale) {
783     let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"]
784                    .getService(Ci.nsIXULChromeRegistry)
785                    .QueryInterface(Ci.nsIToolkitChromeRegistry);
786     locale = chrome.getSelectedLocale("global").toLowerCase();
787   }
789   this._localeRoot = this._manifest;
791   if (this._manifest.locales && this._manifest.locales[locale]) {
792     this._localeRoot = this._manifest.locales[locale];
793   }
794   else if (this._manifest.locales) {
795     // try with the language part of the locale ("en" for en-GB) only
796     let lang = locale.split('-')[0];
797     if (lang != locale && this._manifest.locales[lang])
798       this._localeRoot = this._manifest.locales[lang];
799   }
802 ManifestHelper.prototype = {
803   _localeProp: function(aProp) {
804     if (this._localeRoot[aProp] != undefined)
805       return this._localeRoot[aProp];
806     return this._manifest[aProp];
807   },
809   get name() {
810     return this._localeProp("name");
811   },
813   get short_name() {
814     return this._localeProp("short_name");
815   },
817   get description() {
818     return this._localeProp("description");
819   },
821   get type() {
822     return this._localeProp("type");
823   },
825   get version() {
826     return this._localeProp("version");
827   },
829   get launch_path() {
830     return this._localeProp("launch_path");
831   },
833   get developer() {
834     // Default to {} in order to avoid exception in code
835     // that doesn't check for null `developer`
836     return this._localeProp("developer") || {};
837   },
839   get icons() {
840     return this._localeProp("icons");
841   },
843   get appcache_path() {
844     return this._localeProp("appcache_path");
845   },
847   get orientation() {
848     return this._localeProp("orientation");
849   },
851   get package_path() {
852     return this._localeProp("package_path");
853   },
855   get widgetPages() {
856     return this._localeProp("widgetPages");
857   },
859   get size() {
860     return this._manifest["size"] || 0;
861   },
863   get permissions() {
864     if (this._manifest.permissions) {
865       return this._manifest.permissions;
866     }
867     return {};
868   },
870   get biggestIconURL() {
871     let icons = this._localeProp("icons");
872     if (!icons) {
873       return null;
874     }
876     let iconSizes = Object.keys(icons);
877     if (iconSizes.length == 0) {
878       return null;
879     }
881     iconSizes.sort((a, b) => a - b);
882     let biggestIconSize = iconSizes.pop();
883     let biggestIcon = icons[biggestIconSize];
884     let biggestIconURL = this._baseURI.resolve(biggestIcon);
886     return biggestIconURL;
887   },
889   iconURLForSize: function(aSize) {
890     let icons = this._localeProp("icons");
891     if (!icons)
892       return null;
893     let dist = 100000;
894     let icon = null;
895     for (let size in icons) {
896       let iSize = parseInt(size);
897       if (Math.abs(iSize - aSize) < dist) {
898         icon = this._baseURI.resolve(icons[size]);
899         dist = Math.abs(iSize - aSize);
900       }
901     }
902     return icon;
903   },
905   fullLaunchPath: function(aStartPoint) {
906     // If no start point is specified, we use the root launch path.
907     // In all error cases, we just return null.
908     if ((aStartPoint || "") === "") {
909       return this._baseURI.resolve(this._localeProp("launch_path") || "/");
910     }
912     // Search for the l10n entry_points property.
913     let entryPoints = this._localeProp("entry_points");
914     if (!entryPoints) {
915       return null;
916     }
918     if (entryPoints[aStartPoint]) {
919       return this._baseURI.resolve(entryPoints[aStartPoint].launch_path || "/");
920     }
922     return null;
923   },
925   resolveURL: function(aURI) {
926     // This should be enforced higher up, but check it here just in case.
927     if (isAbsoluteURI(aURI)) {
928       throw new Error("Webapps.jsm: non-relative URI passed to resolve");
929     }
930     return this._baseURI.resolve(aURI);
931   },
933   fullAppcachePath: function() {
934     let appcachePath = this._localeProp("appcache_path");
935     return this._baseURI.resolve(appcachePath ? appcachePath : "/");
936   },
938   fullPackagePath: function() {
939     let packagePath = this._localeProp("package_path");
940     return this._manifestURL.resolve(packagePath ? packagePath : "/");
941   },
943   get role() {
944     return this._manifest.role || "";
945   },
947   get csp() {
948     return this._manifest.csp || "";
949   }