Bug 1297475 - Move content permission prompts into a JSM and add an Integration. r?paolo draft
authorMike Conley <mconley@mozilla.com>
Sat, 27 Aug 2016 23:11:07 -0400
changeset 416137 e43d53841ee90b917468528dbbbfd94db404720e
parent 414757 eaf5eb6f8fa0d8e7a09f3774c0da53c0dd6dadd7
child 416138 d4497d095a0caab125b1e938f3f902e10feebe29
push id30036
push usermconley@mozilla.com
push dateWed, 21 Sep 2016 14:50:38 +0000
reviewerspaolo
bugs1297475
milestone51.0a1
Bug 1297475 - Move content permission prompts into a JSM and add an Integration. r?paolo MozReview-Commit-ID: Dq3I9pzcdyY
browser/components/nsBrowserGlue.js
browser/modules/PermissionUI.jsm
browser/modules/moz.build
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -32,25 +32,27 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["CaptivePortalWatcher", "resource:///modules/CaptivePortalWatcher.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
   ["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
   ["Feeds", "resource:///modules/Feeds.jsm"],
   ["FileUtils", "resource://gre/modules/FileUtils.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
+  ["Integration", "resource://gre/modules/Integration.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["LoginHelper", "resource://gre/modules/LoginHelper.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NetUtil", "resource://gre/modules/NetUtil.jsm"],
   ["NewTabMessages", "resource:///modules/NewTabMessages.jsm"],
   ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
   ["OS", "resource://gre/modules/osfile.jsm"],
   ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
   ["PdfJs", "resource://pdf.js/PdfJs.jsm"],
+  ["PermissionUI", "resource:///modules/PermissionUI.jsm"],
   ["PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"],
   ["PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"],
   ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
   ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"],
   ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"],
   ["ReaderParent", "resource:///modules/ReaderParent.jsm"],
   ["RecentWindow", "resource:///modules/RecentWindow.jsm"],
   ["RemotePrompt", "resource:///modules/RemotePrompt.jsm"],
@@ -2424,318 +2426,110 @@ BrowserGlue.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference,
                                          Ci.nsIBrowserGlue]),
 
   // redefine the default factory for XPCOMUtils
   _xpcom_factory: BrowserGlueServiceFactory,
 }
 
+/**
+ * ContentPermissionIntegration is responsible for showing the user
+ * simple permission prompts when content requests additional
+ * capabilities.
+ *
+ * While there are some built-in permission prompts, createPermissionPrompt
+ * can also be overridden by system add-ons or tests to provide new ones.
+ *
+ * This override ability is provided by Integration.jsm. See
+ * PermissionUI.jsm for an example of how to provide a new prompt
+ * from an add-on.
+ */
+const ContentPermissionIntegration = {
+  /**
+   * Creates a PermissionPrompt for a given permission type and
+   * nsIContentPermissionRequest.
+   *
+   * @param {string} type
+   *        The type of the permission request from content. This normally
+   *        matches the "type" field of an nsIContentPermissionType, but it
+   *        can be something else if the permission does not use the
+   *        nsIContentPermissionRequest model. Note that this type might also
+   *        be different from the permission key used in the permissions
+   *        database.
+   *        Example: "geolocation"
+   * @param {nsIContentPermissionRequest} request
+   *        The request for a permission from content.
+   * @return {PermissionPrompt} (see PermissionUI.jsm),
+   *         or undefined if the type cannot be handled.
+   */
+  createPermissionPrompt(type, request) {
+    switch (type) {
+      case "geolocation": {
+        return new PermissionUI.GeolocationPermissionPrompt(request);
+      }
+      case "desktop-notification": {
+        return new PermissionUI.DesktopNotificationPermissionPrompt(request);
+      }
+    }
+    return undefined;
+  },
+};
+
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
   classID:          Components.ID("{d8903bf6-68d5-4e97-bcd1-e4d3012f721a}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
-  _getBrowserForRequest: function (aRequest) {
-    // "element" is only defined in e10s mode.
-    let browser = aRequest.element;
-    if (!browser) {
-      // Find the requesting browser.
-      browser = aRequest.window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIWebNavigation)
-                                  .QueryInterface(Ci.nsIDocShell)
-                                  .chromeEventHandler;
-    }
-    return browser;
-  },
-
   /**
-   * Show a permission prompt.
+   * This implementation of nsIContentPermissionPrompt.prompt ensures
+   * that there's only one nsIContentPermissionType in the request,
+   * and that it's of type nsIContentPermissionType. Failing to
+   * satisfy either of these conditions will result in this method
+   * throwing NS_ERRORs. If the combined ContentPermissionIntegration
+   * cannot construct a prompt for this particular request, an
+   * NS_ERROR_FAILURE will be thrown.
+   *
+   * Any time an error is thrown, the nsIContentPermissionRequest is
+   * cancelled automatically.
    *
-   * @param aRequest               The permission request.
-   * @param aMessage               The message to display on the prompt.
-   * @param aPermission            The type of permission to prompt.
-   * @param aActions               An array of actions of the form:
-   *                               [main action, secondary actions, ...]
-   *                               Actions are of the form { stringId, action, expireType, callback }
-   *                               Permission is granted if action is null or ALLOW_ACTION.
-   * @param aNotificationId        The id of the PopupNotification.
-   * @param aAnchorId              The id for the PopupNotification anchor.
-   * @param aOptions               Options for the PopupNotification
+   * @param {nsIContentPermissionRequest} request
+   *        The request that we're to show a prompt for.
    */
-  _showPrompt: function CPP_showPrompt(aRequest, aMessage, aPermission, aActions,
-                                       aNotificationId, aAnchorId, aOptions) {
-    var browser = this._getBrowserForRequest(aRequest);
-    var chromeWin = browser.ownerGlobal;
-    var requestPrincipal = aRequest.principal;
-
-    // Transform the prompt actions into PopupNotification actions.
-    var popupNotificationActions = [];
-    for (var i = 0; i < aActions.length; i++) {
-      let promptAction = aActions[i];
-
-      // Don't offer action in PB mode if the action remembers permission for more than a session.
-      if (PrivateBrowsingUtils.isWindowPrivate(chromeWin) &&
-          promptAction.expireType != Ci.nsIPermissionManager.EXPIRE_SESSION &&
-          promptAction.action) {
-        continue;
+  prompt(request) {
+    try {
+      // Only allow exactly one permission request here.
+      let types = request.types.QueryInterface(Ci.nsIArray);
+      if (types.length != 1) {
+        throw Components.Exception(
+          "Expected an nsIContentPermissionRequest with only 1 type.",
+          Cr.NS_ERROR_UNEXPECTED);
       }
 
-      var action = {
-        label: gBrowserBundle.GetStringFromName(promptAction.stringId),
-        accessKey: gBrowserBundle.GetStringFromName(promptAction.stringId + ".accesskey"),
-        callback: function() {
-          if (promptAction.callback) {
-            promptAction.callback();
-          }
-
-          // Remember permissions.
-          if (promptAction.action) {
-            Services.perms.addFromPrincipal(requestPrincipal, aPermission,
-                                            promptAction.action, promptAction.expireType);
-          }
-
-          // Grant permission if action is null or ALLOW_ACTION.
-          if (!promptAction.action || promptAction.action == Ci.nsIPermissionManager.ALLOW_ACTION) {
-            aRequest.allow();
-          } else {
-            aRequest.cancel();
-          }
-        },
-      };
-
-      popupNotificationActions.push(action);
-    }
-
-    var mainAction = popupNotificationActions.length ?
-                       popupNotificationActions[0] : null;
-    var secondaryActions = popupNotificationActions.splice(1);
-
-    // Only allow exactly one permission request here.
-    let types = aRequest.types.QueryInterface(Ci.nsIArray);
-    if (types.length != 1) {
-      aRequest.cancel();
-      return undefined;
-    }
-
-    if (!aOptions)
-      aOptions = {};
-    aOptions.displayURI = requestPrincipal.URI;
-
-    return chromeWin.PopupNotifications.show(browser, aNotificationId, aMessage, aAnchorId,
-                                             mainAction, secondaryActions, aOptions);
-  },
-
-  _promptGeo : function(aRequest) {
-    var secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
-
-    var message;
-
-    // Share location action.
-    var actions = [{
-      stringId: "geolocation.shareLocation",
-      action: null,
-      expireType: null,
-      callback: function() {
-        secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_SHARE_LOCATION);
-      },
-    }];
-
-    let options = {
-      learnMoreURL: Services.urlFormatter.formatURLPref("browser.geolocation.warning.infoURL"),
-    };
-
-    if (aRequest.principal.URI.schemeIs("file")) {
-      message = gBrowserBundle.GetStringFromName("geolocation.shareWithFile2");
-    } else {
-      message = gBrowserBundle.GetStringFromName("geolocation.shareWithSite2");
-      // Always share location action.
-      actions.push({
-        stringId: "geolocation.alwaysShareLocation",
-        action: Ci.nsIPermissionManager.ALLOW_ACTION,
-        expireType: null,
-        callback: function() {
-          secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_ALWAYS_SHARE);
-        },
-      });
-
-      // Never share location action.
-      actions.push({
-        stringId: "geolocation.neverShareLocation",
-        action: Ci.nsIPermissionManager.DENY_ACTION,
-        expireType: null,
-        callback: function() {
-          secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_NEVER_SHARE);
-        },
-      });
-    }
-
-    secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST);
-
-    this._showPrompt(aRequest, message, "geo", actions, "geolocation",
-                     "geo-notification-icon", options);
-  },
-
-  _promptFlyWebPublishServer : function(aRequest) {
-    var message = "Would you like to let this site start a server accessible to nearby devices and people?";
-    var actions = [
-      {
-        stringId: "flyWebPublishServer.allowPublishServer",
-        action: Ci.nsIPermissionManager.ALLOW_ACTION,
-        expireType: Ci.nsIPermissionManager.EXPIRE_SESSION
-      },
-      {
-        stringId: "flyWebPublishServer.denyPublishServer",
-        action: Ci.nsIPermissionManager.DENY_ACTION,
-        expireType: Ci.nsIPermissionManager.EXPIRE_SESSION
-      }
-    ];
-
-    let options = {
-      learnMoreURL: "https://flyweb.github.io",
-      popupIconURL: "chrome://flyweb/skin/icon-64.png"
-    };
+      let type = types.queryElementAt(0, Ci.nsIContentPermissionType).type;
+      let combinedIntegration =
+        Integration.contentPermission.getCombined(ContentPermissionIntegration);
 
-    let browser = this._getBrowserForRequest(aRequest);
-    let chromeDoc = browser.ownerDocument;
-    let iconElem = chromeDoc.getElementById("flyweb-publish-server-notification-icon");
-    if (!iconElem) {
-      let notificationPopupBox = chromeDoc.getElementById("notification-popup-box");
-      let notificationIcon = chromeDoc.createElement("image");
-      notificationIcon.setAttribute("id", "flyweb-publish-server-notification-icon");
-      notificationIcon.setAttribute("src", "chrome://flyweb/skin/icon-64.png");
-      notificationIcon.setAttribute("class", "notification-anchor-icon flyweb-publish-server-icon");
-      notificationIcon.setAttribute("style", "filter: url(chrome://browser/skin/filters.svg#fill); fill: currentColor; opacity: .4;");
-      notificationIcon.setAttribute("role", "button");
-      notificationIcon.setAttribute("aria-label", "View the publish-server request");
-      notificationPopupBox.appendChild(notificationIcon);
-    }
-
-    this._showPrompt(aRequest, message, "flyweb-publish-server", actions, "flyweb-publish-server",
-                     "flyweb-publish-server-notification-icon", options);
-  },
-
-  _promptWebNotifications : function(aRequest) {
-    var message = gBrowserBundle.GetStringFromName("webNotifications.receiveFromSite");
-
-    var actions;
-
-    var browser = this._getBrowserForRequest(aRequest);
-    // Only show "allow for session" in PB mode, we don't
-    // support "allow for session" in non-PB mode.
-    if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
-      actions = [
-        {
-          stringId: "webNotifications.receiveForSession",
-          action: Ci.nsIPermissionManager.ALLOW_ACTION,
-          expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
-          callback: function() {},
-        }
-      ];
-    } else {
-      actions = [
-        {
-          stringId: "webNotifications.alwaysReceive",
-          action: Ci.nsIPermissionManager.ALLOW_ACTION,
-          expireType: null,
-          callback: function() {},
-        },
-        {
-          stringId: "webNotifications.neverShow",
-          action: Ci.nsIPermissionManager.DENY_ACTION,
-          expireType: null,
-          callback: function() {},
-        },
-      ];
-    }
+      let permissionPrompt =
+        combinedIntegration.createPermissionPrompt(type, request);
+      if (!permissionPrompt) {
+        throw Components.Exception(
+          `Failed to handle permission of type ${type}`,
+          Cr.NS_ERROR_FAILURE);
+      }
 
-    var options = {
-      learnMoreURL:
-        Services.urlFormatter.formatURLPref("app.support.baseURL") + "push",
-      eventCallback(type) {
-        if (type == "dismissed") {
-          // Bug 1259148: Hide the doorhanger icon. Unlike other permission
-          // doorhangers, the user can't restore the doorhanger using the icon
-          // in the location bar. Instead, the site will be notified that the
-          // doorhanger was dismissed.
-          this.remove();
-          aRequest.cancel();
-        }
-      },
-    };
-
-    this._showPrompt(aRequest, message, "desktop-notification", actions,
-                     "web-notifications",
-                     "web-notifications-notification-icon", options);
-  },
-
-  prompt: function CPP_prompt(request) {
-    // Only allow exactly one permission request here.
-    let types = request.types.QueryInterface(Ci.nsIArray);
-    if (types.length != 1) {
+      permissionPrompt.prompt();
+    } catch (ex) {
+      Cu.reportError(ex);
       request.cancel();
-      return;
-    }
-    let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
-
-    const kFeatureKeys = { "geolocation" : "geo",
-                           "desktop-notification" : "desktop-notification",
-                           "flyweb-publish-server": "flyweb-publish-server"
-                         };
-
-    // Make sure that we support the request.
-    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 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) {
-      request.allow();
-      return;
-    }
-
-    var browser = this._getBrowserForRequest(request);
-    var chromeWin = browser.ownerGlobal;
-    if (!chromeWin.PopupNotifications)
-      // Ignore requests from browsers hosted in windows that don't support
-      // PopupNotifications.
-      return;
-
-    // Show the prompt.
-    switch (perm.type) {
-    case "geolocation":
-      this._promptGeo(request);
-      break;
-    case "desktop-notification":
-      this._promptWebNotifications(request);
-      break;
-    case "flyweb-publish-server":
-      if (AppConstants.NIGHTLY_BUILD) {
-        this._promptFlyWebPublishServer(request);
-      }
-      break;
+      throw ex;
     }
   },
-
 };
 
 var DefaultBrowserCheck = {
   get OPTIONPOPUP() { return "defaultBrowserNotificationPopup" },
   _setAsDefaultTimer: null,
   _setAsDefaultButtonClickStartTime: 0,
 
   closePrompt: function(aNode) {
new file mode 100644
--- /dev/null
+++ b/browser/modules/PermissionUI.jsm
@@ -0,0 +1,588 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+  "PermissionUI",
+];
+
+/**
+ * PermissionUI is responsible for exposing both a prototype
+ * PermissionPrompt that can be used by arbitrary browser
+ * components and add-ons, but also hosts the implementations of
+ * built-in permission prompts.
+ *
+ * If you're developing a feature that requires web content to ask
+ * for special permissions from the user, this module is for you.
+ *
+ * Suppose a system add-on wants to add a new prompt for a new request
+ * for getting more low-level access to the user's sound card, and the
+ * permission request is coming up from content by way of the
+ * nsContentPermissionHelper. The system add-on could then do the following:
+ *
+ * Cu.import("resource://gre/modules/Integration.jsm");
+ * Cu.import("resource:///modules/PermissionUI.jsm");
+ *
+ * const SoundCardIntegration = (base) => ({
+ *   __proto__: base,
+ *   createPermissionPrompt(type, request) {
+ *     if (type != "sound-api") {
+ *       return super.createPermissionPrompt(...arguments);
+ *     }
+ *
+ *     return {
+ *       __proto__: PermissionUI.PermissionPromptForRequestPrototype,
+ *       get permissionKey() {
+ *         return "sound-permission";
+ *       }
+ *       // etc - see the documentation for PermissionPrompt for
+ *       // a better idea of what things one can and should override.
+ *     }
+ *   },
+ * });
+ *
+ * // Add-on startup:
+ * Integration.contentPermission.register(SoundCardIntegration);
+ * // ...
+ * // Add-on shutdown:
+ * Integration.contentPermission.unregister(SoundCardIntegration);
+ *
+ * Note that PermissionPromptForRequestPrototype must be used as the
+ * prototype, since the prompt is wrapping an nsIContentPermissionRequest,
+ * and going through nsIContentPermissionPrompt.
+ *
+ * It is, however, possible to take advantage of PermissionPrompt without
+ * having to go through nsIContentPermissionPrompt or with a
+ * nsIContentPermissionRequest. The PermissionPromptPrototype can be
+ * imported, subclassed, and have prompt() called directly, without
+ * the caller having called into createPermissionPrompt.
+ */
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
+  return Services.strings
+                 .createBundle('chrome://branding/locale/brand.properties');
+});
+
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
+  return Services.strings
+                 .createBundle('chrome://browser/locale/browser.properties');
+});
+
+this.PermissionUI = {};
+
+/**
+ * PermissionPromptPrototype should be subclassed by callers that
+ * want to display prompts to the user. See each method and property
+ * below for guidance on what to override.
+ *
+ * Note that if you're creating a prompt for an
+ * nsIContentPermissionRequest, you'll want to subclass
+ * PermissionPromptForRequestPrototype instead.
+ */
+this.PermissionPromptPrototype = {
+  /**
+   * Returns the associated <xul:browser> for the request. This should
+   * work for the e10s and non-e10s case.
+   *
+   * Subclasses must override this.
+   *
+   * @return {<xul:browser>}
+   */
+  get browser() {
+    throw new Error("Not implemented.");
+  },
+
+  /**
+   * Returns the nsIPrincipal associated with the request.
+   *
+   * Subclasses must override this.
+   *
+   * @return {nsIPrincipal}
+   */
+  get principal() {
+    throw new Error("Not implemented.");
+  },
+
+  /**
+   * If the nsIPermissionManager is being queried and written
+   * to for this permission request, set this to the key to be
+   * used. If this is undefined, user permissions will not be
+   * read from or written to.
+   *
+   * Note that if a permission is set, in any follow-up
+   * prompting within the expiry window of that permission,
+   * the prompt will be skipped and the allow or deny choice
+   * will be selected automatically.
+   */
+  get permissionKey() {
+    return undefined;
+  },
+
+  /**
+   * These are the options that will be passed to the
+   * PopupNotification when it is shown. See the documentation
+   * for PopupNotification for more details.
+   *
+   * Note that prompt()  will automatically set displayURI to
+   * be the URI of the requesting pricipal.
+   */
+  get popupOptions() {
+    return {};
+  },
+
+  /**
+   * PopupNotification requires a unique ID to open the notification.
+   * You must return a unique ID string here, for which PopupNotification
+   * will then create a <xul:popupnotification> node with the ID
+   * "<notificationID>-notification".
+   *
+   * If there's a custom <xul:popupnotification> you're hoping to show,
+   * then you need to make sure its ID has the "-notification" suffix,
+   * and then return the prefix here.
+   *
+   * See PopupNotification.jsm for more details.
+   *
+   * @return {string}
+   *         The unique ID that will be used to as the
+   *         "<unique ID>-notification" ID for the <xul:popupnotification>
+   *         to use or create.
+   */
+  get notificationID() {
+    throw new Error("Not implemented.");
+  },
+
+  /**
+   * The ID of the element to anchor the PopupNotification to.
+   *
+   * @return {string}
+   */
+  get anchorID() {
+    return "default-notification-icon";
+  },
+
+  /**
+   * The message to show the user in the PopupNotification. This
+   * is usually a string describing the permission that is being
+   * requested.
+   *
+   * Subclasses must override this.
+   *
+   * @return {string}
+   */
+  get message() {
+    throw new Error("Not implemented.");
+  },
+
+  /**
+   * This will be called if the request is to be cancelled.
+   *
+   * Subclasses only need to override this if they provide a
+   * permissionKey.
+   */
+  cancel() {
+    throw new Error("Not implemented.")
+  },
+
+  /**
+   * This will be called if the request is to be allowed.
+   *
+   * Subclasses only need to override this if they provide a
+   * permissionKey.
+   */
+  allow() {
+    throw new Error("Not implemented.");
+  },
+
+  /**
+   * The actions that will be displayed in the PopupNotification
+   * via a dropdown menu. The first item in this array will be
+   * the default selection. Each action is an Object with the
+   * following properties:
+   *
+   *  label (string):
+   *    The label that will be displayed for this choice.
+   *  accessKey (string):
+   *    The access key character that will be used for this choice.
+   *  action (Ci.nsIPermissionManager action, optional)
+   *    The nsIPermissionManager action that will be associated with
+   *    this choice. For example, Ci.nsIPermissionManager.DENY_ACTION.
+   *
+   *    If omitted, the nsIPermissionManager will not be written to
+   *    when this choice is chosen.
+   *  expireType (Ci.nsIPermissionManager expiration policy, optional)
+   *    The nsIPermissionManager expiration policy that will be associated
+   *    with this choice. For example, Ci.nsIPermissionManager.EXPIRE_SESSION.
+   *
+   *    If action is not set, expireType will be ignored.
+   *  callback (function, optional)
+   *    A callback function that will fire if the user makes this choice.
+   */
+  get promptActions() {
+    return [];
+  },
+
+  /**
+   * If the prompt will be shown to the user, this callback will
+   * be called just before. Subclasses may want to override this
+   * in order to, for example, bump a counter Telemetry probe for
+   * how often a particular permission request is seen.
+   */
+  onBeforeShow() {},
+
+  /**
+   * Will determine if a prompt should be shown to the user, and if so,
+   * will show it.
+   *
+   * If a permissionKey is defined prompt() might automatically
+   * allow or cancel itself based on the user's current
+   * permission settings without displaying the prompt.
+   *
+   * If the <xul:browser> that the request is associated with
+   * does not belong to a browser window with the PopupNotifications
+   * global set, the prompt request is ignored.
+   */
+  prompt() {
+    let chromeWin = this.browser.ownerGlobal;
+    if (!chromeWin.PopupNotifications) {
+      return;
+    }
+
+    // We ignore requests from non-nsIStandardURLs
+    let requestingURI = this.principal.URI;
+    if (!(requestingURI instanceof Ci.nsIStandardURL)) {
+      return;
+    }
+
+    if (this.permissionKey) {
+      // If we're reading and setting permissions, then we need
+      // to check to see if we already have a permission setting
+      // for this particular principal.
+      let result =
+        Services.perms.testExactPermissionFromPrincipal(this.principal,
+                                                        this.permissionKey);
+
+      if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+        this.cancel();
+        return;
+      }
+
+      if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+        this.allow();
+        return;
+      }
+    }
+
+    // Transform the PermissionPrompt actions into PopupNotification actions.
+    let popupNotificationActions = [];
+    for (let promptAction of this.promptActions) {
+      // Don't offer action in PB mode if the action remembers permission
+      // for more than a session.
+      if (PrivateBrowsingUtils.isWindowPrivate(chromeWin) &&
+          promptAction.expireType != Ci.nsIPermissionManager.EXPIRE_SESSION &&
+          promptAction.action) {
+        continue;
+      }
+
+      let action = {
+        label: promptAction.label,
+        accessKey: promptAction.accessKey,
+        callback: () => {
+          if (promptAction.callback) {
+            promptAction.callback();
+          }
+
+          if (this.permissionKey) {
+            // Remember permissions.
+            if (promptAction.action) {
+              Services.perms.addFromPrincipal(this.principal,
+                                              this.permissionKey,
+                                              promptAction.action,
+                                              promptAction.expireType);
+            }
+
+            // Grant permission if action is null or ALLOW_ACTION.
+            if (!promptAction.action ||
+                promptAction.action == Ci.nsIPermissionManager.ALLOW_ACTION) {
+              this.allow();
+            } else {
+              this.cancel();
+            }
+          }
+        },
+      };
+
+      popupNotificationActions.push(action);
+    }
+
+    let mainAction = popupNotificationActions.length ?
+                     popupNotificationActions[0] : null;
+    let secondaryActions = popupNotificationActions.splice(1);
+
+    let options = this.popupOptions;
+    options.displayURI = this.principal.URI;
+
+    this.onBeforeShow();
+    chromeWin.PopupNotifications.show(this.browser,
+                                      this.notificationID,
+                                      this.message,
+                                      this.anchorID,
+                                      mainAction,
+                                      secondaryActions,
+                                      options);
+  },
+};
+
+PermissionUI.PermissionPromptPrototype = PermissionPromptPrototype;
+
+/**
+ * A subclass of PermissionPromptPrototype that assumes
+ * that this.request is an nsIContentPermissionRequest
+ * and fills in some of the required properties on the
+ * PermissionPrompt. For callers that are wrapping an
+ * nsIContentPermissionRequest, this should be subclassed
+ * rather than PermissionPromptPrototype.
+ */
+this.PermissionPromptForRequestPrototype = {
+  __proto__: PermissionPromptPrototype,
+
+  get browser() {
+    // In the e10s-case, the <xul:browser> will be at request.element.
+    // In the single-process case, we have to use some XPCOM incantations
+    // to resolve to the <xul:browser>.
+    if (this.request.element) {
+      return this.request.element;
+    }
+    return this.request
+               .window
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShell)
+               .chromeEventHandler;
+  },
+
+  get principal() {
+    return this.request.principal;
+  },
+
+  cancel() {
+    this.request.cancel();
+  },
+
+  allow() {
+    this.request.allow();
+  },
+};
+
+PermissionUI.PermissionPromptForRequestPrototype =
+  PermissionPromptForRequestPrototype;
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the GeoLocation API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ *        The request for a permission from content.
+ */
+function GeolocationPermissionPrompt(request) {
+  this.request = request;
+}
+
+GeolocationPermissionPrompt.prototype = {
+  __proto__: PermissionPromptForRequestPrototype,
+
+  get permissionKey() {
+    return "geo";
+  },
+
+  get popupOptions() {
+    let pref = "browser.geolocation.warning.infoURL";
+    return {
+      learnMoreURL: Services.urlFormatter.formatURLPref(pref),
+    };
+  },
+
+  get notificationID() {
+    return "geolocation";
+  },
+
+  get anchorID() {
+    return "geo-notification-icon";
+  },
+
+  get message() {
+    let message;
+    if (this.principal.URI.schemeIs("file")) {
+      message = gBrowserBundle.GetStringFromName("geolocation.shareWithFile2");
+    } else {
+      message = gBrowserBundle.GetStringFromName("geolocation.shareWithSite2");
+    }
+    return message;
+  },
+
+  get promptActions() {
+    // We collect Telemetry data on Geolocation prompts and how users
+    // respond to them. The probe keys are a bit verbose, so let's alias them.
+    const SHARE_LOCATION =
+      Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_SHARE_LOCATION;
+    const ALWAYS_SHARE =
+      Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_ALWAYS_SHARE;
+    const NEVER_SHARE =
+      Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_NEVER_SHARE;
+
+    let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+
+    let actions = [{
+      label: gBrowserBundle.GetStringFromName("geolocation.shareLocation"),
+      accessKey:
+        gBrowserBundle.GetStringFromName("geolocation.shareLocation.accesskey"),
+      action: null,
+      expireType: null,
+      callback: function() {
+        secHistogram.add(SHARE_LOCATION);
+      },
+    }];
+
+    if (!this.principal.URI.schemeIs("file")) {
+      // Always share location action.
+      actions.push({
+        label: gBrowserBundle.GetStringFromName("geolocation.alwaysShareLocation"),
+        accessKey:
+          gBrowserBundle.GetStringFromName("geolocation.alwaysShareLocation.accesskey"),
+        action: Ci.nsIPermissionManager.ALLOW_ACTION,
+        expireType: null,
+        callback: function() {
+          secHistogram.add(ALWAYS_SHARE);
+        },
+      });
+
+      // Never share location action.
+      actions.push({
+        label: gBrowserBundle.GetStringFromName("geolocation.neverShareLocation"),
+        accessKey:
+          gBrowserBundle.GetStringFromName("geolocation.neverShareLocation.accesskey"),
+        action: Ci.nsIPermissionManager.DENY_ACTION,
+        expireType: null,
+        callback: function() {
+          secHistogram.add(NEVER_SHARE);
+        },
+      });
+    }
+
+    return actions;
+  },
+
+  onBeforeShow() {
+    let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+    const SHOW_REQUEST = Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST;
+    secHistogram.add(SHOW_REQUEST);
+  },
+};
+
+PermissionUI.GeolocationPermissionPrompt = GeolocationPermissionPrompt;
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the Desktop Notification API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ *        The request for a permission from content.
+ * @return {PermissionPrompt} (see documentation in header)
+ */
+function DesktopNotificationPermissionPrompt(request) {
+  this.request = request;
+}
+
+DesktopNotificationPermissionPrompt.prototype = {
+  __proto__: PermissionPromptForRequestPrototype,
+
+  get permissionKey() {
+    return "desktop-notification";
+  },
+
+  get popupOptions() {
+    let learnMoreURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
+
+    // The eventCallback is bound to the Notification that's being
+    // shown. We'll stash a reference to this in the closure so that
+    // the request can be cancelled.
+    let prompt = this;
+
+    let eventCallback = function(type) {
+      if (type == "dismissed") {
+        // Bug 1259148: Hide the doorhanger icon. Unlike other permission
+        // doorhangers, the user can't restore the doorhanger using the icon
+        // in the location bar. Instead, the site will be notified that the
+        // doorhanger was dismissed.
+        this.remove();
+        prompt.request.cancel();
+      }
+    };
+
+    return {
+      learnMoreURL,
+      eventCallback,
+    };
+  },
+
+  get notificationID() {
+    return "web-notifications";
+  },
+
+  get anchorID() {
+    return "web-notifications-notification-icon";
+  },
+
+  get message() {
+    return gBrowserBundle.GetStringFromName("webNotifications.receiveFromSite");
+  },
+
+  get promptActions() {
+    let promptActions;
+    // Only show "allow for session" in PB mode, we don't
+    // support "allow for session" in non-PB mode.
+    if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
+      promptActions = [
+        {
+          label: gBrowserBundle.GetStringFromName("webNotifications.receiveForSession"),
+          accessKey:
+            gBrowserBundle.GetStringFromName("webNotifications.receiveForSession.accesskey"),
+          action: Ci.nsIPermissionManager.ALLOW_ACTION,
+          expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+        }
+      ];
+    } else {
+      promptActions = [
+        {
+          label: gBrowserBundle.GetStringFromName("webNotifications.alwaysReceive"),
+          accessKey:
+            gBrowserBundle.GetStringFromName("webNotifications.alwaysReceive.accesskey"),
+          action: Ci.nsIPermissionManager.ALLOW_ACTION,
+          expireType: null,
+        },
+        {
+          label: gBrowserBundle.GetStringFromName("webNotifications.neverShow"),
+          accessKey:
+            gBrowserBundle.GetStringFromName("webNotifications.neverShow.accesskey"),
+          action: Ci.nsIPermissionManager.DENY_ACTION,
+          expireType: null,
+        },
+      ];
+    }
+
+    return promptActions;
+  },
+};
+
+PermissionUI.DesktopNotificationPermissionPrompt =
+  DesktopNotificationPermissionPrompt;
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -29,16 +29,17 @@ EXTRA_JS_MODULES += [
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HiddenFrame.jsm',
     'LaterRun.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
+    'PermissionUI.jsm',
     'PluginContent.jsm',
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SelfSupportBackend.jsm',
     'SitePermissions.jsm',