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