Bug 1373206 - Create a new dialog for notification settings under Firefox Preferences to match the new spec. r=johannh
authorPrathiksha <prathikshaprasadsuman@gmail.com>
Thu, 29 Jun 2017 23:49:35 +0530
changeset 422370 fd5645e639c87c59cc80f2ffe6fc4a6423803715
parent 422369 797d8666721f4f192ee1f075b41faccf4fc0cdec
child 422371 9ab1b265e36d96b81500612137f4d1ceab4e6e72
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1373206
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1373206 - Create a new dialog for notification settings under Firefox Preferences to match the new spec. r=johannh MozReview-Commit-ID: 5gk4mr3D0jT
browser/components/preferences/in-content-new/privacy.js
browser/components/preferences/in-content-new/tests/browser.ini
browser/components/preferences/in-content-new/tests/browser_permissions_dialog.js
browser/components/preferences/in-content-new/tests/browser_permissions_urlFieldHidden.js
browser/components/preferences/in-content-new/tests/browser_search_subdialogs_within_preferences_2.js
browser/components/preferences/jar.mn
browser/components/preferences/sitePermissions.css
browser/components/preferences/sitePermissions.js
browser/components/preferences/sitePermissions.xul
browser/locales/en-US/chrome/browser/preferences/preferences.properties
--- a/browser/components/preferences/in-content-new/privacy.js
+++ b/browser/components/preferences/in-content-new/privacy.js
@@ -263,18 +263,18 @@ var gPrivacyPane = {
       bundlePrefs.getString("blockliststitle"),
       bundlePrefs.getString("blockliststext"),
     ]);
     appendSearchKeywords("popupPolicyButton", [
       bundlePrefs.getString("popuppermissionstitle2"),
       bundlePrefs.getString("popuppermissionstext"),
     ]);
     appendSearchKeywords("notificationsPolicyButton", [
-      bundlePrefs.getString("notificationspermissionstitle"),
-      bundlePrefs.getString("notificationspermissionstext4"),
+      bundlePrefs.getString("notificationspermissionstitle2"),
+      bundlePrefs.getString("notificationspermissionstext5"),
     ]);
     appendSearchKeywords("addonExceptions", [
       bundlePrefs.getString("addons_permissions_title2"),
       bundlePrefs.getString("addonspermissionstext"),
     ]);
     appendSearchKeywords("viewSecurityDevicesButton", [
       pkiBundle.getString("enable_fips"),
     ]);
@@ -801,21 +801,21 @@ var gPrivacyPane = {
   // NOTIFICATIONS
 
   /**
    * Displays the notifications exceptions dialog where specific site notification
    * preferences can be set.
    */
   showNotificationExceptions() {
     let bundlePreferences = document.getElementById("bundlePreferences");
-    let params = { permissionType: "desktop-notification" };
-    params.windowTitle = bundlePreferences.getString("notificationspermissionstitle");
-    params.introText = bundlePreferences.getString("notificationspermissionstext4");
+    let params = { permissionType: "desktop-notification"};
+    params.windowTitle = bundlePreferences.getString("notificationspermissionstitle2");
+    params.introText = bundlePreferences.getString("notificationspermissionstext5");
 
-    gSubDialog.open("chrome://browser/content/preferences/permissions.xul",
+    gSubDialog.open("chrome://browser/content/preferences/sitePermissions.xul",
                     "resizable=yes", params);
 
     try {
       Services.telemetry
               .getHistogramById("WEB_NOTIFICATION_EXCEPTIONS_OPENED").add();
     } catch (e) {}
   },
 
--- a/browser/components/preferences/in-content-new/tests/browser.ini
+++ b/browser/components/preferences/in-content-new/tests/browser.ini
@@ -57,16 +57,17 @@ skip-if = e10s
 [browser_privacypane_8.js]
 [browser_sanitizeOnShutdown_prefLocked.js]
 [browser_searchsuggestions.js]
 [browser_security-1.js]
 [browser_security-2.js]
 [browser_siteData.js]
 [browser_siteData2.js]
 [browser_site_login_exceptions.js]
+[browser_permissions_dialog.js]
 [browser_cookies_dialog.js]
 [browser_subdialogs.js]
 support-files =
   subdialog.xul
   subdialog2.xul
 [browser_telemetry.js]
 # Skip this test on Android as FHR and Telemetry are separate systems there.
 skip-if = !healthreport || !telemetry || (os == 'linux' && debug) || (os == 'android')
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content-new/tests/browser_permissions_dialog.js
@@ -0,0 +1,215 @@
+"use strict";
+
+/* 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:///modules/SitePermissions.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const PERMISSIONS_URL = "chrome://browser/content/preferences/sitePermissions.xul";
+const URL = "http://www.example.com";
+const URI = Services.io.newURI(URL);
+var sitePermissionsDialog;
+
+function checkPermissionItem(origin, state) {
+  let doc = sitePermissionsDialog.document;
+
+  let label = doc.getElementsByTagName("label")[0];
+  Assert.equal(label.value, origin);
+
+  let menulist = doc.getElementsByTagName("menulist")[0];
+  Assert.equal(menulist.label, state);
+}
+
+async function openPermissionsDialog() {
+  let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let doc = content.document;
+    let settingsButton = doc.getElementById("notificationsPolicyButton");
+    settingsButton.click();
+  });
+
+  sitePermissionsDialog = await dialogOpened;
+}
+
+add_task(async function openSitePermissionsDialog() {
+  await openPreferencesViaOpenPreferencesAPI("privacy", {leaveOpen: true});
+  await openPermissionsDialog();
+});
+
+add_task(async function addPermission() {
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+
+  // First item in the richlistbox contains column headers.
+  Assert.equal(richlistbox.itemCount - 1, 0,
+               "Number of permission items is 0 initially");
+
+  // Add notification permission for a website.
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Observe the added permission changes in the dialog UI.
+  Assert.equal(richlistbox.itemCount - 1, 1);
+  checkPermissionItem(URL, "Allow");
+
+  SitePermissions.remove(URL, "desktop-notification");
+});
+
+add_task(async function observePermissionChange() {
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Change the permission.
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.BLOCK);
+
+  checkPermissionItem(URL, "Block");
+
+  SitePermissions.remove(URL, "desktop-notification");
+});
+
+add_task(async function observePermissionDelete() {
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  Assert.equal(richlistbox.itemCount - 1, 1,
+               "The box contains one permission item initially");
+
+  SitePermissions.remove(URI, "desktop-notification");
+
+  Assert.equal(richlistbox.itemCount - 1, 0);
+});
+
+add_task(async function onPermissionChange() {
+  let doc = sitePermissionsDialog.document;
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Change the permission state in the UI.
+  doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+  Assert.equal(SitePermissions.get(URI, "desktop-notification").state,
+               SitePermissions.ALLOW,
+               "Permission state does not change before saving changes");
+
+  doc.getElementById("btnApplyChanges").click();
+
+  await waitForCondition(() =>
+    SitePermissions.get(URI, "desktop-notification").state == SitePermissions.BLOCK);
+
+  SitePermissions.remove(URI, "desktop-notification");
+});
+
+add_task(async function onPermissionDelete() {
+  await openPermissionsDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  richlistbox.selectItem(richlistbox.getItemAtIndex(1));
+  doc.getElementById("removePermission").click();
+
+  await waitForCondition(() => richlistbox.itemCount - 1 == 0);
+
+  Assert.equal(SitePermissions.get(URI, "desktop-notification").state,
+               SitePermissions.ALLOW,
+               "Permission is not deleted before saving changes");
+
+  doc.getElementById("btnApplyChanges").click();
+
+  await waitForCondition(() =>
+    SitePermissions.get(URI, "desktop-notification").state == SitePermissions.UNKNOWN);
+});
+
+add_task(async function onAllPermissionsDelete() {
+  await openPermissionsDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  let u = Services.io.newURI("http://www.test.com");
+  SitePermissions.set(u, "desktop-notification", SitePermissions.ALLOW);
+
+  doc.getElementById("removeAllPermissions").click();
+  await waitForCondition(() => richlistbox.itemCount - 1 == 0);
+
+  Assert.equal(SitePermissions.get(URI, "desktop-notification").state,
+     SitePermissions.ALLOW);
+  Assert.equal(SitePermissions.get(u, "desktop-notification").state,
+    SitePermissions.ALLOW, "Permissions are not deleted before saving changes");
+
+  doc.getElementById("btnApplyChanges").click();
+
+  await waitForCondition(() =>
+    (SitePermissions.get(URI, "desktop-notification").state == SitePermissions.UNKNOWN) &&
+      (SitePermissions.get(u, "desktop-notification").state == SitePermissions.UNKNOWN));
+});
+
+add_task(async function onPermissionChangeAndDelete() {
+  await openPermissionsDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Change the permission state in the UI.
+  doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+  // Remove that permission by clicking the "Remove" button.
+  richlistbox.selectItem(richlistbox.getItemAtIndex(1));
+  doc.getElementById("removePermission").click();
+
+  await waitForCondition(() => richlistbox.itemCount - 1 == 0);
+
+  doc.getElementById("btnApplyChanges").click();
+
+  await waitForCondition(() =>
+    SitePermissions.get(URI, "desktop-notification").state == SitePermissions.UNKNOWN);
+});
+
+add_task(async function onPermissionChangeCancel() {
+  await openPermissionsDialog();
+
+  let doc = sitePermissionsDialog.document;
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Change the permission state in the UI.
+  doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+  doc.getElementById("cancel").click();
+
+  Assert.equal(SitePermissions.get(URI, "desktop-notification").state,
+               SitePermissions.ALLOW,
+               "Permission state does not change on clicking cancel");
+
+  SitePermissions.remove(URI, "desktop-notification");
+});
+
+add_task(async function onPermissionDeleteCancel() {
+  await openPermissionsDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let richlistbox = doc.getElementById("permissionsBox");
+  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+
+  // Remove that permission by clicking the "Remove" button.
+  richlistbox.selectItem(richlistbox.getItemAtIndex(1));
+  doc.getElementById("removePermission").click();
+
+  await waitForCondition(() => richlistbox.itemCount - 1 == 0);
+
+  doc.getElementById("cancel").click();
+
+  Assert.equal(SitePermissions.get(URI, "desktop-notification").state,
+               SitePermissions.ALLOW,
+               "Permission state does not change on clicking cancel");
+});
+
+add_task(async function removeTab() {
+  gBrowser.removeCurrentTab();
+});
--- a/browser/components/preferences/in-content-new/tests/browser_permissions_urlFieldHidden.js
+++ b/browser/components/preferences/in-content-new/tests/browser_permissions_urlFieldHidden.js
@@ -19,27 +19,8 @@ add_task(async function urlFieldVisibleF
   let urlLabel = dialog.document.getElementById("urlLabel");
   ok(!urlLabel.hidden, "urlLabel should be visible when one of block/session/allow visible");
   let url = dialog.document.getElementById("url");
   ok(!url.hidden, "url should be visible when one of block/session/allow visible");
 
   popupPolicyCheckbox.click();
   gBrowser.removeCurrentTab();
 });
-
-add_task(async function urlFieldHiddenForNotificationPermissions() {
-  await openPreferencesViaOpenPreferencesAPI("panePrivacy", {leaveOpen: true});
-  let win = gBrowser.selectedBrowser.contentWindow;
-  let doc = win.document;
-  let notificationsPolicyButton = doc.getElementById("notificationsPolicyButton");
-  ok(notificationsPolicyButton, "notificationsPolicyButton found");
-  let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL);
-  notificationsPolicyButton.click();
-  let dialog = await dialogPromise;
-  ok(dialog, "dialog loaded");
-
-  let urlLabel = dialog.document.getElementById("urlLabel");
-  ok(urlLabel.hidden, "urlLabel should be hidden as requested");
-  let url = dialog.document.getElementById("url");
-  ok(url.hidden, "url should be hidden as requested");
-
-  gBrowser.removeCurrentTab();
-});
--- a/browser/components/preferences/in-content-new/tests/browser_search_subdialogs_within_preferences_2.js
+++ b/browser/components/preferences/in-content-new/tests/browser_search_subdialogs_within_preferences_2.js
@@ -46,11 +46,11 @@ add_task(async function() {
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for searching for the "Notification Permissions" subdialog.
  */
 add_task(async function() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
-  evaluateSearchResults("request permission again", "permissionsGroup");
+  evaluateSearchResults("send notifications", "permissionsGroup");
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/components/preferences/jar.mn
+++ b/browser/components/preferences/jar.mn
@@ -15,16 +15,19 @@ browser.jar:
     content/browser/preferences/donottrack.xul
 *   content/browser/preferences/fonts.xul
     content/browser/preferences/fonts.js
     content/browser/preferences/handlers.xml
     content/browser/preferences/handlers.css
 *   content/browser/preferences/languages.xul
     content/browser/preferences/languages.js
     content/browser/preferences/permissions.xul
+    content/browser/preferences/sitePermissions.xul
+    content/browser/preferences/sitePermissions.js
+    content/browser/preferences/sitePermissions.css
     content/browser/preferences/containers.xul
     content/browser/preferences/containers.js
     content/browser/preferences/permissions.js
     content/browser/preferences/sanitize.xul
     content/browser/preferences/sanitize.js
     content/browser/preferences/selectBookmark.xul
     content/browser/preferences/selectBookmark.js
     content/browser/preferences/siteDataSettings.xul
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/sitePermissions.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+.website-name {
+  padding: 5px;
+}
+
+#permissionsBox {
+  min-height: 18em;
+}
+
+#permissionsBox > richlistitem {
+  min-height: 35px;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/sitePermissions.js
@@ -0,0 +1,255 @@
+/* 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/AppConstants.jsm");
+Components.utils.import("resource:///modules/SitePermissions.jsm");
+
+function Permission(principal, type, capability, capabilityString) {
+  this.principal = principal;
+  this.origin = principal.origin;
+  this.type = type;
+  this.capability = capability;
+  this.capabilityString = capabilityString;
+}
+
+var gSitePermissionsManager = {
+  _type: "",
+  _isObserving: false,
+  _permissions: new Map(),
+  _permissionsToChange: new Map(),
+  _permissionsToDelete: new Map(),
+  _list: null,
+  _bundle: null,
+  _removeButton: null,
+  _removeAllButton: null,
+
+  onLoad() {
+    let params = window.arguments[0];
+    this.init(params);
+  },
+
+  init(params) {
+    if (!this._isObserving) {
+      Services.obs.addObserver(this, "perm-changed");
+      this._isObserving = true;
+    }
+
+    this._bundle = document.getElementById("bundlePreferences");
+    this._type = params.permissionType;
+    this._list = document.getElementById("permissionsBox");
+    this._removeButton = document.getElementById("removePermission");
+    this._removeAllButton = document.getElementById("removeAllPermissions");
+
+    let permissionsText = document.getElementById("permissionsText");
+    while (permissionsText.hasChildNodes())
+      permissionsText.firstChild.remove();
+    permissionsText.appendChild(document.createTextNode(params.introText));
+
+    document.title = params.windowTitle;
+
+    this._loadPermissions();
+  },
+
+  uninit() {
+    if (this._isObserving) {
+      Services.obs.removeObserver(this, "perm-changed");
+      this._isObserving = false;
+    }
+  },
+
+  observe(subject, topic, data) {
+    if (topic !== "perm-changed")
+      return;
+
+    let permission = subject.QueryInterface(Components.interfaces.nsIPermission);
+
+    // Ignore unrelated permission types.
+    if (permission.type !== this._type)
+      return;
+
+    if (data == "added") {
+      this._addPermissionToList(permission);
+    } else if (data == "changed") {
+      let p = this._permissions.get(permission.principal.origin);
+      p.capability = permission.capability;
+      p.capabilityString = this._getCapabilityString(permission.capability);
+      this._handleCapabilityChange(p);
+    } else if (data == "deleted") {
+      this._removePermissionFromList(permission.principal.origin);
+    }
+  },
+
+  _handleCapabilityChange(perm) {
+    let permissionlistitem = document.getElementsByAttribute("origin", perm.origin)[0];
+    let menulist = permissionlistitem.getElementsByTagName("menulist")[0];
+    menulist.selectedItem =
+      menulist.getElementsByAttribute("value", perm.capability)[0];
+  },
+
+  _getCapabilityString(capability) {
+    let stringKey = null;
+    switch (capability) {
+    case Services.perms.ALLOW_ACTION:
+      stringKey = "can";
+      break;
+    case Services.perms.DENY_ACTION:
+      stringKey = "cannot";
+      break;
+    case Services.perms.PROMPT_ACTION:
+      stringKey = "prompt"
+      break;
+    }
+    return this._bundle.getString(stringKey);
+  },
+
+  _addPermissionToList(perm) {
+    if (perm.type !== this._type)
+      return;
+    let capabilityString = this._getCapabilityString(perm.capability);
+    let p = new Permission(perm.principal, perm.type, perm.capability,
+                           capabilityString);
+    this._permissions.set(p.origin, p);
+    this._createPermissionListItem(p);
+  },
+
+  _removePermissionFromList(origin) {
+    this._permissions.delete(origin);
+    let permissionlistitem = document.getElementsByAttribute("origin", origin)[0];
+    this._list.removeItemAt(this._list.getIndexOfItem(permissionlistitem));
+  },
+
+  _loadPermissions() {
+    // load permissions into a table.
+    let enumerator = Services.perms.enumerator;
+    while (enumerator.hasMoreElements()) {
+      let nextPermission = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission);
+      this._addPermissionToList(nextPermission);
+    }
+
+    // disable "remove all" button if there are none
+    this._setRemoveButtonState();
+  },
+
+  _createPermissionListItem(permission) {
+    let richlistitem = document.createElement("richlistitem");
+    richlistitem.setAttribute("origin", permission.origin);
+    let row = document.createElement("hbox");
+    row.setAttribute("flex", "1");
+
+    let hbox = document.createElement("hbox");
+    let website = document.createElement("label");
+    website.setAttribute("value", permission.origin);
+    website.setAttribute("width", "50");
+    hbox.setAttribute("class", "website-name");
+    hbox.setAttribute("flex", "3");
+    hbox.appendChild(website);
+
+    let menulist = document.createElement("menulist");
+    let menupopup = document.createElement("menupopup");
+    menulist.setAttribute("flex", "1");
+    menulist.setAttribute("width", "50");
+    menulist.appendChild(menupopup);
+    let states = SitePermissions.getAvailableStates(permission.type);
+    for (let state of states) {
+      if (state == SitePermissions.UNKNOWN)
+        continue;
+      let m = document.createElement("menuitem");
+      m.setAttribute("label", this._getCapabilityString(state));
+      m.setAttribute("value", state);
+      menupopup.appendChild(m);
+    }
+    menulist.value = permission.capability;
+
+    menulist.addEventListener("select", () => {
+      this.onPermissionChange(permission, Number(menulist.selectedItem.value));
+    });
+
+    row.appendChild(hbox);
+    row.appendChild(menulist);
+    richlistitem.appendChild(row);
+    this._list.appendChild(richlistitem)
+  },
+
+  onWindowKeyPress(event) {
+    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE)
+      window.close();
+  },
+
+  onPermissionKeyPress(event) {
+    if (!this._list.selectedItem)
+      return;
+
+    if (event.keyCode == KeyEvent.DOM_VK_DELETE ||
+       (AppConstants.platform == "macosx" &&
+        event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
+      this.onPermissionDelete();
+      event.preventDefault();
+    }
+  },
+
+  _setRemoveButtonState() {
+    if (!this._list)
+      return;
+
+    let hasSelection = this._list.selectedIndex;
+    let hasRows = this._list.itemCount > 1;
+    this._removeButton.disabled = !hasSelection;
+    this._removeAllButton.disabled = !hasRows;
+  },
+
+  onPermissionDelete() {
+    let richlistitem = this._list.selectedItem;
+    let origin = richlistitem.getAttribute("origin");
+    let permission = this._permissions.get(origin);
+
+    this._removePermissionFromList(origin);
+    this._permissionsToDelete.set(permission.origin, permission);
+
+    this._setRemoveButtonState();
+  },
+
+  onAllPermissionsDelete() {
+    for (let permission of this._permissions.values()) {
+      this._removePermissionFromList(permission.origin);
+      this._permissionsToDelete.set(permission.origin, permission);
+    }
+
+    this._setRemoveButtonState();
+  },
+
+  onPermissionSelect() {
+    this._setRemoveButtonState();
+  },
+
+  onPermissionChange(perm, capability) {
+    let p = this._permissions.get(perm.origin);
+    if (p.capability == capability)
+      return;
+    p.capability = capability;
+    p.capabilityString = this._getCapabilityString(capability);
+    this._permissionsToChange.set(p.origin, p);
+
+    // enable "remove all" button as needed
+    this._setRemoveButtonState();
+  },
+
+  onApplyChanges() {
+    // Stop observing permission changes since we are about
+    // to write out the pending adds/deletes and don't need
+    // to update the UI
+    this.uninit();
+
+    for (let p of this._permissionsToChange.values()) {
+      let uri = Services.io.newURI(p.origin);
+      SitePermissions.set(uri, p.type, p.capability);
+    }
+
+    for (let p of this._permissionsToDelete.values()) {
+      let uri = Services.io.newURI(p.origin);
+      SitePermissions.remove(uri, p.type);
+    }
+    window.close();
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/sitePermissions.xul
@@ -0,0 +1,73 @@
+<?xml version="1.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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/permissions.dtd" >
+
+<window id="SitePermissionsDialog" class="windowDialog"
+        windowtype="Browser:SitePermissions"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        style="width: &window.width;;"
+        onload="gSitePermissionsManager.onLoad();"
+        onunload="gSitePermissionsManager.uninit();"
+        persist="screenX screenY width height"
+        onkeypress="gSitePermissionsManager.onWindowKeyPress(event);">
+
+  <script src="chrome://browser/content/preferences/sitePermissions.js"/>
+
+  <stringbundle id="bundlePreferences"
+                src="chrome://browser/locale/preferences/preferences.properties"/>
+
+  <keyset>
+    <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
+  </keyset>
+
+  <vbox class="contentPane largeDialogContainer" flex="1">
+    <description id="permissionsText" control="url"/>
+    <separator class="thin"/>
+    <hbox align="start">
+      <textbox id="url" flex="1" placeholder="Search Website"
+               type="search"/>
+    </hbox>
+    <separator class="thin"/>
+    <richlistbox id="permissionsBox" selected="false"
+                 hidecolumnpicker="true" flex="1"
+                 onkeypress="gSitePermissionsManager.onPermissionKeyPress(event);"
+                 onselect="gSitePermissionsManager.onPermissionSelect();">
+      <richlistitem>
+        <hbox flex="1">
+          <treecol id="siteCol" label="&treehead.sitename2.label;" flex="3"
+                   data-field-name="origin" persist="width" width="50"/>
+          <splitter class="tree-splitter"/>
+          <treecol id="statusCol" label="&treehead.status.label;" flex="1"
+                   data-field-name="capability" persist="width" width="50"/>
+        </hbox>
+      </richlistitem>
+    </richlistbox>
+  </vbox>
+  <vbox>
+    <hbox class="actionButtons" align="left" flex="1">
+      <button id="removePermission" disabled="true"
+              accesskey="&removepermission2.accesskey;"
+              icon="remove" label="&removepermission2.label;"
+              oncommand="gSitePermissionsManager.onPermissionDelete();"/>
+      <button id="removeAllPermissions"
+              icon="clear" label="&removeallpermissions2.label;"
+              accesskey="&removeallpermissions2.accesskey;"
+              oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/>
+    </hbox>
+    <spacer flex="1"/>
+    <hbox class="actionButtons" align="right" flex="1">
+      <button oncommand="close();" icon="close" id="cancel"
+              label="&button.cancel.label;" accesskey="&button.cancel.accesskey;" />
+      <button id="btnApplyChanges" oncommand="gSitePermissionsManager.onApplyChanges();" icon="save"
+              label="&button.ok.label;" accesskey="&button.ok.accesskey;"/>
+    </hbox>
+  </vbox>
+</window>
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -22,18 +22,18 @@ acceptVeryLargeMinimumFont=Keep my chang
 trackingprotectionpermissionstext2=You have disabled Tracking Protection on these websites.
 trackingprotectionpermissionstitle=Exceptions - Tracking Protection
 cookiepermissionstext=You can specify which websites are always or never allowed to use cookies.  Type the exact address of the site you want to manage and then click Block, Allow for Session, or Allow.
 cookiepermissionstitle=Exceptions - Cookies
 addonspermissionstext=You can specify which websites are allowed to install add-ons. Type the exact address of the site you want to allow and then click Allow.
 addons_permissions_title2=Allowed Websites - Add-ons Installation
 popuppermissionstext=You can specify which websites are allowed to open pop-up windows. Type the exact address of the site you want to allow and then click Allow.
 popuppermissionstitle2=Allowed Websites - Pop-ups
-notificationspermissionstext4=Control which websites are always or never allowed to send you notifications. If you remove a site, it will need to request permission again.
-notificationspermissionstitle=Notification Permissions
+notificationspermissionstext5=The following websites have requested to send you notifications. You can specify which websites are allowed to send you notifications.
+notificationspermissionstitle2=Settings - Notification Permissions
 invalidURI=Please enter a valid hostname
 invalidURITitle=Invalid Hostname Entered
 savedLoginsExceptions_title=Exceptions - Saved Logins
 savedLoginsExceptions_desc2=Logins for the following websites will not be saved:
 
 #### Block List Manager
 
 blockliststext=You can choose which list Firefox will use to block Web elements that may track your browsing activity.
@@ -115,16 +115,17 @@ hostColon=Host:
 domainColon=Domain:
 forSecureOnly=Encrypted connections only
 forAnyConnection=Any type of connection
 expireAtEndOfSession=At end of session
 can=Allow
 canAccessFirstParty=Allow first party only
 canSession=Allow for Session
 cannot=Block
+prompt=Always Ask
 noCookieSelected=<no cookie selected>
 cookiesAll=The following cookies are stored on your computer:
 cookiesFiltered=The following cookies match your search:
 
 # LOCALIZATION NOTE (removeAllCookies, removeAllShownCookies):
 # removeAllCookies and removeAllShownCookies are both used on the same one button,
 # never displayed together and can share the same accesskey.
 # When only partial cookies are shown as a result of keyword search,