Bug 758269 - Install permissions from manifest to Permission Manager r=fabrice
authorDavid Dahl <ddahl@mozilla.com>
Fri, 28 Sep 2012 17:16:29 -0500
changeset 108658 0153fec641e962a88c23956f82cd48dd67cc63d4
parent 108657 44465ef545e3f46c59939a742acfdbb9558c8e03
child 108659 777564a3cd50c7b441c175a9882f5e921e0892c1
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersfabrice
bugs758269
milestone18.0a1
Bug 758269 - Install permissions from manifest to Permission Manager r=fabrice
dom/apps/src/Makefile.in
dom/apps/src/PermissionsTable.jsm
dom/apps/src/Webapps.js
dom/apps/src/Webapps.jsm
dom/permission/PermissionSettings.js
dom/permission/PermissionSettings.jsm
dom/tests/browser/Makefile.in
dom/tests/browser/browser_webapps_permissions.js
dom/tests/browser/browser_webapps_perms_reinstall.js
dom/tests/browser/test-webapp-original.webapp
dom/tests/browser/test-webapp-reinstall.webapp
dom/tests/browser/test-webapp.webapp
dom/tests/browser/test-webapps-permissions.html
--- a/dom/apps/src/Makefile.in
+++ b/dom/apps/src/Makefile.in
@@ -20,9 +20,13 @@ EXTRA_PP_COMPONENTS = \
   $(NULL)
 
 EXTRA_PP_JS_MODULES += \
   Webapps.jsm \
   AppsServiceChild.jsm \
   AppsUtils.jsm \
   $(NULL)
 
+EXTRA_JS_MODULES += \
+  PermissionsTable.jsm \
+  $(NULL)
+
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Ci = Components.interfaces;
+
+var EXPORTED_SYMBOLS = ["PermissionsTable",
+                        "UNKNOWN_ACTION",
+                        "ALLOW_ACTION",
+                        "DENY_ACTION",
+                        "PROMPT_ACTION",
+                        "AllPossiblePermissions",
+                        "mapSuffixes",
+                       ];
+
+const UNKNOWN_ACTION = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+const ALLOW_ACTION = Ci.nsIPermissionManager.ALLOW_ACTION;
+const DENY_ACTION = Ci.nsIPermissionManager.DENY_ACTION;
+const PROMPT_ACTION = Ci.nsIPermissionManager.PROMPT_ACTION;
+
+/**
+ * Converts ['read', 'write'] to ['contacts-read', 'contacts-write'], etc...
+ * @param string aPermName
+ * @param Array aSuffixes
+ * @returns Array
+ **/
+function mapSuffixes(aPermName, aSuffixes)
+{
+  return aSuffixes.map(function(suf) { return aPermName + "-" + suf; });
+}
+
+// Permissions Matrix: https://docs.google.com/spreadsheet/ccc?key=0Akyz_Bqjgf5pdENVekxYRjBTX0dCXzItMnRyUU1RQ0E#gid=0
+
+// Permissions that are implicit:
+// battery-status, idle, network-information, vibration,
+// device-capabilities, webapps-manage, web-activities
+
+const PermissionsTable = { "resource-lock": {
+                             app: ALLOW_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           geolocation: {
+                             app: PROMPT_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           camera: {
+                             app: DENY_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           alarm: {
+                             app: ALLOW_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "network-tcp": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           contacts: {
+                             app: DENY_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION,
+                             access: ["read",
+                                      "write",
+                                      "create"
+                                     ]
+                           },
+                           "device-storage:apps": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "device-storage:pictures": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "device-storage:videos": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "device-storage:music": {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           sms: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           telephony: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           browser: {
+                             app: DENY_ACTION,
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           bluetooth: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           wifi: {
+                             app: DENY_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           keyboard: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           mobileconnection: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           power: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           push: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           settings: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION,
+                             access: ["read",
+                                      "write"
+                                     ],
+                           },
+                           permissions: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           fmradio: {
+                             app: ALLOW_ACTION,     // Matrix indicates '?'
+                             privileged: ALLOW_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           attention: {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                         };
+
+// Sometimes all permissions (fully expanded) need to be iterated through
+let AllPossiblePermissions = [];
+for (let permName in PermissionsTable) {
+  if (PermissionsTable[permName].access) {
+    AllPossiblePermissions =
+      AllPossiblePermissions.concat(mapSuffixes(permName,
+                                    PermissionsTable[permName].access));
+  }
+  else {
+    AllPossiblePermissions.push(permName);
+  }
+}
--- a/dom/apps/src/Webapps.js
+++ b/dom/apps/src/Webapps.js
@@ -105,23 +105,26 @@ WebappsRegistry.prototype = {
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         let manifest;
         try {
           manifest = JSON.parse(xhr.responseText, installOrigin);
         } catch (e) {
           Services.DOMRequest.fireError(request, "MANIFEST_PARSE_ERROR");
+          Cu.reportError("Error installing app from: " + installOrigin + ": " + "MANIFEST_PARSE_ERROR");
           return;
         }
 
         if (!AppsUtils.checkManifest(manifest, installOrigin)) {
           Services.DOMRequest.fireError(request, "INVALID_MANIFEST");
+          Cu.reportError("Error installing app from: " + installOrigin + ": " + "INVALID_MANIFEST");
         } else if (!this.checkAppStatus(manifest)) {
           Services.DOMRequest.fireError(request, "INVALID_SECURITY_LEVEL");
+          Cu.reportError("Error installing app, '" + manifest.name + "': " + "INVALID_SECURITY_LEVEL");
         } else {
           let receipts = (aParams && aParams.receipts && Array.isArray(aParams.receipts)) ? aParams.receipts : [];
           let categories = (aParams && aParams.categories && Array.isArray(aParams.categories)) ? aParams.categories : [];
           let etag = xhr.getResponseHeader("Etag");
           cpmm.sendAsyncMessage("Webapps:Install", { app: { installOrigin: installOrigin,
                                                             origin: this._getOrigin(aURL),
                                                             manifestURL: aURL,
                                                             manifest: manifest,
@@ -129,21 +132,23 @@ WebappsRegistry.prototype = {
                                                             receipts: receipts,
                                                             categories: categories },
                                                             from: installURL,
                                                             oid: this._id,
                                                             requestID: requestID });
         }
       } else {
         Services.DOMRequest.fireError(request, "MANIFEST_URL_ERROR");
+        Cu.reportError("Error installing app from: " + installOrigin + ": " + "MANIFEST_URL_ERROR");
       }
     }).bind(this), false);
 
     xhr.addEventListener("error", (function() {
       Services.DOMRequest.fireError(request, "NETWORK_ERROR");
+      Cu.reportError("Error installing app from: " + installOrigin + ": " + "NETWORK_ERROR");
     }).bind(this), false);
 
     xhr.send(null);
     return request;
   },
 
   getSelf: function() {
     let request = this.createRequest();
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -11,28 +11,47 @@ const Cr = Components.results;
 
 let EXPORTED_SYMBOLS = ["DOMApplicationRegistry", "DOMApplicationManifest"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/PermissionsTable.jsm");
 
 function debug(aMsg) {
   //dump("-*-*- Webapps.jsm : " + aMsg + "\n");
 }
 
 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
 
+// Permission access flags
+const READONLY = "readonly";
+const CREATEONLY = "createonly";
+const READCREATE = "readcreate";
+const READWRITE = "readwrite";
+
+const PERM_TO_STRING = ["unknown", "allow", "deny", "prompt"];
+
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "PermSettings",
+                                   "@mozilla.org/permissionSettings;1",
+                                   "nsIDOMPermissionSettings");
+
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "permissionManager",
+                                   "@mozilla.org/permissionmanager;1",
+                                   "nsIPermissionManager");
+
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageBroadcaster");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
@@ -47,16 +66,90 @@ XPCOMUtils.defineLazyGetter(this, "msgmg
   const DIRECTORY_NAME = "webappsDir";
 #else
   // If we're executing in the context of the webapp runtime, the data files
   // are in a different directory (currently the Firefox profile that installed
   // the webapp); otherwise, they're in the current profile.
   const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD";
 #endif
 
+/**
+ * Determine the type of app (app, privileged, certified)
+ * that is installed by the manifest
+ * @param object aManifest
+ * @returns integer
+ **/
+function getAppManifestStatus(aManifest)
+{
+  let type = aManifest.type || "web";
+
+  switch(type) {
+  case "web":
+    return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+  case "privileged":
+    return Ci.nsIPrincipal.APP_STATUS_PRIVILEGED;
+  case "certified":
+    return Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
+  default:
+    throw new Error("Webapps.jsm: Undetermined app manifest type");
+  }
+}
+
+/**
+ * Expand an access string into multiple permission names,
+ *   e.g: perm 'contacts' with 'readwrite' =
+ *   ['contacts-read', 'contacts-create', contacts-write']
+ * @param string aPermName
+ * @param string aAccess
+ * @returns Array
+ **/
+function expandPermissions(aPermName, aAccess)
+{
+  if (!PermissionsTable[aPermName]) {
+    Cu.reportError("Unknown permission: " + aPermName);
+    throw new Error("Webapps.jsm: App install failed, Unknown Permission: " + aPermName);
+  }
+  if (!aAccess && PermissionsTable[aPermName].access ||
+      aAccess && !PermissionsTable[aPermName].access) {
+    Cu.reportError("Webapps.jsm: installPermissions: Invalid Manifest");
+    throw new Error("Webapps.jsm: App install failed, Invalid Manifest");
+  }
+  if (!PermissionsTable[aPermName].access) {
+    return [aPermName];
+  }
+
+  let requestedSuffixes = [];
+  switch(aAccess) {
+  case READONLY:
+    requestedSuffixes.push("read");
+    break;
+  case CREATEONLY:
+    requestedSuffixes.push("create");
+    break;
+  case READCREATE:
+    requestedSuffixes.push("read", "create");
+    break;
+  case READWRITE:
+    requestedSuffixes.push("read", "create", "write");
+    break;
+  default:
+    return [];
+  }
+
+  let permArr = mapSuffixes(aPermName, requestedSuffixes);
+
+  let expandedPerms = [];
+  for (let idx in permArr) {
+    if (PermissionsTable[aPermName].access.indexOf(requestedSuffixes[idx]) != -1) {
+      expandedPerms.push(permArr[idx]);
+    }
+  }
+  return expandedPerms;
+}
+
 let DOMApplicationRegistry = {
   appsFile: null,
   webapps: { },
   children: [ ],
   allAppsLaunchable: false,
   downloads: { },
 
   init: function() {
@@ -446,16 +539,17 @@ let DOMApplicationRegistry = {
         Services.obs.notifyObservers(mm, "webapps-ask-install", JSON.stringify(msg));
         break;
       case "Webapps:GetSelf":
         this.getSelf(msg, mm);
         break;
       case "Webapps:Uninstall":
         Services.obs.notifyObservers(mm, "webapps-uninstall", JSON.stringify(msg));
         this.uninstall(msg);
+        debug("Webapps:Uninstall");
         break;
       case "Webapps:Launch":
         this.launchApp(msg, mm);
         break;
       case "Webapps:IsInstalled":
         this.isInstalled(msg, mm);
         break;
       case "Webapps:GetInstalled":
@@ -601,16 +695,117 @@ let DOMApplicationRegistry = {
                                     : updateService.scheduleUpdate(appcacheURI, docURI, null);
       cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
       if (aOfflineCacheObserver) {
         cacheUpdate.addObserver(aOfflineCacheObserver, false);
       }
     }
   },
 
+  /**
+   * Install permissisions or remove deprecated permissions upon re-install
+   * @param object aAppObject
+   *        The just installed AppUtils cloned appObject
+   * @param object aData
+   *        The just-installed app configuration
+   * @param boolean aIsReinstall
+   *        Indicates the app was just re-installed
+   * @returns void
+   **/
+  installPermissions:
+  function installPermissions(aAppObject, aData, aIsReinstall)
+  {
+    try {
+      let newManifest = new DOMApplicationManifest(aData.app.manifest,
+                                                   aData.app.origin);
+      if (!newManifest.permissions && !aIsReinstall) {
+        return;
+      }
+
+      if (aIsReinstall) {
+        // Compare the original permissions against the new permissions
+        // Remove any deprecated Permissions
+
+        if (newManifest.permissions) {
+          // Expand perms
+          let newPerms = [];
+          for (let perm in newManifest.permissions) {
+            let _perms = expandPermissions(perm,
+                                           newManifest.permissions[perm].access);
+            newPerms = newPerms.concat(_perms);
+          }
+
+          for (let idx in AllPossiblePermissions) {
+            let index = newPerms.indexOf(AllPossiblePermissions[idx]);
+            if (index == -1) {
+              // See if the permission was installed previously
+              let _perm = PermSettings.get(AllPossiblePermissions[idx],
+                                           aData.app.manifestURL,
+                                           aData.app.origin,
+                                           false);
+              if (_perm == "unknown" || _perm == "deny") {
+                // All 'deny' permissions should be preserved
+                continue;
+              }
+              // Remove the deprecated permission
+              // TODO: use PermSettings.remove, see bug 793204
+              PermSettings.set(AllPossiblePermissions[idx],
+                               "unknown",
+                               aData.app.manifestURL,
+                               aData.app.origin,
+                               false);
+            }
+          }
+        }
+      }
+
+      let installPermType;
+      // Check to see if the 'webapp' is app/priv/certified
+      switch (getAppManifestStatus(newManifest)) {
+      case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
+        installPermType = "certified";
+        break;
+      case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
+        installPermType = "privileged";
+        break;
+      case Ci.nsIPrincipal.APP_STATUS_INSTALLED:
+        installPermType = "app";
+        break;
+      default:
+        // Cannot determine app type, abort install by throwing an error
+        throw new Error("Webapps.jsm: Cannot determine app type, install cancelled");
+      }
+
+      for (let permName in newManifest.permissions) {
+        if (!PermissionsTable[permName]) {
+          throw new Error("Webapps.jsm: '" + permName + "'" +
+                         " is not a valid Webapps permission type. Aborting Webapp installation");
+          return;
+        }
+
+        let perms = expandPermissions(permName,
+                                      newManifest.permissions[permName].access);
+        for (let idx in perms) {
+          let perm = PermissionsTable[permName][installPermType];
+          let permValue = PERM_TO_STRING[perm];
+          PermSettings.set(perms[idx],
+                           permValue,
+                           aData.app.manifestURL,
+                           aData.app.origin,
+                           false);
+        }
+      }
+    }
+    catch (ex) {
+      debug("Caught webapps install permissions error");
+      Cu.reportError(ex);
+      this.uninstall(aData);
+    }
+   },
+
   checkForUpdate: function(aData, aMm) {
     let app = this.getAppByManifestURL(aData.manifestURL);
     if (!app) {
       aData.error = "NO_SUCH_APP";
       aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
       return;
     }
 
@@ -729,34 +924,37 @@ let DOMApplicationRegistry = {
         dir.remove(true);
       } catch(e) {
       }
     }
     aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
   },
 
   confirmInstall: function(aData, aFromSync, aProfileDir, aOfflineCacheObserver) {
+    let isReinstall = false;
     let app = aData.app;
     app.removable = true;
 
     let origin = Services.io.newURI(app.origin, null, null);
     let manifestURL = origin.resolve(app.manifestURL);
 
     let id = app.syncId || this._appId(app.origin);
     let localId = this.getAppLocalIdByManifestURL(manifestURL);
 
     // Installing an application again is considered as an update.
     if (id) {
+      isReinstall = true;
       let dir = this._getAppDir(id);
       try {
         dir.remove(true);
       } catch(e) {
       }
     } else {
       id = this.makeAppId();
+      app.id = id;
       localId = this._nextLocalId();
     }
 
     let manifestName = "manifest.webapp";
     if (aData.isPackage) {
       // Override the origin with the correct id.
       app.origin = "app://" + id;
 
@@ -769,17 +967,16 @@ let DOMApplicationRegistry = {
     appObject.appStatus = app.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
     appObject.installTime = app.installTime = Date.now();
     appObject.lastUpdateCheck = app.lastUpdateCheck = Date.now();
     let appNote = JSON.stringify(appObject);
     appNote.id = id;
 
     appObject.localId = localId;
     appObject.basePath = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true).path;
-
     let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
     let manFile = dir.clone();
     manFile.append(manifestName);
     let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
     this._writeFile(manFile, JSON.stringify(jsonManifest), function() { });
 
     let manifest = new DOMApplicationManifest(jsonManifest, app.origin);
 
@@ -800,16 +997,17 @@ let DOMApplicationRegistry = {
       appObject.downloadAvailable = false;
       appObject.downloading = false;
       appObject.readyToApplyDownload = false;
     }
 
     appObject.name = manifest.name;
 
     this.webapps[id] = appObject;
+    this.installPermissions(appObject, aData, isReinstall);
     ["installState", "downloadAvailable",
      "downloading", "downloadSize", "readyToApplyDownload"].forEach(function(aProp) {
       aData.app[aProp] = appObject[aProp];
      });
 
     if (!aFromSync)
       this._saveApps((function() {
         this.broadcastMessage("Webapps:Install:Return:OK", aData);
@@ -922,35 +1120,16 @@ let DOMApplicationRegistry = {
                                 error: aError});
     }
 
     function getInferedStatus() {
       // XXX Update once we have digital signatures (bug 772365)
       return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
     }
 
-    function getAppManifestStatus(aManifest) {
-      let type = aManifest.type || "web";
-      let manifestStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
-
-      switch(type) {
-        case "web":
-          manifestStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
-          break;
-        case "privileged":
-          manifestStatus = Ci.nsIPrincipal.APP_STATUS_PRIVILEGED;
-          break
-        case "certified":
-          manifestStatus = Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
-          break;
-      }
-
-      return manifestStatus;
-    }
-
     function getAppStatus(aManifest) {
       let manifestStatus = getAppManifestStatus(aManifest);
       let inferedStatus = getInferedStatus();
 
       return (Services.prefs.getBoolPref("dom.mozApps.dev_mode") ? manifestStatus
                                                                 : inferedStatus);
     }
     // Returns true if the privilege level from the manifest
@@ -1278,16 +1457,17 @@ let DOMApplicationRegistry = {
   },
 
   updateApps: function(aRecords, aCallback) {
     for (let i = 0; i < aRecords.length; i++) {
       let record = aRecords[i];
       if (record.hidden) {
         if (!this.webapps[record.id] || !this.webapps[record.id].removable)
           continue;
+
         let origin = this.webapps[record.id].origin;
         delete this.webapps[record.id];
         let dir = this._getAppDir(record.id);
         try {
           dir.remove(true);
         } catch (e) {
         }
         this.broadcastMessage("Webapps:Uninstall:Return:OK", { origin: origin });
@@ -1581,16 +1761,23 @@ DOMApplicationManifest.prototype = {
   get package_path() {
     return this._localeProp("package_path");
   },
 
   get size() {
     return this._manifest["size"] || 0;
   },
 
+  get permissions() {
+    if (this._manifest.permissions) {
+      return this._manifest.permissions;
+    }
+    return {};
+  },
+
   iconURLForSize: function(aSize) {
     let icons = this._localeProp("icons");
     if (!icons)
       return null;
     let dist = 100000;
     let icon = null;
     for (let size in icons) {
       let iSize = parseInt(size);
--- a/dom/permission/PermissionSettings.js
+++ b/dom/permission/PermissionSettings.js
@@ -1,19 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-let DEBUG = 0;
-if (DEBUG)
-  debug = function (s) { dump("-*- PermissionSettings: " + s + "\n"); }
-else
-  debug = function (s) {}
+function debug(aMsg) {
+  // dump("-*- PermissionSettings.js: " + aMsg + "\n");
+}
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
@@ -40,17 +38,17 @@ PermissionSettings.prototype = {
     let uri = Services.io.newURI(aOrigin, null, null);
     let appID = appsService.getAppLocalIdByManifestURL(aManifestURL);
     let principal = secMan.getAppCodebasePrincipal(uri, appID, aBrowserFlag);
     let result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission);
 
     switch (result)
     {
       case Ci.nsIPermissionManager.UNKNOWN_ACTION:
-        return "unknown"
+        return "unknown";
       case Ci.nsIPermissionManager.ALLOW_ACTION:
         return "allow";
       case Ci.nsIPermissionManager.DENY_ACTION:
         return "deny";
       case Ci.nsIPermissionManager.PROMPT_ACTION:
         return "prompt";
       default:
         dump("Unsupported PermissionSettings Action!\n");
--- a/dom/permission/PermissionSettings.jsm
+++ b/dom/permission/PermissionSettings.jsm
@@ -5,17 +5,17 @@
 "use strict";
 
 let DEBUG = 0;
 if (DEBUG)
   debug = function (s) { dump("-*- PermissionSettings Module: " + s + "\n"); }
 else
   debug = function (s) {}
 
-const Cu = Components.utils; 
+const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 let EXPORTED_SYMBOLS = ["PermissionSettingsModule"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
--- a/dom/tests/browser/Makefile.in
+++ b/dom/tests/browser/Makefile.in
@@ -17,9 +17,23 @@ MOCHITEST_BROWSER_FILES := \
   browser_ConsoleAPITests.js \
   test-console-api.html \
   browser_ConsoleStorageAPITests.js \
   browser_ConsoleStoragePBTest.js \
   browser_autofocus_preference.js \
   browser_bug396843.js \
   $(NULL)
 
+ifeq (Linux,$(OS_ARCH))
+MOCHITEST_BROWSER_FILES += \
+  browser_webapps_permissions.js \
+  test-webapp.webapp \
+  test-webapp-reinstall.webapp \
+  test-webapp-original.webapp \
+  test-webapps-permissions.html \
+  $(NULL)
+endif
+
+
+# TODO: Re-enable permissions tests on Mac and Windows, bug 795334
+# TODO: disabled test browser_webapps_perms_reinstall.js, re-enable when bug 794920 is fixed
+
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_webapps_permissions.js
@@ -0,0 +1,152 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const DEBUG = 0;
+function log()
+{
+  if (DEBUG) {
+    let output = [];
+    for (let prop in arguments) {
+      output.push(arguments[prop]);
+    }
+    dump("-*- browser_webapps_permissions test: " + output.join(" ") + "\n");
+  }
+}
+
+let scope = {};
+Cu.import("resource://gre/modules/PermissionSettings.jsm", scope);
+
+const TEST_URL =
+  "http://mochi.test:8888/browser/dom/tests/browser/test-webapps-permissions.html";
+const TEST_MANIFEST_URL =
+  "http://mochi.test:8888/browser/dom/tests/browser/test-webapp.webapp";
+const TEST_ORIGIN_URL = "http://mochi.test:8888";
+
+const installedPermsToTest = {
+  "geolocation": "prompt",
+  "alarm": "allow",
+  "contacts-read": "deny",
+  "contacts-create": "deny",
+  "contacts-write": "deny",
+  "device-storage:apps": "deny",
+};
+
+const uninstalledPermsToTest = {
+  "geolocation": "unknown",
+  "alarm": "unknown",
+  "contacts-read": "unknown",
+  "contacts-create": "unknown",
+  "contacts-write": "unknown",
+  "device-storage:apps": "unknown",
+};
+
+var gWindow, gNavigator;
+
+function test() {
+  waitForExplicitFinish();
+
+  var tab = gBrowser.addTab(TEST_URL);
+  gBrowser.selectedTab = tab;
+  var browser = gBrowser.selectedBrowser;
+  PopupNotifications.panel.addEventListener("popupshown", handlePopup, false);
+
+  registerCleanupFunction(function () {
+    gWindow = null;
+    gBrowser.removeTab(tab);
+  });
+
+  browser.addEventListener("DOMContentLoaded", function onLoad(event) {
+    browser.removeEventListener("DOMContentLoaded", onLoad, false);
+    gWindow = browser.contentWindow;
+
+    SpecialPowers.setBoolPref("dom.mozApps.dev_mode", true);
+    SpecialPowers.setBoolPref("dom.mozPermissionSettings.enabled", true);
+    SpecialPowers.addPermission("permissions", true, browser.contentWindow.document);
+    SpecialPowers.addPermission("permissions", true, browser.contentDocument);
+
+    executeSoon(function (){
+      gWindow.focus();
+      var nav = XPCNativeWrapper.unwrap(browser.contentWindow.navigator);
+      ok(nav.mozApps, "we have a mozApps property");
+      var navMozPerms = nav.mozPermissionSettings;
+      ok(navMozPerms, "mozPermissions is available");
+      Math.sin(0);
+      // INSTALL app
+      var pendingInstall = nav.mozApps.install(TEST_MANIFEST_URL, null);
+      pendingInstall.onsuccess = function onsuccess()
+      {
+        ok(this.result, "we have a result: " + this.result);
+
+        function testPerm(aPerm, aAccess)
+        {
+          var res =
+            navMozPerms.get(aPerm, TEST_MANIFEST_URL, TEST_ORIGIN_URL, false);
+          is(res, aAccess, "install: " + aPerm + " is " + res);
+        }
+
+        for (let permName in installedPermsToTest) {
+          testPerm(permName, installedPermsToTest[permName]);
+        }
+        // uninstall checks
+        uninstallApp();
+      };
+
+      pendingInstall.onerror = function onerror(e)
+      {
+        ok(false, "install()'s onerror was called: " + e);
+        ok(false, "All permission checks failed, uninstal tests were not run");
+      };
+    });
+  }, false);
+}
+
+function uninstallApp()
+{
+  var browser = gBrowser.selectedBrowser;
+  var nav = XPCNativeWrapper.unwrap(browser.contentWindow.navigator);
+  var navMozPerms = nav.mozPermissionSettings;
+
+  var pending = nav.mozApps.getInstalled();
+  pending.onsuccess = function onsuccess() {
+    var m = this.result;
+    for (var i = 0; i < m.length; i++) {
+      var app = m[i];
+
+      function uninstall() {
+        var pendingUninstall = app.uninstall();
+
+        pendingUninstall.onsuccess = function(r) {
+          // test to make sure all permissions have been removed
+          function testPerm(aPerm, aAccess)
+          {
+            var res =
+              navMozPerms.get(aPerm, TEST_MANIFEST_URL, TEST_ORIGIN_URL, false);
+            is(res, aAccess, "uninstall: " + aPerm + " is " + res);
+          }
+
+          for (let permName in uninstalledPermsToTest) {
+            testPerm(permName, uninstalledPermsToTest[permName]);
+          }
+
+          finish();
+        };
+
+        pending.onerror = function _onerror(e) {
+          ok(false, e);
+          ok(false, "All uninstall() permission checks failed!");
+
+          finish();
+        };
+      };
+      uninstall();
+    }
+  };
+}
+
+function handlePopup(aEvent)
+{
+  aEvent.target.removeEventListener("popupshown", handlePopup, false);
+  SpecialPowers.wrap(this).childNodes[0].button.doCommand();
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_webapps_perms_reinstall.js
@@ -0,0 +1,183 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let scope = {};
+Cu.import("resource://gre/modules/PermissionSettings.jsm", scope);
+
+const TEST_URL =
+  "http://mochi.test:8888/browser/dom/tests/browser/test-webapps-permissions.html";
+const TEST_MANIFEST_URL =
+  "http://mochi.test:8888/browser/dom/tests/browser/test-webapp.webapp";
+const TEST_ORIGIN_URL = "http://mochi.test:8888";
+
+const installedPermsToTest = {
+  "geolocation": "prompt",
+  "alarm": "allow",
+  "contacts-read": "deny",
+  "contacts-create": "deny",
+  "contacts-write": "deny",
+  "device-storage:apps": "deny",
+};
+
+const reinstalledPermsToTest = {
+  "geolocation": "prompt",
+  "alarm": "unknown",
+  "contacts-read": "deny",
+  "contacts-create": "deny",
+  "contacts-write": "deny",
+  "device-storage:apps": "deny",
+};
+
+var gWindow, gNavigator;
+
+function test() {
+  waitForExplicitFinish();
+
+  var tab = gBrowser.addTab(TEST_URL);
+  gBrowser.selectedTab = tab;
+  var browser = gBrowser.selectedBrowser;
+  PopupNotifications.panel.addEventListener("popupshown", handlePopup, false);
+
+  registerCleanupFunction(function () {
+    gWindow = null;
+    gBrowser.removeTab(tab);
+  });
+
+  browser.addEventListener("DOMContentLoaded", function onLoad(event) {
+    browser.removeEventListener("DOMContentLoaded", onLoad, false);
+    gWindow = browser.contentWindow;
+    SpecialPowers.setBoolPref("dom.mozApps.dev_mode", true);
+    SpecialPowers.setBoolPref("dom.mozPermissionSettings.enabled", true);
+    SpecialPowers.addPermission("permissions", true, browser.contentWindow.document);
+    SpecialPowers.addPermission("permissions", true, browser.contentDocument);
+
+    let pendingInstall;
+
+    function testInstall() {
+      var nav = XPCNativeWrapper.unwrap(browser.contentWindow.navigator);
+      ok(nav.mozApps, "we have a mozApps property");
+      var navMozPerms = nav.mozPermissionSettings;
+      ok(navMozPerms, "mozPermissions is available");
+
+      // INSTALL app
+      pendingInstall = nav.mozApps.install(TEST_MANIFEST_URL, null);
+      pendingInstall.onsuccess = function onsuccess()
+      {
+        ok(this.result, "we have a result: " + this.result);
+        function testPerm(aPerm, aAccess)
+        {
+          var res =
+            navMozPerms.get(aPerm, TEST_MANIFEST_URL, TEST_ORIGIN_URL, false);
+          is(res, aAccess, "install: " + aPerm + " is " + res);
+        }
+
+        for (let permName in installedPermsToTest) {
+          testPerm(permName, installedPermsToTest[permName]);
+        }
+
+        writeUpdatesToWebappManifest();
+      };
+
+      pendingInstall.onerror = function onerror(e)
+      {
+        ok(false, "install()'s onerror was called: " + e);
+        ok(false, "All permission checks failed, reinstall tests were not run");
+      };
+    }
+    testInstall();
+  }, false);
+}
+
+function reinstallApp()
+{
+  var browser = gBrowser.selectedBrowser;
+  var nav = XPCNativeWrapper.unwrap(browser.contentWindow.navigator);
+  var navMozPerms = nav.mozPermissionSettings;
+
+  var pendingReinstall = nav.mozApps.install(TEST_MANIFEST_URL);
+  pendingReinstall.onsuccess = function onsuccess()
+  {
+    ok(this.result, "we have a result: " + this.result);
+
+    function testPerm(aPerm, aAccess)
+    {
+      var res =
+        navMozPerms.get(aPerm, TEST_MANIFEST_URL, TEST_ORIGIN_URL, false);
+        is(res, aAccess, "reinstall: " + aPerm + " is " + res);
+      }
+
+      for (let permName in reinstalledPermsToTest) {
+        testPerm(permName, reinstalledPermsToTest[permName]);
+      }
+      writeUpdatesToWebappManifest();
+      finish();
+  };
+};
+
+var qtyPopups = 0;
+
+function handlePopup(aEvent)
+{
+  qtyPopups++;
+  if (qtyPopups == 2) {
+    aEvent.target.removeEventListener("popupshown", handlePopup, false);
+  }
+  SpecialPowers.wrap(this).childNodes[0].button.doCommand();
+}
+
+function writeUpdatesToWebappManifest(aRestore)
+{
+  let newfile = Cc["@mozilla.org/file/directory_service;1"].
+                  getService(Ci.nsIProperties).
+                  get("XCurProcD", Ci.nsIFile);
+
+  let devPath = ["_tests", "testing", "mochitest",
+                 "browser", "dom" , "tests", "browser"];
+  // make package-tests moves tests to:
+  // dist/test-package-stage/mochitest/browser/dom/tests/browser
+  let slavePath = ["dist", "test-package-stage", "mochitest",
+                   "browser", "dom", "tests", "browser"];
+
+  newfile = newfile.parent; // up to dist/
+  newfile = newfile.parent;// up to obj-dir/
+
+  info(newfile.path);
+  try {
+    for (let idx in devPath) {
+      newfile.append(devPath[idx]);
+      if (!newfile.isDirectory()) {
+        info("*** NOT RUNNING ON DEV MACHINE\n\n");
+        throw new Error("Test is not running on dev machine, try dev path");
+      }
+    }
+  }
+  catch (ex) {
+    info(ex + "\n\n");
+    for (let idx in slavePath) {
+      newfile.append(slavePath[idx]);
+      if (!newfile.isDirectory()) {
+        info("*** NOT RUNNING ON BUILD SLAVE\n\n");
+        throw new Error("Test is not running on slave machine, abort test");
+      }
+    }
+  }
+
+  info("Final path: " + newfile.path);
+
+  if (aRestore) {
+    newfile.append("test-webapp-original.webapp");
+  } else {
+    newfile.append("test-webapp-reinstall.webapp");
+  }
+
+  let oldfile = newfile.parent;
+  oldfile.append("test-webapp.webapp");
+
+  newfile.copyTo(null, "test-webapp.webapp");
+
+  if (!aRestore) {
+    executeSoon(function (){ reinstallApp(); });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/test-webapp-original.webapp
@@ -0,0 +1,20 @@
+{
+  "name": "Super Crazy Basic App",
+  "installs_allowed_from": [ "*" ],
+  "type": "privileged",
+  "permissions": {
+    "geolocation": {
+      "description": "geolocate"
+    },
+    "alarm" : {
+      "description": "alarm"
+    },
+    "contacts": {
+      "description": "contacts",
+      "access": "readwrite"
+    },
+    "device-storage:apps": {
+      "description": "storage"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/test-webapp-reinstall.webapp
@@ -0,0 +1,17 @@
+{
+  "name": "Super Crazy Basic App",
+  "installs_allowed_from": [ "*" ],
+  "type": "privileged",
+  "permissions": {
+    "geolocation": {
+      "description": "geolocate"
+    },
+    "contacts": {
+      "description": "contacts",
+      "access": "read"
+    },
+    "device-storage:apps": {
+      "description": "storage"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/test-webapp.webapp
@@ -0,0 +1,20 @@
+{
+  "name": "Super Crazy Basic App",
+  "installs_allowed_from": [ "*" ],
+  "type": "privileged",
+  "permissions": {
+    "geolocation": {
+      "description": "geolocate"
+    },
+    "alarm" : {
+      "description": "alarm"
+    },
+    "contacts": {
+      "description": "contacts",
+      "access": "readwrite"
+    },
+    "device-storage:apps": {
+      "description": "storage"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/test-webapps-permissions.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+  <head>
+    <title>Webapps permissions test page</title>
+  </head>
+  <body>
+    <h1>Webapps permissions</h1>
+  </body>
+</html>