Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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/osfile.jsm");
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/Task.jsm");
15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 Cu.import("resource://gre/modules/Promise.jsm");
18 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
19 "resource://gre/modules/FileUtils.jsm");
21 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
22 "resource://gre/modules/WebappOSUtils.jsm");
24 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
25 "resource://gre/modules/NetUtil.jsm");
27 // Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js
29 this.EXPORTED_SYMBOLS = ["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
31 function debug(s) {
32 //dump("-*- AppsUtils.jsm: " + s + "\n");
33 }
35 this.isAbsoluteURI = function(aURI) {
36 let foo = Services.io.newURI("http://foo", null, null);
37 let bar = Services.io.newURI("http://bar", null, null);
38 return Services.io.newURI(aURI, null, foo).prePath != foo.prePath ||
39 Services.io.newURI(aURI, null, bar).prePath != bar.prePath;
40 }
42 this.mozIApplication = function(aApp) {
43 _setAppProperties(this, aApp);
44 }
46 mozIApplication.prototype = {
47 hasPermission: function(aPermission) {
48 let uri = Services.io.newURI(this.origin, null, null);
49 let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
50 .getService(Ci.nsIScriptSecurityManager);
51 // This helper checks an URI inside |aApp|'s origin and part of |aApp| has a
52 // specific permission. It is not checking if browsers inside |aApp| have such
53 // permission.
54 let principal = secMan.getAppCodebasePrincipal(uri, this.localId,
55 /*mozbrowser*/false);
56 let perm = Services.perms.testExactPermissionFromPrincipal(principal,
57 aPermission);
58 return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
59 },
61 QueryInterface: function(aIID) {
62 if (aIID.equals(Ci.mozIApplication) ||
63 aIID.equals(Ci.nsISupports))
64 return this;
65 throw Cr.NS_ERROR_NO_INTERFACE;
66 }
67 }
69 function _setAppProperties(aObj, aApp) {
70 aObj.name = aApp.name;
71 aObj.csp = aApp.csp;
72 aObj.installOrigin = aApp.installOrigin;
73 aObj.origin = aApp.origin;
74 #ifdef MOZ_ANDROID_SYNTHAPKS
75 aObj.apkPackageName = aApp.apkPackageName;
76 #endif
77 aObj.receipts = aApp.receipts ? JSON.parse(JSON.stringify(aApp.receipts)) : null;
78 aObj.installTime = aApp.installTime;
79 aObj.manifestURL = aApp.manifestURL;
80 aObj.appStatus = aApp.appStatus;
81 aObj.removable = aApp.removable;
82 aObj.id = aApp.id;
83 aObj.localId = aApp.localId;
84 aObj.basePath = aApp.basePath;
85 aObj.progress = aApp.progress || 0.0;
86 aObj.installState = aApp.installState || "installed";
87 aObj.downloadAvailable = aApp.downloadAvailable;
88 aObj.downloading = aApp.downloading;
89 aObj.readyToApplyDownload = aApp.readyToApplyDownload;
90 aObj.downloadSize = aApp.downloadSize || 0;
91 aObj.lastUpdateCheck = aApp.lastUpdateCheck;
92 aObj.updateTime = aApp.updateTime;
93 aObj.etag = aApp.etag;
94 aObj.packageEtag = aApp.packageEtag;
95 aObj.manifestHash = aApp.manifestHash;
96 aObj.packageHash = aApp.packageHash;
97 aObj.staged = aApp.staged;
98 aObj.installerAppId = aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID;
99 aObj.installerIsBrowser = !!aApp.installerIsBrowser;
100 aObj.storeId = aApp.storeId || "";
101 aObj.storeVersion = aApp.storeVersion || 0;
102 aObj.role = aApp.role || "";
103 aObj.redirects = aApp.redirects;
104 }
106 this.AppsUtils = {
107 // Clones a app, without the manifest.
108 cloneAppObject: function(aApp) {
109 let obj = {};
110 _setAppProperties(obj, aApp);
111 return obj;
112 },
114 getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) {
115 debug("getAppByManifestURL " + aManifestURL);
116 // This could be O(1) if |webapps| was a dictionary indexed on manifestURL
117 // which should be the unique app identifier.
118 // It's currently O(n).
119 for (let id in aApps) {
120 let app = aApps[id];
121 if (app.manifestURL == aManifestURL) {
122 return new mozIApplication(app);
123 }
124 }
126 return null;
127 },
129 getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aApps, aManifestURL) {
130 debug("getAppLocalIdByManifestURL " + aManifestURL);
131 for (let id in aApps) {
132 if (aApps[id].manifestURL == aManifestURL) {
133 return aApps[id].localId;
134 }
135 }
137 return Ci.nsIScriptSecurityManager.NO_APP_ID;
138 },
140 getAppLocalIdByStoreId: function(aApps, aStoreId) {
141 debug("getAppLocalIdByStoreId:" + aStoreId);
142 for (let id in aApps) {
143 if (aApps[id].storeId == aStoreId) {
144 return aApps[id].localId;
145 }
146 }
148 return Ci.nsIScriptSecurityManager.NO_APP_ID;
149 },
151 getCSPByLocalId: function getCSPByLocalId(aApps, aLocalId) {
152 debug("getCSPByLocalId " + aLocalId);
153 for (let id in aApps) {
154 let app = aApps[id];
155 if (app.localId == aLocalId) {
156 return ( app.csp || "" );
157 }
158 }
160 return "";
161 },
163 getAppByLocalId: function getAppByLocalId(aApps, aLocalId) {
164 debug("getAppByLocalId " + aLocalId);
165 for (let id in aApps) {
166 let app = aApps[id];
167 if (app.localId == aLocalId) {
168 return new mozIApplication(app);
169 }
170 }
172 return null;
173 },
175 getManifestURLByLocalId: function getManifestURLByLocalId(aApps, aLocalId) {
176 debug("getManifestURLByLocalId " + aLocalId);
177 for (let id in aApps) {
178 let app = aApps[id];
179 if (app.localId == aLocalId) {
180 return app.manifestURL;
181 }
182 }
184 return "";
185 },
187 getCoreAppsBasePath: function getCoreAppsBasePath() {
188 debug("getCoreAppsBasePath()");
189 try {
190 return FileUtils.getDir("coreAppsDir", ["webapps"], false).path;
191 } catch(e) {
192 return null;
193 }
194 },
196 getAppInfo: function getAppInfo(aApps, aAppId) {
197 let app = aApps[aAppId];
199 if (!app) {
200 debug("No webapp for " + aAppId);
201 return null;
202 }
204 // We can have 3rd party apps that are non-removable,
205 // so we can't use the 'removable' property for isCoreApp
206 // Instead, we check if the app is installed under /system/b2g
207 let isCoreApp = false;
209 #ifdef MOZ_WIDGET_GONK
210 isCoreApp = app.basePath == this.getCoreAppsBasePath();
211 #endif
212 debug(app.basePath + " isCoreApp: " + isCoreApp);
213 return { "path": WebappOSUtils.getPackagePath(app),
214 "isCoreApp": isCoreApp };
215 },
217 /**
218 * Remove potential HTML tags from displayable fields in the manifest.
219 * We check name, description, developer name, and permission description
220 */
221 sanitizeManifest: function(aManifest) {
222 let sanitizer = Cc["@mozilla.org/parserutils;1"]
223 .getService(Ci.nsIParserUtils);
224 if (!sanitizer) {
225 return;
226 }
228 function sanitize(aStr) {
229 return sanitizer.convertToPlainText(aStr,
230 Ci.nsIDocumentEncoder.OutputRaw, 0);
231 }
233 function sanitizeEntryPoint(aRoot) {
234 aRoot.name = sanitize(aRoot.name);
236 if (aRoot.description) {
237 aRoot.description = sanitize(aRoot.description);
238 }
240 if (aRoot.developer && aRoot.developer.name) {
241 aRoot.developer.name = sanitize(aRoot.developer.name);
242 }
244 if (aRoot.permissions) {
245 for (let permission in aRoot.permissions) {
246 if (aRoot.permissions[permission].description) {
247 aRoot.permissions[permission].description =
248 sanitize(aRoot.permissions[permission].description);
249 }
250 }
251 }
252 }
254 // First process the main section, then the entry points.
255 sanitizeEntryPoint(aManifest);
257 if (aManifest.entry_points) {
258 for (let entry in aManifest.entry_points) {
259 sanitizeEntryPoint(aManifest.entry_points[entry]);
260 }
261 }
262 },
264 /**
265 * From https://developer.mozilla.org/en/OpenWebApps/The_Manifest
266 * Only the name property is mandatory.
267 */
268 checkManifest: function(aManifest, app) {
269 if (aManifest.name == undefined)
270 return false;
272 this.sanitizeManifest(aManifest);
274 // launch_path, entry_points launch paths, message hrefs, and activity hrefs can't be absolute
275 if (aManifest.launch_path && isAbsoluteURI(aManifest.launch_path))
276 return false;
278 function checkAbsoluteEntryPoints(entryPoints) {
279 for (let name in entryPoints) {
280 if (entryPoints[name].launch_path && isAbsoluteURI(entryPoints[name].launch_path)) {
281 return true;
282 }
283 }
284 return false;
285 }
287 if (checkAbsoluteEntryPoints(aManifest.entry_points))
288 return false;
290 for (let localeName in aManifest.locales) {
291 if (checkAbsoluteEntryPoints(aManifest.locales[localeName].entry_points)) {
292 return false;
293 }
294 }
296 if (aManifest.activities) {
297 for (let activityName in aManifest.activities) {
298 let activity = aManifest.activities[activityName];
299 if (activity.href && isAbsoluteURI(activity.href)) {
300 return false;
301 }
302 }
303 }
305 // |messages| is an array of items, where each item is either a string or
306 // a {name: href} object.
307 let messages = aManifest.messages;
308 if (messages) {
309 if (!Array.isArray(messages)) {
310 return false;
311 }
312 for (let item of aManifest.messages) {
313 if (typeof item == "object") {
314 let keys = Object.keys(item);
315 if (keys.length != 1) {
316 return false;
317 }
318 if (isAbsoluteURI(item[keys[0]])) {
319 return false;
320 }
321 }
322 }
323 }
325 // The 'size' field must be a positive integer.
326 if (aManifest.size) {
327 aManifest.size = parseInt(aManifest.size);
328 if (Number.isNaN(aManifest.size) || aManifest.size < 0) {
329 return false;
330 }
331 }
333 // The 'role' field must be a string.
334 if (aManifest.role && (typeof aManifest.role !== "string")) {
335 return false;
336 }
337 return true;
338 },
340 checkManifestContentType: function
341 checkManifestContentType(aInstallOrigin, aWebappOrigin, aContentType) {
342 let hadCharset = { };
343 let charset = { };
344 let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
345 let contentType = netutil.parseContentType(aContentType, charset, hadCharset);
346 if (aInstallOrigin != aWebappOrigin &&
347 contentType != "application/x-web-app-manifest+json") {
348 return false;
349 }
350 return true;
351 },
353 /**
354 * Method to apply modifications to webapp manifests file saved internally.
355 * For now, only ensure app can't rename itself.
356 */
357 ensureSameAppName: function ensureSameAppName(aOldManifest, aNewManifest, aApp) {
358 // Ensure that app name can't be updated
359 aNewManifest.name = aApp.name;
361 // Nor through localized names
362 if ('locales' in aNewManifest) {
363 let defaultName = new ManifestHelper(aOldManifest, aApp.origin).name;
364 for (let locale in aNewManifest.locales) {
365 let entry = aNewManifest.locales[locale];
366 if (!entry.name) {
367 continue;
368 }
369 // In case previous manifest didn't had a name,
370 // we use the default app name
371 let localizedName = defaultName;
372 if (aOldManifest && 'locales' in aOldManifest &&
373 locale in aOldManifest.locales) {
374 localizedName = aOldManifest.locales[locale].name;
375 }
376 entry.name = localizedName;
377 }
378 }
379 },
381 /**
382 * Determines whether the manifest allows installs for the given origin.
383 * @param object aManifest
384 * @param string aInstallOrigin
385 * @return boolean
386 **/
387 checkInstallAllowed: function checkInstallAllowed(aManifest, aInstallOrigin) {
388 if (!aManifest.installs_allowed_from) {
389 return true;
390 }
392 function cbCheckAllowedOrigin(aOrigin) {
393 return aOrigin == "*" || aOrigin == aInstallOrigin;
394 }
396 return aManifest.installs_allowed_from.some(cbCheckAllowedOrigin);
397 },
399 /**
400 * Determine the type of app (app, privileged, certified)
401 * that is installed by the manifest
402 * @param object aManifest
403 * @returns integer
404 **/
405 getAppManifestStatus: function getAppManifestStatus(aManifest) {
406 let type = aManifest.type || "web";
408 switch(type) {
409 case "web":
410 return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
411 case "privileged":
412 return Ci.nsIPrincipal.APP_STATUS_PRIVILEGED;
413 case "certified":
414 return Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
415 default:
416 throw new Error("Webapps.jsm: Undetermined app manifest type");
417 }
418 },
420 /**
421 * Determines if an update or a factory reset occured.
422 */
423 isFirstRun: function isFirstRun(aPrefBranch) {
424 let savedmstone = null;
425 try {
426 savedmstone = aPrefBranch.getCharPref("gecko.mstone");
427 } catch (e) {}
429 let mstone = Services.appinfo.platformVersion;
431 let savedBuildID = null;
432 try {
433 savedBuildID = aPrefBranch.getCharPref("gecko.buildID");
434 } catch (e) {}
436 let buildID = Services.appinfo.platformBuildID;
438 aPrefBranch.setCharPref("gecko.mstone", mstone);
439 aPrefBranch.setCharPref("gecko.buildID", buildID);
441 return ((mstone != savedmstone) || (buildID != savedBuildID));
442 },
444 /**
445 * Check if two manifests have the same set of properties and that the
446 * values of these properties are the same, in each locale.
447 * Manifests here are raw json ones.
448 */
449 compareManifests: function compareManifests(aManifest1, aManifest2) {
450 // 1. check if we have the same locales in both manifests.
451 let locales1 = [];
452 let locales2 = [];
453 if (aManifest1.locales) {
454 for (let locale in aManifest1.locales) {
455 locales1.push(locale);
456 }
457 }
458 if (aManifest2.locales) {
459 for (let locale in aManifest2.locales) {
460 locales2.push(locale);
461 }
462 }
463 if (locales1.sort().join() !== locales2.sort().join()) {
464 return false;
465 }
467 // Helper function to check the app name and developer information for
468 // two given roots.
469 let checkNameAndDev = function(aRoot1, aRoot2) {
470 let name1 = aRoot1.name;
471 let name2 = aRoot2.name;
472 if (name1 !== name2) {
473 return false;
474 }
476 let dev1 = aRoot1.developer;
477 let dev2 = aRoot2.developer;
478 if ((dev1 && !dev2) || (dev2 && !dev1)) {
479 return false;
480 }
482 return (!dev1 && !dev2) ||
483 (dev1.name === dev2.name && dev1.url === dev2.url);
484 }
486 // 2. For each locale, check if the name and dev info are the same.
487 if (!checkNameAndDev(aManifest1, aManifest2)) {
488 return false;
489 }
491 for (let locale in aManifest1.locales) {
492 if (!checkNameAndDev(aManifest1.locales[locale],
493 aManifest2.locales[locale])) {
494 return false;
495 }
496 }
498 // Nothing failed.
499 return true;
500 },
502 // Asynchronously loads a JSON file. aPath is a string representing the path
503 // of the file to be read.
504 loadJSONAsync: function(aPath) {
505 let deferred = Promise.defer();
507 try {
508 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
509 file.initWithPath(aPath);
511 let channel = NetUtil.newChannel(file);
512 channel.contentType = "application/json";
514 NetUtil.asyncFetch(channel, function(aStream, aResult) {
515 if (!Components.isSuccessCode(aResult)) {
516 deferred.resolve(null);
518 if (aResult == Cr.NS_ERROR_FILE_NOT_FOUND) {
519 // We expect this under certain circumstances, like for webapps.json
520 // on firstrun, so we return early without reporting an error.
521 return;
522 }
524 Cu.reportError("AppsUtils: Could not read from json file " + aPath);
525 return;
526 }
528 try {
529 // Obtain a converter to read from a UTF-8 encoded input stream.
530 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
531 .createInstance(Ci.nsIScriptableUnicodeConverter);
532 converter.charset = "UTF-8";
534 // Read json file into a string
535 let data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream,
536 aStream.available()) || ""));
537 aStream.close();
539 deferred.resolve(data);
540 } catch (ex) {
541 Cu.reportError("AppsUtils: Could not parse JSON: " +
542 aPath + " " + ex + "\n" + ex.stack);
543 deferred.resolve(null);
544 }
545 });
546 } catch (ex) {
547 Cu.reportError("AppsUtils: Could not read from " +
548 aPath + " : " + ex + "\n" + ex.stack);
549 deferred.resolve(null);
550 }
552 return deferred.promise;
553 },
555 // Returns the MD5 hash of a string.
556 computeHash: function(aString) {
557 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
558 .createInstance(Ci.nsIScriptableUnicodeConverter);
559 converter.charset = "UTF-8";
560 let result = {};
561 // Data is an array of bytes.
562 let data = converter.convertToByteArray(aString, result);
564 let hasher = Cc["@mozilla.org/security/hash;1"]
565 .createInstance(Ci.nsICryptoHash);
566 hasher.init(hasher.MD5);
567 hasher.update(data, data.length);
568 // We're passing false to get the binary hash and not base64.
569 let hash = hasher.finish(false);
571 function toHexString(charCode) {
572 return ("0" + charCode.toString(16)).slice(-2);
573 }
575 // Convert the binary hash data to a hex string.
576 return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
577 }
578 }
580 /**
581 * Helper object to access manifest information with locale support
582 */
583 this.ManifestHelper = function(aManifest, aOrigin) {
584 this._origin = Services.io.newURI(aOrigin, null, null);
585 this._manifest = aManifest;
586 let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry)
587 .QueryInterface(Ci.nsIToolkitChromeRegistry);
588 let locale = chrome.getSelectedLocale("global").toLowerCase();
589 this._localeRoot = this._manifest;
591 if (this._manifest.locales && this._manifest.locales[locale]) {
592 this._localeRoot = this._manifest.locales[locale];
593 }
594 else if (this._manifest.locales) {
595 // try with the language part of the locale ("en" for en-GB) only
596 let lang = locale.split('-')[0];
597 if (lang != locale && this._manifest.locales[lang])
598 this._localeRoot = this._manifest.locales[lang];
599 }
600 };
602 ManifestHelper.prototype = {
603 _localeProp: function(aProp) {
604 if (this._localeRoot[aProp] != undefined)
605 return this._localeRoot[aProp];
606 return this._manifest[aProp];
607 },
609 get name() {
610 return this._localeProp("name");
611 },
613 get description() {
614 return this._localeProp("description");
615 },
617 get type() {
618 return this._localeProp("type");
619 },
621 get version() {
622 return this._localeProp("version");
623 },
625 get launch_path() {
626 return this._localeProp("launch_path");
627 },
629 get developer() {
630 // Default to {} in order to avoid exception in code
631 // that doesn't check for null `developer`
632 return this._localeProp("developer") || {};
633 },
635 get icons() {
636 return this._localeProp("icons");
637 },
639 get appcache_path() {
640 return this._localeProp("appcache_path");
641 },
643 get orientation() {
644 return this._localeProp("orientation");
645 },
647 get package_path() {
648 return this._localeProp("package_path");
649 },
651 get size() {
652 return this._manifest["size"] || 0;
653 },
655 get permissions() {
656 if (this._manifest.permissions) {
657 return this._manifest.permissions;
658 }
659 return {};
660 },
662 get biggestIconURL() {
663 let icons = this._localeProp("icons");
664 if (!icons) {
665 return null;
666 }
668 let iconSizes = Object.keys(icons);
669 if (iconSizes.length == 0) {
670 return null;
671 }
673 iconSizes.sort((a, b) => a - b);
674 let biggestIconSize = iconSizes.pop();
675 let biggestIcon = icons[biggestIconSize];
676 let biggestIconURL = this._origin.resolve(biggestIcon);
678 return biggestIconURL;
679 },
681 iconURLForSize: function(aSize) {
682 let icons = this._localeProp("icons");
683 if (!icons)
684 return null;
685 let dist = 100000;
686 let icon = null;
687 for (let size in icons) {
688 let iSize = parseInt(size);
689 if (Math.abs(iSize - aSize) < dist) {
690 icon = this._origin.resolve(icons[size]);
691 dist = Math.abs(iSize - aSize);
692 }
693 }
694 return icon;
695 },
697 fullLaunchPath: function(aStartPoint) {
698 // If no start point is specified, we use the root launch path.
699 // In all error cases, we just return null.
700 if ((aStartPoint || "") === "") {
701 return this._origin.resolve(this._localeProp("launch_path") || "");
702 }
704 // Search for the l10n entry_points property.
705 let entryPoints = this._localeProp("entry_points");
706 if (!entryPoints) {
707 return null;
708 }
710 if (entryPoints[aStartPoint]) {
711 return this._origin.resolve(entryPoints[aStartPoint].launch_path || "");
712 }
714 return null;
715 },
717 resolveFromOrigin: function(aURI) {
718 // This should be enforced higher up, but check it here just in case.
719 if (isAbsoluteURI(aURI)) {
720 throw new Error("Webapps.jsm: non-relative URI passed to resolveFromOrigin");
721 }
722 return this._origin.resolve(aURI);
723 },
725 fullAppcachePath: function() {
726 let appcachePath = this._localeProp("appcache_path");
727 return this._origin.resolve(appcachePath ? appcachePath : "");
728 },
730 fullPackagePath: function() {
731 let packagePath = this._localeProp("package_path");
732 return this._origin.resolve(packagePath ? packagePath : "");
733 },
735 get role() {
736 return this._manifest.role || "";
737 }
738 }