Bug 853356 - Part 1: Add new permissions and prompt for mutiple permissions request. r=fabrice, felipc, wjohnston, mrbkap, jimm
authorAlfredo Yang <ayang@mozilla.com>
Sat, 26 Oct 2013 21:19:32 -0400
changeset 166189 7fe2c77ebbf2f0585a847694f708e732bb8fcee0
parent 166188 a32f9f36d344db32b683f27eb79871fac773a47b
child 166190 0f687f920370eea477f2af3e8cbe2448741d6d0f
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, felipc, wjohnston, mrbkap, jimm
bugs853356
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 853356 - Part 1: Add new permissions and prompt for mutiple permissions request. r=fabrice, felipc, wjohnston, mrbkap, jimm
b2g/components/ContentPermissionPrompt.js
browser/components/nsBrowserGlue.js
browser/metro/base/content/helperui/IndexedDB.js
browser/metro/components/ContentPermissionPrompt.js
dom/apps/src/PermissionsTable.jsm
mobile/android/components/ContentPermissionPrompt.js
testing/specialpowers/content/MockPermissionPrompt.jsm
webapprt/ContentPermission.js
--- a/b2g/components/ContentPermissionPrompt.js
+++ b/b2g/components/ContentPermissionPrompt.js
@@ -1,28 +1,31 @@
 /* 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"
 
 function debug(str) {
-  //dump("-*- ContentPermissionPrompt: " + s + "\n");
+  //dump("-*- ContentPermissionPrompt: " + str + "\n");
 }
 
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 const Cc = Components.classes;
 
-const PROMPT_FOR_UNKNOWN    = ["geolocation", "desktop-notification",
-                               "audio-capture"];
+const PROMPT_FOR_UNKNOWN = ["audio-capture",
+                            "desktop-notification",
+                            "geolocation",
+                            "video-capture"];
 // Due to privary issue, permission requests like GetUserMedia should prompt
 // every time instead of providing session persistence.
-const PERMISSION_NO_SESSION = ["audio-capture"];
+const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"];
+const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
 Cu.import("resource://gre/modules/PermissionsTable.jsm");
 
@@ -36,206 +39,311 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/permissionSettings;1",
                                    "nsIDOMPermissionSettings");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "AudioManager",
                                    "@mozilla.org/telephony/audiomanager;1",
                                    "nsIAudioManager");
 
-function rememberPermission(aPermission, aPrincipal, aSession)
+/**
+ * aTypesInfo is an array of {permission, access, action, deny} which keeps
+ * the information of each permission. This arrary is initialized in
+ * ContentPermissionPrompt.prompt and used among functions.
+ *
+ * aTypesInfo[].permission : permission name
+ * aTypesInfo[].access     : permission name + request.access
+ * aTypesInfo[].action     : the default action of this permission
+ * aTypesInfo[].deny       : true if security manager denied this app's origin
+ *                           principal.
+ * Note:
+ *   aTypesInfo[].permission will be sent to prompt only when
+ *   aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false.
+ */
+function rememberPermission(aTypesInfo, aPrincipal, aSession)
 {
   function convertPermToAllow(aPerm, aPrincipal)
   {
     let type =
       permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm);
     if (type == Ci.nsIPermissionManager.PROMPT_ACTION ||
         (type == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
-        PROMPT_FOR_UNKNOWN.indexOf(aPermission) >= 0)) {
+        PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0)) {
+      debug("add " + aPerm + " to permission manager with ALLOW_ACTION");
       if (!aSession) {
         permissionManager.addFromPrincipal(aPrincipal,
                                            aPerm,
                                            Ci.nsIPermissionManager.ALLOW_ACTION);
-      } else if (PERMISSION_NO_SESSION.indexOf(aPermission) < 0) {
+      } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) {
         permissionManager.addFromPrincipal(aPrincipal,
                                            aPerm,
                                            Ci.nsIPermissionManager.ALLOW_ACTION,
                                            Ci.nsIPermissionManager.EXPIRE_SESSION, 0);
       }
     }
   }
 
-  // Expand the permission to see if we have multiple access properties to convert
-  let access = PermissionsTable[aPermission].access;
-  if (access) {
-    for (let idx in access) {
-      convertPermToAllow(aPermission + "-" + access[idx], aPrincipal);
+  for (let i in aTypesInfo) {
+    // Expand the permission to see if we have multiple access properties
+    // to convert
+    let perm = aTypesInfo[i].permission;
+    let access = PermissionsTable[perm].access;
+    if (access) {
+      for (let idx in access) {
+        convertPermToAllow(perm + "-" + access[idx], aPrincipal);
+      }
+    } else {
+      convertPermToAllow(perm, aPrincipal);
     }
-  } else {
-    convertPermToAllow(aPermission, aPrincipal);
   }
 }
 
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
 
-  handleExistingPermission: function handleExistingPermission(request) {
-    let access = (request.access && request.access !== "unused") ? request.type + "-" + request.access :
-                                                                   request.type;
-    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, access);
-    if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+  handleExistingPermission: function handleExistingPermission(request,
+                                                              typesInfo) {
+    typesInfo.forEach(function(type) {
+      type.action =
+        Services.perms.testExactPermissionFromPrincipal(request.principal,
+                                                        type.access);
+    });
+
+    // If all permissions are allowed already, call allow() without prompting.
+    let checkAllowPermission = function(type) {
+      if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION) {
+        return true;
+      }
+      return false;
+    }
+    if (typesInfo.every(checkAllowPermission)) {
+      debug("all permission requests are allowed");
       request.allow();
       return true;
     }
-    if (result == Ci.nsIPermissionManager.DENY_ACTION ||
-        result == Ci.nsIPermissionManager.UNKNOWN_ACTION && PROMPT_FOR_UNKNOWN.indexOf(access) < 0) {
+
+    // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel()
+    // without prompting.
+    let checkDenyPermission = function(type) {
+      if (type.action == Ci.nsIPermissionManager.DENY_ACTION ||
+          type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
+          PROMPT_FOR_UNKNOWN.indexOf(type.access) < 0) {
+        return true;
+      }
+      return false;
+    }
+    if (typesInfo.every(checkDenyPermission)) {
+      debug("all permission requests are denied");
       request.cancel();
       return true;
     }
     return false;
   },
 
-  handledByApp: function handledByApp(request) {
+  // multiple requests should be audio and video
+  checkMultipleRequest: function checkMultipleRequest(typesInfo) {
+    if (typesInfo.length == 1) {
+      return true;
+    } else if (typesInfo.length > 1) {
+      let checkIfAllowMultiRequest = function(type) {
+        return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1);
+      }
+      if (typesInfo.every(checkIfAllowMultiRequest)) {
+        debug("legal multiple requests");
+        return true;
+      }
+    }
+
+    return false;
+  },
+
+  handledByApp: function handledByApp(request, typesInfo) {
     if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
         request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
       // This should not really happen
       request.cancel();
       return true;
     }
 
     let appsService = Cc["@mozilla.org/AppsService;1"]
                         .getService(Ci.nsIAppsService);
     let app = appsService.getAppByLocalId(request.principal.appId);
 
-    let url = Services.io.newURI(app.origin, null, null);
-    let principal = secMan.getAppCodebasePrincipal(url, request.principal.appId,
-                                                   /*mozbrowser*/false);
-    let access = (request.access && request.access !== "unused") ? request.type + "-" + request.access :
-                                                                   request.type;
-    let result = Services.perms.testExactPermissionFromPrincipal(principal, access);
+    // Check each permission if it's denied by permission manager with app's
+    // URL.
+    let checkIfDenyAppPrincipal = function(type) {
+      let url = Services.io.newURI(app.origin, null, null);
+      let principal = secMan.getAppCodebasePrincipal(url,
+                                                     request.principal.appId,
+                                                     /*mozbrowser*/false);
+      let result = Services.perms.testExactPermissionFromPrincipal(principal,
+                                                                   type.access);
 
-    if (result == Ci.nsIPermissionManager.ALLOW_ACTION ||
-        result == Ci.nsIPermissionManager.PROMPT_ACTION) {
-      return false;
+      if (result == Ci.nsIPermissionManager.ALLOW_ACTION ||
+          result == Ci.nsIPermissionManager.PROMPT_ACTION) {
+        type.deny = false;
+      }
+      return type.deny;
+    }
+    if (typesInfo.every(checkIfDenyAppPrincipal)) {
+      request.cancel();
+      return true;
     }
 
-    request.cancel();
-    return true;
+    return false;
   },
 
-  handledByPermissionType: function handledByPermissionType(request) {
-    return permissionSpecificChecker.hasOwnProperty(request.type)
-             ? permissionSpecificChecker[request.type](request)
-             : false;
+  handledByPermissionType: function handledByPermissionType(request, typesInfo) {
+    for (let i in typesInfo) {
+      if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) &&
+          permissionSpecificChecker[typesInfo[i].permission](request)) {
+        return true;
+      }
+    }
+
+    return false;
   },
 
   _id: 0,
   prompt: function(request) {
     if (secMan.isSystemPrincipal(request.principal)) {
       request.allow();
-      return true;
+      return;
     }
 
-    if (this.handledByApp(request) ||
-        this.handledByPermissionType(request)) {
+    // Initialize the typesInfo and set the default value.
+    let typesInfo = [];
+    let perms = request.types.QueryInterface(Ci.nsIArray);
+    for (let idx = 0; idx < perms.length; idx++) {
+      let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType);
+      let tmp = {
+        permission: perm.type,
+        access: (perm.access && perm.access !== "unused") ?
+                  perm.type + "-" + perm.access : perm.type,
+        deny: true,
+        action: Ci.nsIPermissionManager.UNKNOWN_ACTION
+      };
+      typesInfo.push(tmp);
+    }
+    if (typesInfo.length == 0) {
+      request.cancel();
+      return;
+    }
+
+    if(!this.checkMultipleRequest(typesInfo)) {
+      request.cancel();
+      return;
+    }
+
+    if (this.handledByApp(request, typesInfo) ||
+        this.handledByPermissionType(request, typesInfo)) {
       return;
     }
 
     // returns true if the request was handled
-    if (this.handleExistingPermission(request))
+    if (this.handleExistingPermission(request, typesInfo)) {
        return;
+    }
+
+    // prompt PROMPT_ACTION request only.
+    typesInfo.forEach(function(aType, aIndex) {
+      if (aType.action != Ci.nsIPermissionManager.PROMPT_ACTION || aType.deny) {
+        typesInfo.splice(aIndex);
+      }
+    });
 
     let frame = request.element;
     let requestId = this._id++;
 
     if (!frame) {
-      this.delegatePrompt(request, requestId);
+      this.delegatePrompt(request, requestId, typesInfo);
       return;
     }
 
     frame = frame.wrappedJSObject;
     var cancelRequest = function() {
       frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
       request.cancel();
     }
 
     var self = this;
     var onVisibilityChange = function(evt) {
       if (evt.detail.visible === true)
         return;
 
-      self.cancelPrompt(request, requestId);
+      self.cancelPrompt(request, requestId, typesInfo);
       cancelRequest();
     }
 
     // If the request was initiated from a hidden iframe
     // we don't forward it to content and cancel it right away
     let domRequest = frame.getVisible();
     domRequest.onsuccess = function gv_success(evt) {
       if (!evt.target.result) {
         cancelRequest();
         return;
       }
 
       // Monitor the frame visibility and cancel the request if the frame goes
       // away but the request is still here.
       frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange);
 
-      self.delegatePrompt(request, requestId, function onCallback() {
+      self.delegatePrompt(request, requestId, typesInfo, function onCallback() {
         frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
       });
     };
 
     // Something went wrong. Let's cancel the request just in case.
     domRequest.onerror = function gv_error() {
       cancelRequest();
     }
   },
 
-  cancelPrompt: function(request, requestId) {
-    this.sendToBrowserWindow("cancel-permission-prompt", request, requestId);
+  cancelPrompt: function(request, requestId, typesInfo) {
+    this.sendToBrowserWindow("cancel-permission-prompt", request, requestId,
+                             typesInfo);
   },
 
-  delegatePrompt: function(request, requestId, callback) {
-    let access = (request.access && request.access !== "unused") ? request.type + "-" + request.access :
-                                                                   request.type;
-    let principal = request.principal;
+  delegatePrompt: function(request, requestId, typesInfo, callback) {
 
-    this._permission = access;
-    this._uri = principal.URI.spec;
-    this._origin = principal.origin;
-
-    this.sendToBrowserWindow("permission-prompt", request, requestId, function(type, remember) {
+    this.sendToBrowserWindow("permission-prompt", request, requestId, typesInfo,
+                             function(type, remember) {
       if (type == "permission-allow") {
-        rememberPermission(request.type, principal, !remember);
+        rememberPermission(typesInfo, request.principal, !remember);
         if (callback) {
           callback();
         }
         request.allow();
         return;
       }
 
-      if (remember) {
-        Services.perms.addFromPrincipal(principal, access,
-                                        Ci.nsIPermissionManager.DENY_ACTION);
-      } else {
-        Services.perms.addFromPrincipal(principal, access,
-                                        Ci.nsIPermissionManager.DENY_ACTION,
-                                        Ci.nsIPermissionManager.EXPIRE_SESSION, 0);
+      let addDenyPermission = function(type) {
+        debug("add " + type.permission +
+              " to permission manager with DENY_ACTION");
+        if (remember) {
+          Services.perms.addFromPrincipal(request.principal, type.access,
+                                          Ci.nsIPermissionManager.DENY_ACTION);
+        } else {
+          Services.perms.addFromPrincipal(request.principal, type.access,
+                                          Ci.nsIPermissionManager.DENY_ACTION,
+                                          Ci.nsIPermissionManager.EXPIRE_SESSION,
+                                          0);
+        }
       }
+      typesInfo.forEach(addDenyPermission);
 
       if (callback) {
         callback();
       }
       request.cancel();
     });
   },
 
-  sendToBrowserWindow: function(type, request, requestId, callback) {
+  sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) {
     let browser = Services.wm.getMostRecentWindow("navigator:browser");
     let content = browser.getContentWindow();
     if (!content)
       return;
 
     if (callback) {
       content.addEventListener("mozContentEvent", function contentEvent(evt) {
         let detail = evt.detail;
@@ -248,20 +356,25 @@ ContentPermissionPrompt.prototype = {
     }
 
     let principal = request.principal;
     let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED;
     let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
                     principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED)
                     ? true
                     : request.remember;
+    let permissions = [];
+    for (let i in typesInfo) {
+      debug("prompt " + typesInfo[i].permission);
+      permissions.push(typesInfo[i].permission);
+    }
 
     let details = {
       type: type,
-      permission: request.type,
+      permissions: permissions,
       id: requestId,
       origin: principal.origin,
       isApp: isApp,
       remember: remember
     };
 
     if (!isApp) {
       browser.shell.sendChromeEvent(details);
@@ -284,11 +397,10 @@ ContentPermissionPrompt.prototype = {
       request.cancel();
       return true;
     } else {
       return false;
     }
   };
 })();
 
-
 //module initialization
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1959,53 +1959,61 @@ ContentPermissionPrompt.prototype = {
     }
 
     this._showPrompt(aRequest, message, "pointerLock", actions, "pointerLock",
                      "pointerLock-notification-icon", null);
   },
 
   prompt: function CPP_prompt(request) {
 
+    // Only allow exactly one permission rquest here.
+    let types = request.types.QueryInterface(Ci.nsIArray);
+    if (types.length != 1) {
+      request.cancel();
+      return;
+    }
+    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
     const kFeatureKeys = { "geolocation" : "geo",
                            "desktop-notification" : "desktop-notification",
                            "pointerLock" : "pointerLock",
                          };
 
     // Make sure that we support the request.
-    if (!(request.type in kFeatureKeys)) {
+    if (!(perm.type in kFeatureKeys)) {
         return;
     }
 
     var requestingPrincipal = request.principal;
     var requestingURI = requestingPrincipal.URI;
 
     // Ignore requests from non-nsIStandardURLs
     if (!(requestingURI instanceof Ci.nsIStandardURL))
       return;
 
     var autoAllow = false;
-    var permissionKey = kFeatureKeys[request.type];
+    var permissionKey = kFeatureKeys[perm.type];
     var result = Services.perms.testExactPermissionFromPrincipal(requestingPrincipal, permissionKey);
 
     if (result == Ci.nsIPermissionManager.DENY_ACTION) {
       request.cancel();
       return;
     }
 
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       autoAllow = true;
       // For pointerLock, we still want to show a warning prompt.
-      if (request.type != "pointerLock") {
+      if (perm.type != "pointerLock") {
         request.allow();
         return;
       }
     }
 
     // Show the prompt.
-    switch (request.type) {
+    switch (perm.type) {
     case "geolocation":
       this._promptGeo(request);
       break;
     case "desktop-notification":
       this._promptWebNotifications(request);
       break;
     case "pointerLock":
       this._promptPointerLock(request, autoAllow);
--- a/browser/metro/base/content/helperui/IndexedDB.js
+++ b/browser/metro/base/content/helperui/IndexedDB.js
@@ -39,33 +39,35 @@ let IndexedDB = {
     } else if (topic == this._quotaCancel) {
       payload.permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
       browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
       // XXX Need to actually save this?
       return;
     }
 
     let prompt = Cc["@mozilla.org/content-permission/prompt;1"].createInstance(Ci.nsIContentPermissionPrompt);
+    let types = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    types.appendElement({type: type, access: "unused"}, false);
 
     // If the user waits a long time before responding, we default to UNKNOWN_ACTION.
     let timeoutId = setTimeout(function() {
       payload.permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
       browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
       timeoutId = null;
     }, 30000);
  
     function checkTimeout() {
       if (timeoutId === null) return true;
       clearTimeout(timeoutId);
       timeoutId = null;
       return false;
     }
 
     prompt.prompt({
-      type: type,
+      types: types,
       uri: Services.io.newURI(payload.location, null, null),
       window: null,
       element: aMessage.target,
 
       cancel: function() {
         if (checkTimeout()) return;
         payload.permission = Ci.nsIPermissionManager.DENY_ACTION;
         browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
--- a/browser/metro/components/ContentPermissionPrompt.js
+++ b/browser/metro/components/ContentPermissionPrompt.js
@@ -51,78 +51,86 @@ ContentPermissionPrompt.prototype = {
       let requestingWindow = request.window.top;
       let windowID = request.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
       let browser = chromeWin.Browser.getBrowserForWindowId(windowID);
       return chromeWin.getNotificationBox(browser);
     }
     return chromeWin.Browser.getNotificationBox(request.element);
   },
 
-  handleExistingPermission: function handleExistingPermission(request) {
-    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, request.type);
+  handleExistingPermission: function handleExistingPermission(request, type) {
+    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, type);
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       request.allow();
       return true;
     }
     if (result == Ci.nsIPermissionManager.DENY_ACTION) {
       request.cancel();
       return true;
     }
     return false;
   },
 
   prompt: function(request) {
+    // Only allow exactly one permission rquest here.
+    let types = request.types.QueryInterface(Ci.nsIArray);
+    if (types.length != 1) {
+      request.cancel();
+      return;
+    }
+    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
     // returns true if the request was handled
-    if (this.handleExistingPermission(request))
+    if (this.handleExistingPermission(request, perm.type))
        return;
 
     let pm = Services.perms;
     let notificationBox = this.getNotificationBoxForRequest(request);
     let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
     
-    let notification = notificationBox.getNotificationWithValue(request.type);
+    let notification = notificationBox.getNotificationWithValue(perm.type);
     if (notification)
       return;
 
-    let entityName = kEntities[request.type];
-    let icon = kIcons[request.type] || "";
+    let entityName = kEntities[perm.type];
+    let icon = kIcons[perm.type] || "";
 
     let buttons = [{
       label: browserBundle.GetStringFromName(entityName + ".allow"),
       accessKey: "",
       callback: function(notification) {
         request.allow();
       }
     },
     {
       label: browserBundle.GetStringFromName("contentPermissions.alwaysForSite"),
       accessKey: "",
       callback: function(notification) {
-        Services.perms.addFromPrincipal(request.principal, request.type, Ci.nsIPermissionManager.ALLOW_ACTION);
+        Services.perms.addFromPrincipal(request.principal, perm.type, Ci.nsIPermissionManager.ALLOW_ACTION);
         request.allow();
       }
     },
     {
       label: browserBundle.GetStringFromName("contentPermissions.neverForSite"),
       accessKey: "",
       callback: function(notification) {
-        Services.perms.addFromPrincipal(request.principal, request.type, Ci.nsIPermissionManager.DENY_ACTION);
+        Services.perms.addFromPrincipal(request.principal, perm.type, Ci.nsIPermissionManager.DENY_ACTION);
         request.cancel();
       }
     }];
 
     let message = browserBundle.formatStringFromName(entityName + ".wantsTo",
                                                      [request.principal.URI.host], 1);
     let newBar = notificationBox.appendNotification(message,
-                                                    request.type,
+                                                    perm.type,
                                                     icon,
                                                     notificationBox.PRIORITY_WARNING_MEDIUM,
                                                     buttons);
 
-    if (request.type == "geolocation") {
+    if (perm.type == "geolocation") {
       // Add the "learn more" link.
       let link = newBar.ownerDocument.createElement("label");
       link.setAttribute("value", browserBundle.GetStringFromName("geolocation.learnMore"));
       link.setAttribute("class", "text-link notification-link");
       newBar.insertBefore(link, newBar.firstChild);
 
       let win = this.getChromeWindowForRequest(request);
       link.addEventListener("click", function() {
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -297,16 +297,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "audio-capture": {
                              app: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: PROMPT_ACTION
                            },
+                           "video-capture": {
+                             app: PROMPT_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: PROMPT_ACTION
+                           },
                          };
 
 /**
  * Append access modes to the permission name as suffixes.
  *   e.g. permission name 'contacts' with ['read', 'write'] =
  *   ['contacts-read', contacts-write']
  * @param string aPermName
  * @param array aAccess
--- a/mobile/android/components/ContentPermissionPrompt.js
+++ b/mobile/android/components/ContentPermissionPrompt.js
@@ -16,28 +16,28 @@ const kEntities = { "geolocation": "geol
 
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
   classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
-  handleExistingPermission: function handleExistingPermission(request, isApp) {
-    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, request.type);
+  handleExistingPermission: function handleExistingPermission(request, type, isApp) {
+    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, type);
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       request.allow();
       return true;
     }
     if (result == Ci.nsIPermissionManager.DENY_ACTION) {
       request.cancel();
       return true;
     }
 
-    if (isApp && (result == Ci.nsIPermissionManager.UNKNOWN_ACTION && !!kEntities[request.type])) {
+    if (isApp && (result == Ci.nsIPermissionManager.UNKNOWN_ACTION && !!kEntities[type])) {
       request.cancel();
       return true;
     }
 
     return false;
   },
 
   getChromeWindow: function getChromeWindow(aWindow) {
@@ -57,48 +57,56 @@ ContentPermissionPrompt.prototype = {
       return this.getChromeWindow(requestingWindow).wrappedJSObject;
     }
     return request.element.ownerDocument.defaultView;
   },
 
   prompt: function(request) {
     let isApp = request.principal.appId !== Ci.nsIScriptSecurityManager.NO_APP_ID && request.principal.appId !== Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID;
 
+    // Only allow exactly one permission rquest here.
+    let types = request.types.QueryInterface(Ci.nsIArray);
+    if (types.length != 1) {
+      request.cancel();
+      return;
+    }
+    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
     // Returns true if the request was handled
-    if (this.handleExistingPermission(request, isApp))
+    if (this.handleExistingPermission(request, perm.type, isApp))
        return;
 
     let chromeWin = this.getChromeForRequest(request);
     let tab = chromeWin.BrowserApp.getTabForWindow(request.window.top);
     if (!tab)
       return;
 
     let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
-    let entityName = kEntities[request.type];
+    let entityName = kEntities[perm.type];
 
     let buttons = [{
       label: browserBundle.GetStringFromName(entityName + ".allow"),
       callback: function(aChecked) {
         // If the user checked "Don't ask again", make a permanent exception
         if (aChecked) {
-          Services.perms.addFromPrincipal(request.principal, request.type, Ci.nsIPermissionManager.ALLOW_ACTION);
+          Services.perms.addFromPrincipal(request.principal, perm.type, Ci.nsIPermissionManager.ALLOW_ACTION);
         } else if (isApp || entityName == "desktopNotification") {
           // Otherwise allow the permission for the current session (if the request comes from an app or if it's a desktop-notification request)
-          Services.perms.addFromPrincipal(request.principal, request.type, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION);
+          Services.perms.addFromPrincipal(request.principal, perm.type, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION);
         }
 
         request.allow();
       }
     },
     {
       label: browserBundle.GetStringFromName(entityName + ".dontAllow"),
       callback: function(aChecked) {
         // If the user checked "Don't ask again", make a permanent exception
         if (aChecked)
-          Services.perms.addFromPrincipal(request.principal, request.type, Ci.nsIPermissionManager.DENY_ACTION);
+          Services.perms.addFromPrincipal(request.principal, perm.type, Ci.nsIPermissionManager.DENY_ACTION);
 
         request.cancel();
       }
     }];
 
     let requestor = chromeWin.BrowserApp.manifest ? "'" + chromeWin.BrowserApp.manifest.name + "'" : request.principal.URI.host;
     let message = browserBundle.formatStringFromName(entityName + ".ask", [requestor], 1);
     let options = { checkbox: browserBundle.GetStringFromName(entityName + ".dontAskAgain") };
--- a/testing/specialpowers/content/MockPermissionPrompt.jsm
+++ b/testing/specialpowers/content/MockPermissionPrompt.jsm
@@ -29,19 +29,28 @@ var newFactory = {
   },
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
 };
 
 this.MockPermissionPrompt = {
   init: function() {
     this.reset();
     if (!registrar.isCIDRegistered(newClassID)) {
-      oldClassID = registrar.contractIDToCID(CONTRACT_ID);
-      oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory);
-      registrar.unregisterFactory(oldClassID, oldFactory);
+      try {
+        oldClassID = registrar.contractIDToCID(CONTRACT_ID);
+        oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory);
+      } catch (ex) {
+        oldClassID = "";
+        oldFactory = null;
+        dump("TEST-INFO | can't get permission prompt registered component, " +
+            "assuming there is none");
+      }
+      if (oldFactory != "" && oldFactory != null) {
+        registrar.unregisterFactory(oldClassID, oldFactory);
+      }
       registrar.registerFactory(newClassID, "", CONTRACT_ID, newFactory);
     }
   },
   
   reset: function() {
   },
   
   cleanup: function() {
@@ -56,24 +65,27 @@ this.MockPermissionPrompt = {
 function MockPermissionPromptInstance() { };
 MockPermissionPromptInstance.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
   promptResult: Ci.nsIPermissionManager.UNKNOWN_ACTION,
 
   prompt: function(request) {
 
-    this.promptResult = Services.perms.testExactPermissionFromPrincipal(request.principal,
-                                                                        request.type);
-    if (this.promptResult == Ci.nsIPermissionManager.ALLOW_ACTION) {
-      request.allow();
+    let perms = request.types.QueryInterface(Ci.nsIArray);
+    for (let idx = 0; idx < perms.length; idx++) {
+      let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType);
+      if (Services.perms.testExactPermissionFromPrincipal(
+           request.principal, perm.type) != Ci.nsIPermissionManager.ALLOW_ACTION) {
+        request.cancel();
+        return;
+      }
     }
-    else {
-      request.cancel();
-    }
+
+    request.allow();
   }
 };
 
 // Expose everything to content. We call reset() here so that all of the relevant
 // lazy expandos get added.
 MockPermissionPrompt.reset();
 function exposeAll(obj) {
   var props = {};
--- a/webapprt/ContentPermission.js
+++ b/webapprt/ContentPermission.js
@@ -25,80 +25,88 @@ ContentPermission.prototype = {
       .QueryInterface(Ci.nsIDocShellTreeItem)
       .rootTreeItem
       .QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIDOMWindow)
       .QueryInterface(Ci.nsIDOMChromeWindow);
   },
 
   prompt: function(request) {
+    // Only allow exactly one permission rquest here.
+    let types = request.types.QueryInterface(Ci.nsIArray);
+    if (types.length != 1) {
+      request.cancel();
+      return;
+    }
+    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
     // Reuse any remembered permission preferences
     let result =
       Services.perms.testExactPermissionFromPrincipal(request.principal,
-                                                      request.type);
+                                                      perm.type);
 
     // We used to use the name "geo" for the geolocation permission, now we're
     // using "geolocation".  We need to check both to support existing
     // installations.
     if ((result == Ci.nsIPermissionManager.UNKNOWN_ACTION ||
          result == Ci.nsIPermissionManager.PROMPT_ACTION) &&
-        request.type == "geolocation") {
+        perm.type == "geolocation") {
       let geoResult = Services.perms.testExactPermission(request.principal.URI,
                                                          "geo");
       // We override the result only if the "geo" permission was allowed or
       // denied.
       if (geoResult == Ci.nsIPermissionManager.ALLOW_ACTION ||
           geoResult == Ci.nsIPermissionManager.DENY_ACTION) {
         result = geoResult;
       }
     }
 
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       request.allow();
       return;
     } else if (result == Ci.nsIPermissionManager.DENY_ACTION ||
                (result == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
-                UNKNOWN_FAIL.indexOf(request.type) >= 0)) {
+                UNKNOWN_FAIL.indexOf(perm.type) >= 0)) {
       request.cancel();
       return;
     }
 
     // Display a prompt at the top level
     let {name} = WebappRT.localeManifest;
     let requestingWindow = request.window.top;
     let chromeWin = this._getChromeWindow(requestingWindow);
     let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties");
 
     // Construct a prompt with share/don't and remember checkbox
     let remember = {value: false};
     let choice = Services.prompt.confirmEx(
       chromeWin,
-      bundle.formatStringFromName(request.type + ".title", [name], 1),
-      bundle.GetStringFromName(request.type + ".description"),
+      bundle.formatStringFromName(perm.type + ".title", [name], 1),
+      bundle.GetStringFromName(perm.type + ".description"),
       // Set both buttons to strings with the cancel button being default
       Ci.nsIPromptService.BUTTON_POS_1_DEFAULT |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_0 |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_1,
-      bundle.GetStringFromName(request.type + ".allow"),
-      bundle.GetStringFromName(request.type + ".deny"),
+      bundle.GetStringFromName(perm.type + ".allow"),
+      bundle.GetStringFromName(perm.type + ".deny"),
       null,
-      bundle.GetStringFromName(request.type + ".remember"),
+      bundle.GetStringFromName(perm.type + ".remember"),
       remember);
 
     let action = Ci.nsIPermissionManager.ALLOW_ACTION;
     if (choice != 0) {
       action = Ci.nsIPermissionManager.DENY_ACTION;
     }
 
     if (remember.value) {
       // Persist the choice if the user wants to remember
-      Services.perms.addFromPrincipal(request.principal, request.type, action);
+      Services.perms.addFromPrincipal(request.principal, perm.type, action);
     } else {
       // Otherwise allow the permission for the current session
-      Services.perms.addFromPrincipal(request.principal, request.type, action,
+      Services.perms.addFromPrincipal(request.principal, perm.type, action,
                                       Ci.nsIPermissionManager.EXPIRE_SESSION);
     }
 
     // Trigger the selected choice
     if (choice == 0) {
       request.allow();
     }
     else {