Bug 1270416 - registerProtocolHandler improvement with Permission UI and control center. r?Paolo Amadini draft
authorEden Chuang <echuang@mozilla.com>
Tue, 14 Feb 2017 23:25:37 +0800
changeset 483583 29f75c933141e2ef0d8f22f7aefe71a779fc370b
parent 480952 f505911eb333d5ae8c2bf5c44f7b85add6450b53
child 545672 3288d81d5948d4939d44da247fd5daf0e870e358
push id45348
push userechuang@mozilla.com
push dateTue, 14 Feb 2017 15:26:15 +0000
reviewersPaolo
bugs1270416
milestone54.0a1
Bug 1270416 - registerProtocolHandler improvement with Permission UI and control center. r?Paolo Amadini MozReview-Commit-ID: 8SdBmpBRBK6
browser/base/content/browser.xul
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_registerProtocolHandler_notification.html
browser/base/content/test/general/browser_registerProtocolHandler_notification.js
browser/components/feeds/WebContentConverter.js
browser/components/feeds/moz.build
browser/components/feeds/test/browser/browser.ini
browser/components/feeds/test/browser/browser_handlerRegistrationPermissionControl.js
browser/components/feeds/test/browser/browser_registerHandlerWithPermissionUI.js
browser/components/feeds/test/browser/head.js
browser/components/feeds/test/browser/multipleRegistration.html
browser/components/feeds/test/browser/protocolHandler.html
browser/components/feeds/test/browser/superLongProtocolHandler.html
browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/feeds/subscribe.properties
browser/locales/en-US/chrome/browser/sitePermissions.properties
browser/modules/PermissionUI.jsm
browser/modules/SitePermissions.jsm
browser/modules/test/xpcshell/test_SitePermissions.js
browser/themes/shared/notification-icons.inc.css
browser/themes/shared/notification-icons.svg
uriloader/exthandler/tests/mochitest/browser.ini
uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -705,16 +705,18 @@
                        onclick="PageProxyClickHandler(event);"/>
                 <image id="sharing-icon" mousethrough="always"/>
                 <image id="tracking-protection-icon"/>
                 <box id="blocked-permissions-container" align="center">
                   <image data-permission-id="geo" class="blocked-permission-icon geo-icon" role="button"
                          tooltiptext="&urlbar.geolocationBlocked.tooltip;"/>
                   <image data-permission-id="desktop-notification" class="blocked-permission-icon desktop-notification-icon" role="button"
                          tooltiptext="&urlbar.webNotificationsBlocked.tooltip;"/>
+                  <image data-permission-id="handler-registration" class="blocked-permission-icon handler-registration-icon" role="button"
+                         tooltiptext="&urlbar.handlerRegistrationBlocked.tooltip;"/>
                   <image data-permission-id="camera" class="blocked-permission-icon camera-icon" role="button"
                          tooltiptext="&urlbar.cameraBlocked.tooltip;"/>
                   <image data-permission-id="indexedDB" class="blocked-permission-icon indexedDB-icon" role="button"
                          tooltiptext="&urlbar.indexedDBBlocked.tooltip;"/>
                   <image data-permission-id="microphone" class="blocked-permission-icon microphone-icon" role="button"
                          tooltiptext="&urlbar.microphoneBlocked.tooltip;"/>
                   <image data-permission-id="screen" class="blocked-permission-icon screen-icon" role="button"
                          tooltiptext="&urlbar.screenBlocked.tooltip;"/>
@@ -747,16 +749,18 @@
                   <image id="servicesInstall-notification-icon" class="notification-anchor-icon service-icon" role="button"
                          tooltiptext="&urlbar.servicesNotificationAnchor.tooltip;"/>
                   <image id="translate-notification-icon" class="notification-anchor-icon translation-icon" role="button"
                          tooltiptext="&urlbar.translateNotificationAnchor.tooltip;"/>
                   <image id="translated-notification-icon" class="notification-anchor-icon translation-icon in-use" role="button"
                          tooltiptext="&urlbar.translatedNotificationAnchor.tooltip;"/>
                   <image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button"
                          tooltiptext="&urlbar.emeNotificationAnchor.tooltip;"/>
+                  <image id="handler-registration-notification-icon" class="notification-anchor-icon handler-registration-icon" role="button"
+                         tooltiptext="&urlbar.handlerRegistrationNotificationAnchor.tooltip;"/>
                 </box>
                 <image id="connection-icon"/>
                 <hbox id="identity-icon-labels">
                   <label id="identity-icon-label" class="plain" flex="1"/>
                   <label id="identity-icon-country-label" class="plain"/>
                 </hbox>
               </box>
               <box id="urlbar-display-box" align="center">
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -7,17 +7,16 @@ support-files =
   app_subframe_bug575561.html
   aboutHome_content_script.js
   audio.ogg
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
   browser_fxa_web_channel.html
-  browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   browser_web_channel_iframe.html
   bug1262648_string_with_newlines.dtd
   bug592338.html
   bug792517-2.html
   bug792517.html
@@ -416,17 +415,16 @@ skip-if = true # Bug 1005420 - fails int
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
 [browser_visibleTabs_tabPreview.js]
 skip-if = (os == "win" && !debug)
 [browser_web_channel.js]
 [browser_windowopen_reflows.js]
 [browser_zbug569342.js]
 skip-if = e10s || debug # Bug 1094240 - has findbar-related failures
-[browser_registerProtocolHandler_notification.js]
 [browser_addCertException.js]
 [browser_e10s_about_page_triggeringprincipal.js]
 support-files =
   file_about_child.html
   file_about_parent.html
 [browser_e10s_switchbrowser.js]
 [browser_e10s_about_process.js]
 [browser_e10s_chrome_process.js]
deleted file mode 100644
--- a/browser/base/content/test/general/browser_registerProtocolHandler_notification.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<html>
-  <head>
-    <title>Protocol registrar page</title>
-    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
-    <meta content="utf-8" http-equiv="encoding">
-  </head>
-  <body>
-    <script type="text/javascript">
-      navigator.registerProtocolHandler("testprotocol",
-          "https://example.com/foobar?uri=%s",
-          "Test Protocol");
-    </script>
-  </body>
-</html>
deleted file mode 100644
--- a/browser/base/content/test/general/browser_registerProtocolHandler_notification.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* 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/. */
-
-function test() {
-  waitForExplicitFinish();
-  let notificationValue = "Protocol Registration: testprotocol";
-  let testURI = "http://example.com/browser/" +
-    "browser/base/content/test/general/browser_registerProtocolHandler_notification.html";
-
-    waitForCondition(function() {
-        // Do not start until the notification is up
-        let notificationBox = window.gBrowser.getNotificationBox();
-        let notification = notificationBox.getNotificationWithValue(notificationValue);
-        return notification;
-    },
-    function() {
-
-        let notificationBox = window.gBrowser.getNotificationBox();
-        let notification = notificationBox.getNotificationWithValue(notificationValue);
-        ok(notification, "Notification box should be displayed");
-        if (notification == null) {
-            finish();
-            return;
-        }
-        is(notification.type, "info", "We expect this notification to have the type of 'info'.");
-        isnot(notification.image, null, "We expect this notification to have an icon.");
-
-        let buttons = notification.getElementsByClassName("notification-button-default");
-        is(buttons.length, 1, "We expect see one default button.");
-
-        buttons = notification.getElementsByClassName("notification-button");
-        is(buttons.length, 1, "We expect see one button.");
-
-        let button = buttons[0];
-        isnot(button.label, null, "We expect the add button to have a label.");
-        todo_isnot(button.accesskey, null, "We expect the add button to have a accesskey.");
-
-        finish();
-    }, "Still can not get notification after retry 100 times.", 100);
-
-    window.gBrowser.selectedBrowser.loadURI(testURI);
-}
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -2,16 +2,21 @@
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionUI",
+                                  "resource:///modules/PermissionUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
+                                  "resource:///modules/SitePermissions.jsm");
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 function LOG(str) {
   dump("*** " + str + "\n");
 }
 
@@ -217,16 +222,82 @@ const Utils = {
 
   resolveContentType(aContentType) {
     if (aContentType in this._mappings)
       return this._mappings[aContentType];
     return aContentType;
   }
 };
 
+function HandlerRegistrationRequest(aBrowser, aPrincipal, aURIString, aTitle, aType) {
+  this._browser = aBrowser;
+  this._principal = aPrincipal;
+  this._handlerURI = aURIString;
+  this._title = aTitle;
+  this._type = aType;
+}
+
+HandlerRegistrationRequest.prototype = {
+  get browser() {
+    return this._browser;
+  },
+
+  get permissionKey() {
+    return "handler-registration";
+  },
+
+  get principal() {
+    return this._principal;
+  },
+
+  get uri() {
+    return this._handlerURI;
+  },
+
+  get type() {
+    return this._type;
+  },
+
+  get title() {
+    return this._title;
+  },
+
+  _setAsDefaultHandler() {
+    // create handler app and save it into handler database
+    let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
+                  createInstance(Ci.nsIWebHandlerApp);
+    handler.name = this._title;
+    handler.uriTemplate = this._handlerURI;
+
+    let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+              getService(Ci.nsIExternalProtocolService);
+    let handlerInfo = eps.getProtocolHandlerInfo(this._type);
+
+    handlerInfo.possibleApplicationHandlers.appendElement(handler, false);
+    handlerInfo.alwaysAskBeforeHandling = false;
+    handlerInfo.preferredApplicationHandler = handler;
+    handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+
+    let hs = Cc["@mozilla.org/uriloader/handler-service;1"].
+             getService(Ci.nsIHandlerService);
+    hs.store(handlerInfo);
+  },
+
+  allow() {
+    this._setAsDefaultHandler();
+  },
+
+  cancel() {
+    SitePermissions.set(this.principal.URI,
+                        this.permissionKey,
+                        SitePermissions.BLOCK,
+                        SitePermissions.SCOPE_PERSISTENT);
+  },
+};
+
 function WebContentConverterRegistrar() {
   this._contentTypes = {};
   this._autoHandleContentTypes = {};
 }
 
 WebContentConverterRegistrar.prototype = {
   get stringBundle() {
     let sb = Services.strings.createBundle(STRING_BUNDLE_URI);
@@ -411,59 +482,24 @@ WebContentConverterRegistrar.prototype =
       // would have some way to tell what's going wrong.
       Services.console.
       logStringMessage("Web page denied access to register a protocol handler inside private browsing mode");
       return;
     }
 
     Utils.checkProtocolHandlerAllowed(aProtocol, aURIString,
                                       haveWindow ? aBrowserOrWindow : null);
-
-    // Now Ask the user and provide the proper callback
-    let message = this._getFormattedString("addProtocolHandler",
-                                           [aTitle, uri.host, aProtocol]);
-
-    let notificationIcon = uri.prePath + "/favicon.ico";
-    let notificationValue = "Protocol Registration: " + aProtocol;
-    let addButton = {
-      label: this._getString("addProtocolHandlerAddButton"),
-      accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"),
-      protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle },
-
-      callback(aNotification, aButtonInfo) {
-          let protocol = aButtonInfo.protocolInfo.protocol;
-          let name     = aButtonInfo.protocolInfo.name;
-
-          let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
-                        createInstance(Ci.nsIWebHandlerApp);
-          handler.name = name;
-          handler.uriTemplate = aButtonInfo.protocolInfo.uri;
-
-          let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
-                    getService(Ci.nsIExternalProtocolService);
-          let handlerInfo = eps.getProtocolHandlerInfo(protocol);
-          handlerInfo.possibleApplicationHandlers.appendElement(handler, false);
-
-          // Since the user has agreed to add a new handler, chances are good
-          // that the next time they see a handler of this type, they're going
-          // to want to use it.  Reset the handlerInfo to ask before the next
-          // use.
-          handlerInfo.alwaysAskBeforeHandling = true;
-
-          let hs = Cc["@mozilla.org/uriloader/handler-service;1"].
-                   getService(Ci.nsIHandlerService);
-          hs.store(handlerInfo);
-        }
-    };
-    let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
-    notificationBox.appendNotification(message,
-                                       notificationValue,
-                                       notificationIcon,
-                                       notificationBox.PRIORITY_INFO_LOW,
-                                       [addButton]);
+    let handlerRegistrationRequest =
+        new HandlerRegistrationRequest(browser,
+                                       browser.contentPrincipal,
+                                       aURIString,
+                                       aTitle,
+                                       aProtocol);
+    let requestPrompt = new PermissionUI.HandlerRegistrationPermissionPrompt(handlerRegistrationRequest);
+    requestPrompt.prompt();
   },
 
   /**
    * See nsIWebContentHandlerRegistrar
    * If a DOM window is provided, then the request came from content, so we
    * prompt the user to confirm the registration.
    */
   registerContentHandler(aContentType, aURIString, aTitle, aWindowOrBrowser) {
--- a/browser/components/feeds/moz.build
+++ b/browser/components/feeds/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 XPIDL_SOURCES += [
     'nsIFeedResultService.idl',
     'nsIWebContentConverterRegistrar.idl',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+  head.js
+  protocolHandler.html
+  superLongProtocolHandler.html
+  multipleRegistration.html
+
+[browser_registerHandlerWithPermissionUI.js]
+[browser_handlerRegistrationPermissionControl.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/browser_handlerRegistrationPermissionControl.js
@@ -0,0 +1,169 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let gPermsSvc = Services.perms;
+
+const kTestObj = {
+  url: kTestRoot + "protocolHandler.html",
+  protocolType: "testprotocol",
+  permissionKey: "handler-registration",
+  handlerName: "testprotocol handler",
+  handlerUriTemplate: "https://example.com/foobar?uri=%s",
+};
+const CONTROL_CENTER_PANEL = gIdentityHandler._identityPopup;
+
+registerCleanupFunction(function() {
+  // traverse all permissions and remove the permission we create in tests.
+  let permissionKey = "handler-registration";
+  let permsEnum = gPermsSvc.enumerator;
+  while (permsEnum.hasMoreElements()) {
+    let perm = permsEnum.getNext().QueryInterface(Ci.nsIPermission);
+    if (perm.type == permissionKey) {
+      gPermsSvc.removeFromPrincipal(perm.principal);
+    }
+  }
+
+  // cleanup all handler info we create in the tests
+  removeHandlerInfoForProtocol(kTestObj.protocolType);
+});
+
+function* openIdentityPopup() {
+  let shownPromise =
+    BrowserTestUtils.waitForEvent(CONTROL_CENTER_PANEL, "popupshown");
+  gIdentityHandler._identityBox.click();
+  return shownPromise;
+}
+
+function* closeIdentityPopup() {
+  let closePromise =
+    BrowserTestUtils.waitForEvent(CONTROL_CENTER_PANEL, "popuphidden");
+  gIdentityHandler._identityPopup.hidePopup();
+  return closePromise;
+}
+
+let permissionElementsGetter = {
+  permissionsList() {
+    return document.getElementById("identity-popup-permission-list");
+  },
+  permissionLabels() {
+    return this.permissionsList().querySelectorAll(".identity-popup-permission-label");
+  },
+  cancelButtons() {
+    return this.permissionsList().querySelectorAll(".identity-popup-permission-remove-button");
+  },
+}
+
+/**
+ *  Test handler-registration permission control with Control Center.
+ */
+add_task(function*() {
+  yield BrowserTestUtils.withNewTab(
+    kTestObj.url,
+    function* (browser) {
+      let principal = browser.contentPrincipal;
+
+      yield clickButtonOnPrompt(false);
+
+      // check permissions' label on control center
+      isnot(CONTROL_CENTER_PANEL, null, "control center panel should not be null.");
+      yield openIdentityPopup();
+
+      let labels = permissionElementsGetter.permissionLabels();
+      is(labels.length, 1,
+         "Should be only one permission in the permission list of control center.");
+
+      // click the cancel button to remove the permission
+      let cancelButtons = permissionElementsGetter.cancelButtons();
+      cancelButtons[0].click();
+
+      labels = permissionElementsGetter.permissionLabels();
+      is(labels.length, 0,
+         "Should be no permissions in the permission list of control center.");
+
+      is(gPermsSvc.testExactPermissionFromPrincipal(principal, kTestObj.permissionKey),
+         gPermsSvc.UNKNOWN_ACTION,
+         "Handler registration permission should be removed.");
+
+      yield closeIdentityPopup();
+
+      // cleanup permissions and handler info
+      gPermsSvc.removeFromPrincipal(principal, kTestObj.permissionKey);
+      removeHandlerInfoForProtocol(kTestObj.protocolType);
+    }
+  );
+});
+
+/**
+ *  Test handler-registration permission control with page info's permission tab.
+ */
+add_task(function*() {
+  yield BrowserTestUtils.withNewTab(
+    kTestObj.url,
+    function* (browser) {
+      let principal = browser.contentPrincipal;
+      yield clickButtonOnPrompt(false);
+
+      let pageInfo;
+      let shownPromise =  new Promise(resolve => {
+        function observer(subject, topic, data) {
+          let observedWindow = subject.QueryInterface(Ci.nsIDOMWindow);
+          Services.obs.removeObserver(observer, "page-info-dialog-loaded");
+          pageInfo.onFinished.push(() => {
+            // check the radio buttons status of page info's permission tab
+            let permissionRadioGroup = pageInfo.document.
+              getElementById(kTestObj.permissionKey + "RadioGroup");
+            isnot(permissionRadioGroup, null,
+                  "Radio button group should not be null.");
+            let permissionRadioAlwaysAsk = pageInfo.document.
+              getElementById(kTestObj.permissionKey + "#0");
+            isnot(permissionRadioAlwaysAsk, null,
+                  "AlwaysAsk radio button should not be null.");
+            let permissionRadioBlock = pageInfo.document.
+              getElementById(kTestObj.permissionKey + "#2");
+            isnot(permissionRadioBlock, null,
+                  "Block radio button should not be null.");
+            is(permissionRadioGroup.selectedItem, permissionRadioBlock,
+               "Should be selected on Block radio buttion.");
+            is(gPermsSvc.testExactPermissionFromPrincipal(principal, kTestObj.permissionKey),
+               gPermsSvc.DENY_ACTION,
+               "Permission should be DENY ACTION");
+
+            // click AlwaysAsk radio button and then test the permission status
+            permissionRadioAlwaysAsk.click();
+            is(permissionRadioGroup.selectedItem, permissionRadioAlwaysAsk,
+               "Should be selected on AlwaysAsk radio buttion.");
+            is(gPermsSvc.testExactPermissionFromPrincipal(principal, kTestObj.permissionKey),
+               gPermsSvc.UNKNOWN_ACTION,
+               "Permission should be UNKNOWN ACTION");
+            let useDefaultCheckbox = pageInfo.document.
+              getElementById(kTestObj.permissionKey + "Def");
+            isnot(useDefaultCheckbox, null,
+                  "useDefault checkbox should not be null");
+            ok(useDefaultCheckbox.checked, "useDefault checkbox should be checked");
+
+            // click Block radio button and then test the permission status
+            useDefaultCheckbox.click();
+            permissionRadioBlock.click();
+            is(permissionRadioGroup.selectedItem, permissionRadioBlock,
+               "Should be selected on Block radio buttion.");
+            is(gPermsSvc.testExactPermissionFromPrincipal(principal, kTestObj.permissionKey),
+               gPermsSvc.DENY_ACTION,
+               "Permission should be DENY ACTION");
+
+            pageInfo.close();
+          });
+          resolve(observedWindow);
+        }
+        Services.obs.addObserver(observer, "page-info-dialog-loaded", false);
+      });
+      pageInfo = BrowserPageInfo(browser.currentURI.spec, "permTab");
+      let win = yield shownPromise;
+      yield BrowserTestUtils.domWindowClosed(win);
+
+      // cleanup permissions and handlerInfo
+      gPermsSvc.removeFromPrincipal(principal, kTestObj.permissionKey);
+      removeHandlerInfoForProtocol(kTestObj.protocolType);
+    }
+  );
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/browser_registerHandlerWithPermissionUI.js
@@ -0,0 +1,308 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let gPermsSvc = Services.perms;
+
+const kDefaultHandler = {
+  uri: Services.io.newURI("https://test1.example.org/testprotocol=%s"),
+  name: "testprotocol default handler",
+};
+
+const kTestInfos = [
+  {
+    page: "protocolHandler.html",
+    protocolType: "testprotocol",
+    handlerName: "Test Protocol",
+  },
+  {
+    page: "superLongProtocolHandler.html",
+    protocolType: "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylongprotocol",
+    handlerName: "Super Long Test Protocol",
+  },
+];
+
+const kMultiRegistrationTestInfos = [
+  {
+    page: "multipleRegistration.html",
+    protocolType: "testprotocol1",
+    handlerName: "Test Protocol 1",
+  },
+  {
+    page: "multipleRegistration.html",
+    protocolType: "testprotocol2",
+    handlerName: "Test Protocol 2",
+  },
+];
+
+function createTestObj(testInfo) {
+  return {
+    url: kTestRoot + testInfo.page,
+    protocolType: testInfo.protocolType,
+    permissionKey: "handler-registration",
+    notificationID: "handler-registration",
+    handlerName: testInfo.handlerName,
+    handlerUriTemplate: "https://example.com/foobar?uri=%s",
+    expectedURL: "https://example.com/foobar?uri=" + testInfo.protocolType + "%3Atest",
+  };
+}
+
+function setupDefaultHandler(aProtocolType) {
+  let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
+                   createInstance(Ci.nsIWebHandlerApp);
+  handler.name = kDefaultHandler.name
+  handler.uriTemplate = kDefaultHandler.uri.spec;
+  let handlerInfo = getHandlerInfo(aProtocolType);
+  handlerInfo.possibleApplicationHandlers.appendElement(handler, false);
+  handlerInfo.alwaysAskBeforeHandling = false;
+  handlerInfo.preferredApplicationHandler = handler;
+  handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+  gHandlerSvc.store(handlerInfo);
+}
+
+function checkHandlerInfoProperties(aProtocolType, aHandlerName, aUriTemplate) {
+  let handlerInfo = getHandlerInfo(aProtocolType);
+  is(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp,
+     "The preferredAction should be useHelperApp.");
+  ok(handlerInfo.preferredApplicationHandler,
+     "The preferredApplicationHandler should exist.");
+  is(handlerInfo.preferredApplicationHandler.name, aHandlerName,
+     "The preferredAppliacitonHandler name should be " + aHandlerName + ".");
+  is(handlerInfo.preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp).uriTemplate,
+     aUriTemplate,
+     "The preferredApplicationHandler uriTemplate should be '" + aUriTemplate + "'.");
+  ok(!handlerInfo.alwaysAskBeforeHandling,
+     "alwaysAskBeforeHandling should be false");
+}
+
+function* checkLinkIsHandledCorrectly(aTestObj, aBrowser) {
+  // Middle-click a testprotocol link and check the new tab is correct
+  let link = "#link";
+  const expectedURL = aTestObj.expectedURL;
+
+  let promiseTabOpened =
+    BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+  yield BrowserTestUtils.synthesizeMouseAtCenter(link, {button: 1}, aBrowser);
+  let tab = yield promiseTabOpened;
+  yield BrowserTestUtils.switchTab(gBrowser, tab);
+  is(gURLBar.value, expectedURL,
+     "the expected URL is displayed in the location bar");
+  yield BrowserTestUtils.removeTab(tab);
+
+  // Click the testprotocol link and check the url in the current tab.
+  let stopPromise = BrowserTestUtils.browserStopped(aBrowser);
+  yield BrowserTestUtils.synthesizeMouseAtCenter(link, {}, aBrowser);
+  yield stopPromise;
+  is(gURLBar.value, expectedURL,
+     "the expected URL is displayed in the location bar");
+}
+
+registerCleanupFunction(function() {
+  // traverse all permissions and remove the permission we create in tests.
+  let permissionKey = "handler-registration";
+  let permsEnum = gPermsSvc.enumerator;
+  while (permsEnum.hasMoreElements()) {
+    let perm = permsEnum.getNext().QueryInterface(Ci.nsIPermission);
+    if (perm.type == permissionKey) {
+      gPermsSvc.removeFromPrincipal(perm.principal);
+    }
+  }
+
+  // cleanup all handler info we create in the tests
+  let testInfos = kTestInfos;
+  testInfos.concat(kMultiRegistrationTestInfos);
+  for (let testInfo of testInfos) {
+    removeHandlerInfoForProtocol(testInfo.protocolType);
+  }
+});
+
+/**
+ *  Test the notification actions' label and access key.
+ */
+add_task(function*() {
+  let testObj = createTestObj(kTestInfos[0]);
+  yield BrowserTestUtils.withNewTab(
+    testObj.url,
+    function* (browser) {
+      let notification =
+        PopupNotifications.getNotification(testObj.notificationID, browser);
+
+      ok(notification, "Should have gotten the notification");
+      is(notification.options.checkbox.checked, false,
+         "The checkbox should be not checked.");
+      is(notification.mainAction.label, "Allow",
+         "The main action should have the right label.");
+      is(notification.mainAction.accessKey, "A",
+         "The main action should have the right access key.");
+      is(notification.secondaryActions.length, 1,
+         "There should only be 1 secondary action.");
+      is(notification.secondaryActions[0].label, "Never Allow",
+         "The secondary action should have the right label.");
+      is(notification.secondaryActions[0].accessKey, "N",
+         "The secondary action should have the right access key.");
+
+      let removePromise =
+        BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+      notification.remove();
+      yield removePromise;
+    }
+  );
+});
+
+/**
+ *  Test the Never Allow action. This test includes following situations
+ *  1. without any default handlers.
+ *  2. with a default handler.
+ *  3. super long protocol type without any default handlers.
+ *  4. super long protocol type with a default handler.
+ */
+add_task(function*() {
+  for (let testInfo of kTestInfos) {
+    let testObj = createTestObj(testInfo);
+    for (let useDefaultHandler of [false, true]) {
+      yield BrowserTestUtils.withNewTab(
+        testObj.url,
+        function* (browser) {
+          let principal = browser.contentPrincipal;
+
+          // setup a default handler if needed
+          if (useDefaultHandler) {
+            setupDefaultHandler(testObj.protocolType);
+          }
+
+          // check the current permission before clicking Never Allow
+          let curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                                   testObj.permissionKey);
+          is(curPerm, gPermsSvc.UNKNOWN_ACTION,
+             "Permission for " + testObj.permissionKey +
+             " should be no permission before clicking 'Never Allow'.");
+
+          // click the Never Allow button
+          yield clickButtonOnPrompt(false);
+
+          // check the permission after clicking Never Allow
+          curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                               testObj.permissionKey);
+          is(curPerm, gPermsSvc.DENY_ACTION,
+             "Permission for " + testObj.permissionKey +
+             " should be DENY after clicking 'Never Allow'.");
+
+          // check permission of the default registrar and handler information
+          if (useDefaultHandler) {
+            checkHandlerInfoProperties(testObj.protocolType,
+                                       kDefaultHandler.name,
+                                       kDefaultHandler.uri.spec);
+          }
+
+          // cleanup
+          gPermsSvc.removeFromPrincipal(principal, testObj.permissionKey);
+          removeHandlerInfoForProtocol(testObj.protocolType);
+        }
+      );
+    }
+  }
+});
+
+/**
+ *  test the Allow action and check the protocol link are handled by
+ *  registered handler. This test includes following situations.
+ *  1. without any default handlers.
+ *  2. with a default handler.
+ *  3. super long protocol type without any default handlers.
+ *  4. super long protocol type with a default handler.
+ */
+add_task(function*() {
+  for (let testInfo of kTestInfos) {
+    let testObj = createTestObj(testInfo);
+    for (let useDefaultHandler of [false, true]) {
+      yield BrowserTestUtils.withNewTab(
+        testObj.url,
+        function* (browser) {
+          let principal = browser.contentPrincipal;
+
+          // check the permission before clicking Allow
+          let curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                                   testObj.permissionKey);
+          is(curPerm, gPermsSvc.UNKNOWN_ACTION,
+             "Permission for " + testObj.permissionKey +
+             " should be no permission before clicking 'Allow'.");
+
+          // setup a default handler if needed
+          if (useDefaultHandler) {
+            setupDefaultHandler(testObj.protocolType);
+          }
+
+          // click the Allow button
+          yield clickButtonOnPrompt(true);
+
+          // check the permission after clicking Allow
+          curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                               testObj.permissionKey);
+
+          // bug 1270416, we don't save allow permission for handler registration
+          is(curPerm, gPermsSvc.UNKNOWN_ACTION,
+             "Permission for " + testObj.permissionKey +
+             " should be no permission after clicking 'Allow'.");
+
+          checkHandlerInfoProperties(testObj.protocolType,
+                                     testObj.handlerName,
+                                     testObj.handlerUriTemplate);
+
+          yield checkLinkIsHandledCorrectly(testObj, browser);
+
+          // cleanup
+          gPermsSvc.removeFromPrincipal(principal, testObj.permissionKey);
+          removeHandlerInfoForProtocol(testObj.protocolType);
+        }
+      );
+    }
+  }
+});
+
+/**
+ *  Test the notification actions' label and access key.
+ */
+add_task(function*() {
+  let testObj1 = createTestObj(kMultiRegistrationTestInfos[0]);
+  let testObj2 = createTestObj(kMultiRegistrationTestInfos[1]);
+  yield BrowserTestUtils.withNewTab(
+    testObj1.url,
+    function* (browser) {
+      let principal = browser.contentPrincipal;
+      let curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                               testObj1.permissionKey);
+      is(curPerm, gPermsSvc.UNKNOWN_ACTION,
+         "Permission for " + testObj1.permissionKey +
+         " should be UNKNOWN.");
+
+      curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                           testObj2.permissionKey);
+      is(curPerm, gPermsSvc.UNKNOWN_ACTION,
+         "Permission for " + testObj2.permissionKey +
+         " should be UNKNOWN.");
+
+      yield clickButtonOnPrompt(false);
+
+      // Since the notificationID is the same for all handler registration,
+      // the notification for testObj1.permissionKey will be replaced by the
+      // notification for testObj2.permissionKey.
+      curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                           testObj1.permissionKey);
+      is(curPerm, gPermsSvc.DENY_ACTION,
+         "Permission for " + testObj1.permissionKey +
+         " should be DENY after clicking 'Never Allow'.");
+
+      curPerm = gPermsSvc.testExactPermissionFromPrincipal(principal,
+                                                           testObj2.permissionKey);
+      is(curPerm, gPermsSvc.DENY_ACTION,
+         "Permission for " + testObj2.permissionKey +
+         " should be DENY after clicking 'Never Allow'.");
+
+      // cleanup
+      gPermsSvc.removeFromPrincipal(principal, testObj1.permissionKey);
+      removeHandlerInfoForProtocol(testObj1.protocolType);
+      gPermsSvc.removeFromPrincipal(principal, testObj2.permissionKey);
+      removeHandlerInfoForProtocol(testObj2.protocolType);
+    }
+  );
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/head.js
@@ -0,0 +1,33 @@
+let gExtProtocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+                         getService(Ci.nsIExternalProtocolService);
+let gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
+                     getService(Ci.nsIHandlerService);
+
+const kTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content",
+                                                      "https://example.com");
+
+function* clickButtonOnPrompt(aAllow) {
+  let removePromise =
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+  let popupNotifications = PopupNotifications.panel.childNodes;
+  is(popupNotifications.length, 1, "Should be showing a <xul:popupnotification>");
+  let popupNotification = popupNotifications[0];
+  if (aAllow) {
+    popupNotification.checkbox.click();
+    popupNotification.button.click();
+  } else {
+    popupNotification.secondaryButton.click();
+  }
+  return removePromise;
+}
+
+function getHandlerInfo(aProtocolType) {
+  return gExtProtocolSvc.getProtocolHandlerInfo(aProtocolType);
+}
+
+function removeHandlerInfoForProtocol(aProtocolType) {
+  let handlerInfo = getHandlerInfo(aProtocolType);
+  if (gHandlerSvc.exists(handlerInfo)) {
+    gHandlerSvc.remove(handlerInfo);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/multipleRegistration.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Multiple registration</title>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+    <meta content="utf-8" http-equiv="encoding">
+  </head>
+  <body>
+    <script type="text/javascript">
+      navigator.registerProtocolHandler("testprotocol1",
+          "https://example.com/foobar?uri=%s",
+          "Test Protocol 1");
+      navigator.registerProtocolHandler("testprotocol2",
+          "https://example.com/foobar?uri=%s",
+          "Test Protocol 2");
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/protocolHandler.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Protocol handler</title>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+    <meta content="utf-8" http-equiv="encoding">
+  </head>
+  <body>
+    <script type="text/javascript">
+      navigator.registerProtocolHandler("testprotocol",
+          "https://example.com/foobar?uri=%s",
+          "Test Protocol");
+    </script>
+    <a id="link" href="testprotocol:test">testprotocol link</a>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/feeds/test/browser/superLongProtocolHandler.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Protocol handler</title>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+    <meta content="utf-8" http-equiv="encoding">
+  </head>
+  <body>
+    <script type="text/javascript">
+      navigator.registerProtocolHandler(
+                "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylongprotocol",
+                "https://example.com/foobar?uri=%s",
+                "Super Long Test Protocol");
+    </script>
+    <a id="link" href="veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylongprotocol:test">testprotocol link</a>
+  </body>
+</html>
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js
@@ -1,41 +1,45 @@
 /* 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/. */
 
 // This test makes sure that the web pages can't register protocol handlers
 // inside the private browsing mode.
 
 add_task(function* test() {
-  let notificationValue = "Protocol Registration: testprotocol";
+  let notificationValue = "handler-registration";
   let testURI = "http://example.com/browser/" +
     "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html";
 
   let doTest = Task.async(function* (aIsPrivateMode, aWindow) {
-    let tab = aWindow.gBrowser.selectedTab = aWindow.gBrowser.addTab(testURI);
-    yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    let tab = aWindow.gBrowser.addTab("about:blank");
+    let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    yield BrowserTestUtils.loadURI(tab.linkedBrowser, testURI);
+    yield loadPromise;
 
-    let promiseFinished = PromiseUtils.defer();
-    setTimeout(function() {
-      let notificationBox = aWindow.gBrowser.getNotificationBox();
-      let notification = notificationBox.getNotificationWithValue(notificationValue);
-
-      if (aIsPrivateMode) {
+    if (aIsPrivateMode) {
+      let promiseFinished = PromiseUtils.defer();
+      setTimeout(function() {
+        let notification = PopupNotifications.getNotification(notificationValue,
+                                                              tab.linkedBrowser);
         // Make sure the notification is correctly displayed without a remember control
         ok(!notification, "Notification box should not be displayed inside of private browsing mode");
-      } else {
-        // Make sure the notification is correctly displayed with a remember control
-        ok(notification, "Notification box should be displaying outside of private browsing mode");
-      }
-
-      promiseFinished.resolve();
-    }, 100); // remember control is added in a setTimeout(0) call
-
-    yield promiseFinished.promise;
+        promiseFinished.resolve();
+      }, 100); // remember control is added in a setTimeout(0) call
+      yield promiseFinished.promise;
+    } else {
+      let shownPromise = BrowserTestUtils.waitForEvent(aWindow.PopupNotifications.panel, "popupshown");
+      yield BrowserTestUtils.switchTab(aWindow.gBrowser, tab);
+      yield shownPromise;
+      let notification = PopupNotifications.getNotification(notificationValue,
+                                                            tab.linkedBrowser);
+      // Make sure the notification is correctly displayed with a remember control
+      ok(notification, "Notification box should be displaying outside of private browsing mode");
+    }
   });
 
   // test first when not on private mode
   let win = yield BrowserTestUtils.openNewBrowserWindow();
   yield doTest(false, win);
 
   // then test when on private mode
   let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -205,16 +205,17 @@ These should match what Safari and other
 <!ENTITY goEndCap.tooltip             "Go to the address in the Location Bar">
 <!ENTITY printButton.label            "Print">
 <!ENTITY printButton.tooltip          "Print this page">
 
 <!ENTITY urlbar.viewSiteInfo.label                      "View site information">
 
 <!ENTITY urlbar.defaultNotificationAnchor.tooltip         "Open message panel">
 <!ENTITY urlbar.geolocationNotificationAnchor.tooltip     "Open location request panel">
+<!ENTITY urlbar.handlerRegistrationNotificationAnchor.tooltip     "Open default application setting request panel">
 <!ENTITY urlbar.addonsNotificationAnchor.tooltip          "Open add-on installation message panel">
 <!ENTITY urlbar.indexedDBNotificationAnchor.tooltip       "Open offline storage message panel">
 <!ENTITY urlbar.passwordNotificationAnchor.tooltip        "Open save password message panel">
 <!ENTITY urlbar.pluginsNotificationAnchor.tooltip         "Manage plug-in use">
 <!ENTITY urlbar.webNotificationAnchor.tooltip             "Change whether you can receive notifications from the site">
 
 <!ENTITY urlbar.webRTCShareDevicesNotificationAnchor.tooltip      "Manage sharing your camera and/or microphone with the site">
 <!ENTITY urlbar.webRTCShareMicrophoneNotificationAnchor.tooltip   "Manage sharing your microphone with the site">
@@ -224,16 +225,17 @@ These should match what Safari and other
 <!ENTITY urlbar.translateNotificationAnchor.tooltip       "Translate this page">
 <!ENTITY urlbar.translatedNotificationAnchor.tooltip      "Manage page translation">
 <!ENTITY urlbar.emeNotificationAnchor.tooltip             "Manage use of DRM software">
 
 <!ENTITY urlbar.cameraBlocked.tooltip            "You have blocked your camera for this website.">
 <!ENTITY urlbar.microphoneBlocked.tooltip        "You have blocked your microphone for this website.">
 <!ENTITY urlbar.screenBlocked.tooltip            "You have blocked this website from sharing your screen.">
 <!ENTITY urlbar.geolocationBlocked.tooltip       "You have blocked location information for this website.">
+<!ENTITY urlbar.handlerRegistrationBlocked.tooltip        "You have blocked setting default application by this website.">
 <!ENTITY urlbar.indexedDBBlocked.tooltip         "You have blocked data storage for this website.">
 <!ENTITY urlbar.webNotificationsBlocked.tooltip  "You have blocked notifications for this website.">
 
 <!ENTITY urlbar.openHistoryPopup.tooltip                "Show history">
 
 <!ENTITY urlbar.zoomReset.tooltip     "Reset zoom level">
 
 <!ENTITY searchItem.title             "Search">
--- a/browser/locales/en-US/chrome/browser/feeds/subscribe.properties
+++ b/browser/locales/en-US/chrome/browser/feeds/subscribe.properties
@@ -41,12 +41,21 @@ feedSubscriptionFeed1=This is a “feed” of frequently changing content on this site.
 feedSubscriptionAudioPodcast1=This is a “podcast” of frequently changing content on this site.
 feedSubscriptionVideoPodcast1=This is a “video podcast” of frequently changing content on this site.
 
 feedSubscriptionFeed2=You can subscribe to this feed to receive updates when this content changes.
 feedSubscriptionAudioPodcast2=You can subscribe to this podcast to receive updates when this content changes.
 feedSubscriptionVideoPodcast2=You can subscribe to this video podcast to receive updates when this content changes.
 
 # Protocol Handling
-# "Add %appName (%appDomain) as an application for %protocolType links?"
-addProtocolHandler=Add %S (%S) as an application for %S links?
-addProtocolHandlerAddButton=Add Application
-addProtocolHandlerAddButtonAccesskey=A
\ No newline at end of file
+# for the case that default handler is not a web app.
+# "Would you like %originalDefaultAppName to replace %appName (%appDomain) as the default application for %protocolType links?"
+replaceDefaultProtocolHandler=Would you like %S to replace %S (%S) as the default application for %S links?
+# for the case that default handler is a web app.
+# "Would you like %originalDefaultAppName (%originalDefaultAppDomain) to replace %appName (%appDomain) as the default application for %protocolType links?"
+replaceDefaultWebProtocolHandler=Would you like %S (%S) to replace %S (%S) as the default application for %S links?
+# "Set %appName (%appDomain) as the default application for %protocolType links?"
+setDefaultProtocolHandler=Set %S (%S) as the default application for %S links?
+handlerRegistration.allow.label=Allow
+handlerRegistration.allow.accessKey=A
+handlerRegistration.neverAllow.label=Never Allow
+handlerRegistration.neverAllow.accessKey=N
+handlerRegistration.checkbox.label=Allow setting default application
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -30,8 +30,9 @@ permission.desktop-notification2.label =
 permission.image.label = Load Images
 permission.camera.label = Use the Camera
 permission.microphone.label = Use the Microphone
 permission.screen.label = Share the Screen
 permission.install.label = Install Add-ons
 permission.popup.label = Open Pop-up Windows
 permission.geo.label = Access Your Location
 permission.indexedDB.label = Maintain Offline Storage
+permission.handler-registration.label = Set the Default Application for Protocols 
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -296,17 +296,18 @@ this.PermissionPromptPrototype = {
         callback: state => {
           if (promptAction.callback) {
             promptAction.callback();
           }
 
           if (this.permissionKey) {
 
             // Permanently store permission.
-            if (state && state.checkboxChecked) {
+            if (state && state.checkboxChecked &&
+                this.permissionKey != "handler-registration") {
               let scope = SitePermissions.SCOPE_PERSISTENT;
               // Only remember permission for session if in PB mode.
               if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
                 scope = SitePermissions.SCOPE_SESSION;
               }
               SitePermissions.set(this.principal.URI,
                                   this.permissionKey,
                                   promptAction.action,
@@ -591,8 +592,134 @@ DesktopNotificationPermissionPrompt.prot
         action: SitePermissions.BLOCK,
       },
     ];
   },
 };
 
 PermissionUI.DesktopNotificationPermissionPrompt =
   DesktopNotificationPermissionPrompt;
+
+XPCOMUtils.defineLazyGetter(this, "gHandlerStringBundle", function() {
+  return Services.strings.createBundle("chrome://browser/locale/feeds/subscribe.properties");
+});
+
+/**
+ * Creates a PermissionPrompt for a nsIContentPermissionRequest for
+ * the Handler Registration(registerProtocolHandler/registerContentHandler) API.
+ *
+ * @param request (nsIContentPermissionRequest)
+ *        The request for a permission from content.
+ * @return {PermissionPrompt} (see documentation in header)
+ */
+function HandlerRegistrationPermissionPrompt(request) {
+  this.request = request;
+}
+
+HandlerRegistrationPermissionPrompt.prototype = {
+  __proto__: PermissionPromptForRequestPrototype,
+
+  get browser() {
+    return this.request.browser;
+  },
+
+  get principal() {
+    return this.request.principal;
+  },
+
+  get permissionKey() {
+    return this.request.permissionKey;
+  },
+
+  get notificationID() {
+    return "handler-registration";
+  },
+
+  get anchorID() {
+    return "handler-registration-notification-icon";
+  },
+
+  get message() {
+    let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+              getService(Ci.nsIExternalProtocolService);
+    let handlerInfo = eps.getProtocolHandlerInfo(this.request.type);
+
+    // for the case if we already have a default handler for the protocol
+    if (handlerInfo &&
+        handlerInfo.preferredApplicationHandler &&
+        handlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp) {
+      let defaultHandler = handlerInfo.preferredApplicationHandler;
+      if (defaultHandler.uriTemplate != this.request.uri) {
+        // for the case if the default handler is a web app.
+        if (defaultHandler instanceof Ci.nsIWebHandlerApp) {
+          let originalURI = Services.io.newURI(defaultHandler.uriTemplate);
+          let params = [
+            defaultHandler.name,
+            originalURI.host,
+            this.request.title,
+            this.request.principal.URI.host,
+            this.request.type
+          ];
+          return gHandlerStringBundle.
+                 formatStringFromName("replaceDefaultWebProtocolHandler",
+                                      params,
+                                      params.length);
+        }
+        let params = [
+          defaultHandler.name,
+          this.request.title,
+          this.request.principal.URI.host,
+          this.request.type
+        ];
+        return gHandlerStringBundle.
+               formatStringFromName("replaceDefaultProtocolHandler",
+                                    params,
+                                    params.length);
+      }
+    }
+
+    // for the case we don't have a default handler
+    let params = [
+      this.request.title,
+      this.request.principal.URI.host,
+      this.request.type
+    ];
+    return gHandlerStringBundle.
+           formatStringFromName("setDefaultProtocolHandler",
+                                params,
+                                params.length);
+  },
+
+  get popupOptions() {
+    let options = {};
+    options.checkbox = {
+      show: true,
+      checked: false,
+      label: gHandlerStringBundle.GetStringFromName("handlerRegistration.checkbox.label"),
+      uncheckedState: {
+        disableMainAction: true,
+      },
+    };
+    return options;
+  },
+
+  get promptActions() {
+    let promptActions;
+    promptActions = [
+      {
+        label: gHandlerStringBundle.GetStringFromName("handlerRegistration.allow.label"),
+        accessKey: gHandlerStringBundle.GetStringFromName("handlerRegistration.allow.accessKey"),
+        action: SitePermissions.ALLOW,
+        expireType: SitePermissions.SCOPE_PERSISTENT,
+      },
+      {
+        label: gHandlerStringBundle.GetStringFromName("handlerRegistration.neverAllow.label"),
+        accessKey: gHandlerStringBundle.GetStringFromName("handlerRegistration.neverAllow.accessKey"),
+        action: SitePermissions.BLOCK,
+        expireType: SitePermissions.SCOPE_PERSISTENT,
+      },
+    ];
+    return promptActions;
+  },
+};
+
+PermissionUI.HandlerRegistrationPermissionPrompt =
+  HandlerRegistrationPermissionPrompt;
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -596,14 +596,19 @@ var gPermissionObject = {
                SitePermissions.BLOCK : SitePermissions.ALLOW;
     }
   },
 
   "geo": {
     exactHostMatch: true
   },
 
-  "indexedDB": {}
+  "indexedDB": {},
+
+  "handler-registration": {
+    exactHostMatch: true,
+    states: [ SitePermissions.UNKNOWN, SitePermissions.BLOCK ],
+  }
 };
 
 XPCOMUtils.defineLazyPreferenceGetter(SitePermissions, "temporaryPermissionExpireTime",
                                       "privacy.temporary_permission_expire_time_ms", 3600 * 1000);
 
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -3,18 +3,18 @@
  */
 "use strict";
 
 Components.utils.import("resource:///modules/SitePermissions.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 add_task(function* testPermissionsListing() {
   Assert.deepEqual(SitePermissions.listPermissions().sort(),
-    ["camera", "cookie", "desktop-notification", "geo", "image",
-     "indexedDB", "install", "microphone", "popup", "screen"],
+    ["camera", "cookie", "desktop-notification", "geo", "handler-registration",
+    "image", "indexedDB", "install", "microphone", "popup", "screen"],
     "Correct list of all permissions");
 });
 
 add_task(function* testGetAllByURI() {
   // check that it returns an empty array on an invalid URI
   // like a file URI, which doesn't support site permissions
   let wrongURI = Services.io.newURI("file:///example.js")
   Assert.deepEqual(SitePermissions.getAllByURI(wrongURI), []);
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -81,16 +81,25 @@
   list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-osx);
 %elif defined(MOZ_WIDGET_GTK)
   list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-linux-detailed);
 %else
   list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-windows-detailed);
 %endif
 }
 
+.popup-notification-icon[popupid="handler-registration"], 
+.handler-registration-icon {
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#handler-registration)
+}
+
+.handler-registration-icon.blocked-permission-icon {
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#handler-registration-blocked);
+}
+
 .popup-notification-icon[popupid="indexedDB-permissions-prompt"],
 .indexedDB-icon {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#indexedDB);
 }
 
 .indexedDB-icon.blocked-permission-icon {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#indexedDB-blocked);
 }
--- a/browser/themes/shared/notification-icons.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -50,16 +50,17 @@
   <defs>
     <path id="camera-icon" d="m 2,23 a 3,3 0 0 0 3,3 l 14,0 a 3,3 0 0 0 3,-3 l 0,-4 6,5.5 c 0.5,0.5 1,0.7 2,0.5 l 0,-18 c -1,-0.2 -1.5,0 -2,0.5 l -6,5.5 0,-4 a 3,3 0 0 0 -3,-3 l -14,0 a 3,3 0 0 0 -3,3 z" />
     <path id="desktop-notification-icon" d="m 2,20 a 4,4 0 0 0 4,4 l 13,0 7,7 0,-7 a 4,4 0 0 0 4,-4 l 0,-12 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 5,-2 a 1,1 0 1 1 0,-2 l 10,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 14,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 18,0 a 1,1 0 1 1 0,2 z" />
     <path id="geo-linux-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 4,2.1 a 10,10 0 0 0 8,8 l 0,-4 4,0 0,4 a 10,10 0 0 0 8,-8 l -4,0 0,-4 4,0 a 10,10 0 0 0 -8,-8 l 0,4 -4,0 0,-4 a 10,10 0 0 0 -8,8 l 4,0 0,4 z" />
     <path id="geo-linux-detailed-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 3,2.1 a 11,11 0 0 0 9,9 l 1,-5 2,0 1,5 a 11,11 0 0 0 9,-9 l -5,-1 0,-2 5,-1 a 11,11 0 0 0 -9,-9 l -1,5 -2,0 -1,-5 a 11,11 0 0 0 -9,9 l 5,1 0,2 z" />
     <path id="geo-osx-icon" d="m 0,16 16,0 0,16 12,-28 z" />
     <path id="geo-windows-icon" d="m 2,14 0,4 2,0 a 12,12 0 0 0 10,10 l 0,2 4,0 0,-2 a 12,12 0 0 0 10,-10 l 2,0 0,-4 -2,0 a 12,12 0 0 0 -10,-10 l 0,-2 -4,0 0,2 a 12,12 0 0 0 -10,10 z m 4,1.9 a 10,10 0 1 1 0,0.2 z m 4,0 a 6,6 0 1 1 0,0.2 z" />
     <path id="geo-windows-detailed-icon" d="m 2,14.5 0,3 2,0.5 a 12,12 0 0 0 10,10 l 0.5,2 3,0 0.5,-2 a 12,12 0 0 0 10,-10 l 2,-0.5 0,-3 -2,-0.5 a 12,12 0 0 0 -10,-10 l -0.5,-2 -3,0 -0.5,2 a 12,12 0 0 0 -10,10 z m 4,1.4 a 10,10 0 1 1 0,0.2 z m 3,0 a 7,7 0 1 1 0,0.2 z" />
+    <path id="handler-registration-icon" d="m 2,20 a 4,4 0 0 0 4,4 l 13,0 7,7 0,-7 a 4,4 0 0 0 4,-4 l 0,-12 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 5,-2 a 1,1 0 1 1 0,-2 l 10,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 14,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 18,0 a 1,1 0 1 1 0,2 z" />
     <path id="indexedDB-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 2,0 0,-4 -2,0 0,-16 20,0 0,16 -2,0 0,4 2,0 a 4,4 0 0 0 4,-4 l 0,-16 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 8,-2 6,7 6,-7 -4,0 0,-8 -4,0 0,8 z" />
     <path id="login-icon" d="m 2,26 0,4 6,0 0,-2 2,0 0,-2 1,0 0,-1 2,0 0,-3 2,0 2.5,-2.5 1.5,1.5 3,-3 a 8,8 0 1 0 -8,-8 l -3,3 2,2 z m 20,-18.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="login-detailed-icon" d="m 1,27 0,3.5 a 0.5,0.5 0 0 0 0.5,0.5 l 5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1.5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-2 2,0 2.5,-2.5 q 0.5,-0.5 1,0 l 1,1 c 0.5,0.5 1,0.5 1.5,-0.5 l 1,-2 a 9,9 0 1 0 -8,-8 l -2,1 c -1,0.5 -1,1 -0.5,1.5 l 1.5,1.5 q 0.5,0.5 0,1 z m 21,-19.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="microphone-icon" d="m 8,14 0,4 a 8,8 0 0 0 6,7.7 l 0,2.3 -2,0 a 2,2 0 0 0 -2,2 l 12,0 a 2,2 0 0 0 -2,-2 l -2,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 -2,0 0,4 a 6,6 0 0 1 -12,0 l 0,-4 z m 4,4 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
     <path id="microphone-detailed-icon" d="m 8,18 a 8,8 0 0 0 6,7.7 l 0,2.3 -1,0 a 3,2 0 0 0 -3,2 l 12,0 a 3,2 0 0 0 -3,-2 l -1,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 a 1,1 0 0 0 -2,0 l 0,4 a 6,6 0 0 1 -12,0 l 0,-4 a 1,1 0 0 0 -2,0 z m 4,0 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
     <path id="plugin-icon" d="m 2,26 a 2,2 0 0 0 2,2 l 24,0 a 2,2 0 0 0 2,-2 l 0,-16 a 2,2 0 0 0 -2,-2 l -24,0 a 2,2 0 0 0 -2,2 z m 2,-20 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z m 14,0 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z" />
     <path id="popup-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 8,0 a 10,10 0 0 1 -2,-4 l -4,0 a 2,2 0 0 1 -2,-2 l 0,-12 18,0 0,2 a 10,10 0 0 1 4,2 l 0,-8 a 4,4 0 0 0 -4,-4 l -18,0 a 4,4 0 0 0 -4,4 z m 12,-2.1 a 8,8 0 1 1 0,0.2 m 10.7,-4.3 a 5,5 0 0 0 -6.9,6.9 z m -5.4,8.4 a 5,5 0 0 0 6.9,-6.9 z" />
     <path id="screen-icon" d="m 2,18 a 2,2 0 0 0 2,2 l 2,0 0,-6 a 4,4 0 0 1 4,-4 l 14,0 0,-6 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z m 6,10 a 2,2 0 0 0 2,2 l 18,0 a 2,2 0 0 0 2,-2 l 0,-14 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z" />
@@ -78,16 +79,18 @@
   <use id="geo-osx" xlink:href="#geo-osx-icon" />
   <use id="geo-osx-blocked" class="blocked" xlink:href="#geo-osx-icon" />
   <use id="geo-linux" xlink:href="#geo-linux-icon" />
   <use id="geo-linux-blocked" class="blocked" xlink:href="#geo-linux-icon" />
   <use id="geo-linux-detailed" xlink:href="#geo-linux-detailed-icon" />
   <use id="geo-windows" xlink:href="#geo-windows-icon" />
   <use id="geo-windows-blocked" class="blocked" xlink:href="#geo-windows-icon" />
   <use id="geo-windows-detailed" xlink:href="#geo-windows-detailed-icon" />
+  <use id="handler-registration" xlink:href="#handler-registration-icon" />
+  <use id="handler-registration-blocked" class="blocked" xlink:href="#handler-registration-icon" />
   <use id="indexedDB" xlink:href="#indexedDB-icon" />
   <use id="indexedDB-blocked" class="blocked" xlink:href="#indexedDB-icon" />
   <use id="login" xlink:href="#login-icon" />
   <use id="login-highlighted" class="highlighted" xlink:href="#login-icon" />
   <use id="login-detailed" xlink:href="#login-detailed-icon" />
   <use id="microphone" xlink:href="#microphone-icon" />
   <use id="microphone-sharing" xlink:href="#microphone-icon"/>
   <use id="microphone-indicator" xlink:href="#microphone-icon"/>
--- a/uriloader/exthandler/tests/mochitest/browser.ini
+++ b/uriloader/exthandler/tests/mochitest/browser.ini
@@ -1,8 +1,5 @@
 [DEFAULT]
 head = head.js
-support-files =
-  protocolHandler.html
 
 [browser_download_always_ask_preferred_app.js]
 [browser_remember_download_option.js]
-[browser_web_protocol_handlers.js]
deleted file mode 100644
--- a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
+++ /dev/null
@@ -1,76 +0,0 @@
-let testURL = "http://example.com/browser/" +
-  "uriloader/exthandler/tests/mochitest/protocolHandler.html";
-
-add_task(function*() {
-  // Load a page registering a protocol handler.
-  let browser = gBrowser.selectedBrowser;
-  browser.loadURI(testURL);
-  yield BrowserTestUtils.browserLoaded(browser, testURL);
-
-  // Register the protocol handler by clicking the notificationbar button.
-  let notificationValue = "Protocol Registration: testprotocol";
-  let getNotification = () =>
-    gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
-  yield BrowserTestUtils.waitForCondition(getNotification);
-  let notification = getNotification();
-  let button =
-    notification.getElementsByClassName("notification-button-default")[0];
-  ok(button, "got registration button");
-  button.click();
-
-  // Set the new handler as default.
-  const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
-                     getService(Ci.nsIExternalProtocolService);
-  let protoInfo = protoSvc.getProtocolHandlerInfo("testprotocol");
-  is(protoInfo.preferredAction, protoInfo.useHelperApp,
-     "using a helper application is the preferred action");
-  ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
-  let handlers = protoInfo.possibleApplicationHandlers;
-  is(1, handlers.length, "only one handler registered for testprotocol");
-  let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
-  ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler");
-  is(handler.uriTemplate, "https://example.com/foobar?uri=%s",
-     "correct url template")
-  protoInfo.preferredApplicationHandler = handler;
-  protoInfo.alwaysAskBeforeHandling = false;
-  const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
-                       getService(Ci.nsIHandlerService);
-  handlerSvc.store(protoInfo);
-
-  // Middle-click a testprotocol link and check the new tab is correct
-  let link = "#link";
-  const expectedURL = "https://example.com/foobar?uri=testprotocol%3Atest";
-
-  let promiseTabOpened =
-    BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
-  yield BrowserTestUtils.synthesizeMouseAtCenter(link, {button: 1}, browser);
-  let tab = yield promiseTabOpened;
-  gBrowser.selectedTab = tab;
-  is(gURLBar.value, expectedURL,
-     "the expected URL is displayed in the location bar");
-  yield BrowserTestUtils.removeTab(tab);
-
-  // Shift-click the testprotocol link and check the new window.
-  let newWindowPromise = BrowserTestUtils.waitForNewWindow();
-  yield BrowserTestUtils.synthesizeMouseAtCenter(link, {shiftKey: true},
-                                                 browser);
-  let win = yield newWindowPromise;
-  yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
-  yield BrowserTestUtils.waitForCondition(() => win.gBrowser.currentURI.spec == expectedURL);
-  is(win.gURLBar.value, expectedURL,
-     "the expected URL is displayed in the location bar");
-  yield BrowserTestUtils.closeWindow(win);
-
-  // Click the testprotocol link and check the url in the current tab.
-  let loadPromise = BrowserTestUtils.browserLoaded(browser);
-  yield BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser);
-  yield loadPromise;
-  yield BrowserTestUtils.waitForCondition(() => gURLBar.value != testURL);
-  is(gURLBar.value, expectedURL,
-     "the expected URL is displayed in the location bar");
-
-  // Cleanup.
-  protoInfo.preferredApplicationHandler = null;
-  handlers.removeElementAt(0);
-  handlerSvc.store(protoInfo);
-});