Merge autoland to mozilla-central. a=merge
authorCsoregi Natalia <ncsoregi@mozilla.com>
Fri, 16 Aug 2019 06:57:14 +0300
changeset 488383 a58b7dc85887bcc257f525c2ae1df4427f1df613
parent 488331 9229fd85bc05af4c33c8cb8f5e489eca1043722d (current diff)
parent 488382 257776683ca2ea65d61b7e3cc1100e43a93cd6c3 (diff)
child 488384 d887276421d30c773431c006b0607cd30f4d9259
push id113906
push userncsoregi@mozilla.com
push dateFri, 16 Aug 2019 04:07:24 +0000
treeherdermozilla-inbound@d887276421d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone70.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
Merge autoland to mozilla-central. a=merge
devtools/client/debugger/src/actions/tests/helpers/threadFront.js
devtools/client/debugger/src/client/firefox/workers.js
taskcluster/ci/checksums-signing/kind.yml
taskcluster/taskgraph/optimize.py
taskcluster/taskgraph/transforms/checksums_signing.py
taskcluster/taskgraph/util/seta.py
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1,29 +1,23 @@
-# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-# 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/.
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// 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/.
 
-// XXX Toolkit-specific preferences should be moved into toolkit.js
+// Please indent all prefs defined within #ifdef/#ifndef conditions. This
+// improves readability, particular for conditional blocks that exceed a single
+// screen.
 
 #filter substitution
 
-#
-# SYNTAX HINTS:
-#
-#  - Dashes are delimiters; use underscores instead.
-#  - The first character after a period must be alphabetic.
-#  - Computed values (e.g. 50 * 1024) don't work.
-#
-
 #ifdef XP_UNIX
-#ifndef XP_MACOSX
-#define UNIX_BUT_NOT_MAC
-#endif
+  #ifndef XP_MACOSX
+    #define UNIX_BUT_NOT_MAC
+  #endif
 #endif
 
 pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindowMac.xhtml");
 
 // Enables some extra Extension System Logging (can reduce performance)
 pref("extensions.logging.enabled", false);
 
 // Disables strict compatibility, making addons compatible-by-default.
@@ -136,17 +130,17 @@ pref("app.update.elevation.promptMaxAtte
 
 // If set to true, the Update Service will automatically download updates if the
 // user can apply updates. This pref is no longer used on Windows, except as the
 // default value to migrate to the new location that this data is now stored
 // (which is in a file in the update directory). Because of this, this pref
 // should no longer be used directly. Instead, getAppUpdateAutoEnabled and
 // getAppUpdateAutoEnabled from UpdateUtils.jsm should be used.
 #ifndef XP_WIN
-pref("app.update.auto", true);
+  pref("app.update.auto", true);
 #endif
 
 // If set to true, the Update Service will apply updates in the background
 // when it finishes downloading them.
 pref("app.update.staging.enabled", true);
 
 // Update service URL:
 pref("app.update.url", "https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
@@ -154,23 +148,23 @@ pref("app.update.url", "https://aus5.moz
 // app.update.url.details is in branding section
 
 // app.update.badgeWaitTime is in branding section
 // app.update.interval is in branding section
 // app.update.promptWaitTime is in branding section
 
 // Whether or not to attempt using the service for updates.
 #ifdef MOZ_MAINTENANCE_SERVICE
-pref("app.update.service.enabled", true);
+  pref("app.update.service.enabled", true);
 #endif
 
 #ifdef XP_WIN
-// If set to true, the Update Service will attempt to use Windows BITS to
-// download updates and will fallback to downloading internally if that fails.
-pref("app.update.BITS.enabled", true);
+  // If set to true, the Update Service will attempt to use Windows BITS to
+  // download updates and will fallback to downloading internally if that fails.
+  pref("app.update.BITS.enabled", true);
 #endif
 
 // Symmetric (can be overridden by individual extensions) update preferences.
 // e.g.
 //  extensions.{GUID}.update.enabled
 //  extensions.{GUID}.update.url
 //  .. etc ..
 //
@@ -178,38 +172,38 @@ pref("extensions.update.enabled", true);
 pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.interval", 86400);  // Check for updates to Extensions and
                                             // Themes every day
 
 pref("lightweightThemes.getMoreURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes");
 
 #if defined(MOZ_WIDEVINE_EME)
-pref("browser.eme.ui.enabled", true);
+  pref("browser.eme.ui.enabled", true);
 #else
-pref("browser.eme.ui.enabled", false);
+  pref("browser.eme.ui.enabled", false);
 #endif
 
 // UI tour experience.
 pref("browser.uitour.enabled", true);
 pref("browser.uitour.loglevel", "Error");
 pref("browser.uitour.requireSecure", true);
 pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
 pref("browser.uitour.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tour/");
 // How long to show a Hearbeat survey (two hours, in seconds)
 pref("browser.uitour.surveyDuration", 7200);
 
 pref("keyword.enabled", true);
 pref("browser.fixup.domainwhitelist.localhost", true);
 
 pref("general.smoothScroll", true);
 #ifdef UNIX_BUT_NOT_MAC
-pref("general.autoScroll", false);
+  pref("general.autoScroll", false);
 #else
-pref("general.autoScroll", true);
+  pref("general.autoScroll", true);
 #endif
 
 pref("browser.stopReloadAnimation.enabled", true);
 
 // UI density of the browser chrome. This mostly affects toolbarbutton
 // and urlbar spacing. The possible values are 0=normal, 1=compact, 2=touch.
 pref("browser.uidensity", 0);
 // Whether Firefox will automatically override the uidensity to "touch"
@@ -231,19 +225,19 @@ pref("browser.startup.page",            
 pref("browser.startup.homepage",            "about:home");
 // Whether we should skip the homepage when opening the first-run page
 pref("browser.startup.firstrunSkipsHomepage", true);
 
 // Show an about:blank window as early as possible for quick startup feedback.
 // Held to nightly on Linux due to bug 1450626.
 // Disabled on Mac because the bouncing dock icon already provides feedback.
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) && defined(NIGHTLY_BUILD)
-pref("browser.startup.blankWindow", true);
+  pref("browser.startup.blankWindow", true);
 #else
-pref("browser.startup.blankWindow", false);
+  pref("browser.startup.blankWindow", false);
 #endif
 
 // Don't create the hidden window during startup on
 // platforms that don't always need it (Win/Linux).
 pref("toolkit.lazyHiddenWindow", true);
 
 pref("browser.slowStartup.notificationDisabled", false);
 pref("browser.slowStartup.timeThreshold", 20000);
@@ -252,24 +246,24 @@ pref("browser.slowStartup.maxSamples", 5
 pref("browser.enable_automatic_image_resizing", true);
 pref("browser.chrome.site_icons", true);
 // browser.warnOnQuit == false will override all other possible prompts when quitting or restarting
 pref("browser.warnOnQuit", true);
 pref("browser.fullscreen.autohide", true);
 pref("browser.overlink-delay", 80);
 
 #ifdef UNIX_BUT_NOT_MAC
-pref("browser.urlbar.clickSelectsAll", false);
+  pref("browser.urlbar.clickSelectsAll", false);
 #else
-pref("browser.urlbar.clickSelectsAll", true);
+  pref("browser.urlbar.clickSelectsAll", true);
 #endif
 #ifdef UNIX_BUT_NOT_MAC
-pref("browser.urlbar.doubleClickSelectsAll", true);
+  pref("browser.urlbar.doubleClickSelectsAll", true);
 #else
-pref("browser.urlbar.doubleClickSelectsAll", false);
+  pref("browser.urlbar.doubleClickSelectsAll", false);
 #endif
 
 // Whether using `ctrl` when hitting return/enter in the URL bar
 // (or clicking 'go') should prefix 'www.' and suffix
 // browser.fixup.alternate.suffix to the URL bar value prior to
 // navigating.
 pref("browser.urlbar.ctrlCanonizesURLs", true);
 
@@ -357,17 +351,17 @@ pref("browser.download.animateNotificati
 // This records whether or not the panel has been shown at least once.
 pref("browser.download.panel.shown", false);
 
 // This controls whether the button is automatically shown/hidden depending
 // on whether there are downloads to show.
 pref("browser.download.autohideButton", true);
 
 #ifndef XP_MACOSX
-pref("browser.helperApps.deleteTempFileOnExit", true);
+  pref("browser.helperApps.deleteTempFileOnExit", true);
 #endif
 
 // search engines URL
 pref("browser.search.searchEnginesURL",      "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/");
 
 // Market-specific search defaults
 pref("browser.search.geoSpecificDefaults", true);
 pref("browser.search.geoSpecificDefaults.url", "https://search.services.mozilla.com/1/%APP%/%VERSION%/%CHANNEL%/%LOCALE%/%REGION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%");
@@ -393,30 +387,30 @@ pref("permissions.manager.defaultsUrl", 
 // the user to be able to globally change.
 pref("permissions.default.camera", 0);
 pref("permissions.default.microphone", 0);
 pref("permissions.default.geo", 0);
 pref("permissions.default.desktop-notification", 0);
 pref("permissions.default.shortcuts", 0);
 
 #ifdef EARLY_BETA_OR_EARLIER
-pref("permissions.desktop-notification.postPrompt.enabled", true);
+  pref("permissions.desktop-notification.postPrompt.enabled", true);
 #else
-pref("permissions.desktop-notification.postPrompt.enabled", false);
+  pref("permissions.desktop-notification.postPrompt.enabled", false);
 #endif
 
 pref("permissions.fullscreen.allowed", false);
 
 pref("permissions.postPrompt.animate", true);
 
 // This is primarily meant to be enabled for studies.
 #ifdef NIGHTLY_BUILD
-pref("permissions.eventTelemetry.enabled", true);
+  pref("permissions.eventTelemetry.enabled", true);
 #else
-pref("permissions.eventTelemetry.enabled", false);
+  pref("permissions.eventTelemetry.enabled", false);
 #endif
 
 // handle links targeting new windows
 // 1=current window/tab, 2=new window, 3=new tab in most recent window
 pref("browser.link.open_newwindow", 3);
 
 // handle external links (i.e. links opened from a different application)
 // default: use browser.link.open_newwindow
@@ -429,19 +423,19 @@ pref("browser.link.open_newwindow.overri
 pref("browser.link.open_newwindow.restriction", 2);
 
 // If true, this pref causes windows opened by window.open to be forced into new
 // tabs (rather than potentially opening separate windows, depending on
 // window.open arguments) when the browser is in fullscreen mode.
 // We set this differently on Mac because the fullscreen implementation there is
 // different.
 #ifdef XP_MACOSX
-pref("browser.link.open_newwindow.disabled_in_fullscreen", true);
+  pref("browser.link.open_newwindow.disabled_in_fullscreen", true);
 #else
-pref("browser.link.open_newwindow.disabled_in_fullscreen", false);
+  pref("browser.link.open_newwindow.disabled_in_fullscreen", false);
 #endif
 
 // Tabbed browser
 pref("browser.tabs.multiselect", true);
 pref("browser.tabs.closeTabByDblclick", false);
 pref("browser.tabs.closeWindowWithLastTab", true);
 // Open related links to a tab, e.g., link in current tab, at next to the
 // current tab if |insertRelatedAfterCurrent| is true.  Otherwise, always
@@ -461,17 +455,17 @@ pref("browser.tabs.opentabfor.middleclic
 pref("browser.tabs.loadDivertedInBackground", false);
 pref("browser.tabs.loadBookmarksInBackground", false);
 pref("browser.tabs.loadBookmarksInTabs", false);
 pref("browser.tabs.tabClipWidth", 140);
 pref("browser.tabs.tabMinWidth", 76);
 // Initial titlebar state is managed by -moz-gtk-csd-hide-titlebar-by-default
 // on Linux.
 #ifndef UNIX_BUT_NOT_MAC
-pref("browser.tabs.drawInTitlebar", true);
+  pref("browser.tabs.drawInTitlebar", true);
 #endif
 
 // Offer additional drag space to the user. The drag space
 // will only be shown if browser.tabs.drawInTitlebar is true.
 pref("browser.tabs.extraDragSpace", false);
 
 // When tabs opened by links in other tabs via a combination of
 // browser.link.open_newwindow being set to 3 and target="_blank" etc are
@@ -480,39 +474,39 @@ pref("browser.tabs.extraDragSpace", fals
 // false  return to the adjacent tab (old default)
 pref("browser.tabs.selectOwnerOnClose", true);
 
 pref("browser.tabs.showAudioPlayingIcon", true);
 // This should match Chromium's audio indicator delay.
 pref("browser.tabs.delayHidingAudioPlayingIconMS", 3000);
 
 #if defined(NIGHTLY_BUILD) && !defined(MOZ_ASAN)
-// Pref to control whether we use a separate privileged content process
-// for about: pages. This pref name did not age well: we will have multiple
-// types of privileged content processes, each with different privileges.
-// types of privleged content processes, each with different privleges.
-pref("browser.tabs.remote.separatePrivilegedContentProcess", true);
-// Pref to control whether we use a separate privileged content process
-// for certain mozilla webpages (which are listed in the pref
-// browser.tabs.remote.separatedMozillaDomains).
-pref("browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", false);
-// This pref will cause assertions when a remoteType triggers a process switch
-// to a new remoteType it should not be able to trigger.
-pref("browser.tabs.remote.enforceRemoteTypeRestrictions", true);
+  // Pref to control whether we use a separate privileged content process
+  // for about: pages. This pref name did not age well: we will have multiple
+  // types of privileged content processes, each with different privileges.
+  // types of privleged content processes, each with different privleges.
+  pref("browser.tabs.remote.separatePrivilegedContentProcess", true);
+  // Pref to control whether we use a separate privileged content process
+  // for certain mozilla webpages (which are listed in the pref
+  // browser.tabs.remote.separatedMozillaDomains).
+  pref("browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", false);
+  // This pref will cause assertions when a remoteType triggers a process switch
+  // to a new remoteType it should not be able to trigger.
+  pref("browser.tabs.remote.enforceRemoteTypeRestrictions", true);
 #endif
 
 #ifdef NIGHTLY_BUILD
-// allow_eval_* is enabled on Firefox Desktop only at this
-// point in time
-pref("security.allow_eval_with_system_principal", false);
-pref("security.allow_eval_in_parent_process", false);
-pref("browser.tabs.remote.useHTTPResponseProcessSelection", true);
+  // allow_eval_* is enabled on Firefox Desktop only at this
+  // point in time
+  pref("security.allow_eval_with_system_principal", false);
+  pref("security.allow_eval_in_parent_process", false);
+  pref("browser.tabs.remote.useHTTPResponseProcessSelection", true);
 #else
-// Disabled outside of nightly due to bug 1554217
-pref("browser.tabs.remote.useHTTPResponseProcessSelection", false);
+  // Disabled outside of nightly due to bug 1554217
+  pref("browser.tabs.remote.useHTTPResponseProcessSelection", false);
 #endif
 
 
 // Unload tabs when available memory is running low
 pref("browser.tabs.unloadOnLowMemory", false);
 
 pref("browser.ctrlTab.recentlyUsedOrder", true);
 
@@ -602,34 +596,34 @@ pref("privacy.spoof_english", 0);
 pref("network.proxy.share_proxy_settings",  false); // use the same proxy settings for all protocols
 
 // simple gestures support
 pref("browser.gesture.swipe.left", "Browser:BackOrBackDuplicate");
 pref("browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate");
 pref("browser.gesture.swipe.up", "cmd_scrollTop");
 pref("browser.gesture.swipe.down", "cmd_scrollBottom");
 #ifdef XP_MACOSX
-pref("browser.gesture.pinch.latched", true);
-pref("browser.gesture.pinch.threshold", 150);
+  pref("browser.gesture.pinch.latched", true);
+  pref("browser.gesture.pinch.threshold", 150);
 #else
-pref("browser.gesture.pinch.latched", false);
-pref("browser.gesture.pinch.threshold", 25);
+  pref("browser.gesture.pinch.latched", false);
+  pref("browser.gesture.pinch.threshold", 25);
 #endif
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
-// Enabled for touch input display zoom.
-pref("browser.gesture.pinch.out", "cmd_fullZoomEnlarge");
-pref("browser.gesture.pinch.in", "cmd_fullZoomReduce");
-pref("browser.gesture.pinch.out.shift", "cmd_fullZoomReset");
-pref("browser.gesture.pinch.in.shift", "cmd_fullZoomReset");
+  // Enabled for touch input display zoom.
+  pref("browser.gesture.pinch.out", "cmd_fullZoomEnlarge");
+  pref("browser.gesture.pinch.in", "cmd_fullZoomReduce");
+  pref("browser.gesture.pinch.out.shift", "cmd_fullZoomReset");
+  pref("browser.gesture.pinch.in.shift", "cmd_fullZoomReset");
 #else
-// Disabled by default due to issues with track pad input.
-pref("browser.gesture.pinch.out", "");
-pref("browser.gesture.pinch.in", "");
-pref("browser.gesture.pinch.out.shift", "");
-pref("browser.gesture.pinch.in.shift", "");
+  // Disabled by default due to issues with track pad input.
+  pref("browser.gesture.pinch.out", "");
+  pref("browser.gesture.pinch.in", "");
+  pref("browser.gesture.pinch.out.shift", "");
+  pref("browser.gesture.pinch.in.shift", "");
 #endif
 pref("browser.gesture.twist.latched", false);
 pref("browser.gesture.twist.threshold", 0);
 pref("browser.gesture.twist.right", "cmd_gestureRotateRight");
 pref("browser.gesture.twist.left", "cmd_gestureRotateLeft");
 pref("browser.gesture.twist.end", "cmd_gestureRotateEnd");
 pref("browser.gesture.tap", "cmd_fullZoomReset");
 
@@ -637,41 +631,41 @@ pref("browser.history_swipe_animation.di
 
 // 0: Nothing happens
 // 1: Scrolling contents
 // 2: Go back or go forward, in your history
 // 3: Zoom in or out (reflowing zoom).
 // 4: Treat vertical wheel as horizontal scroll
 // 5: Zoom in or out (pinch zoom).
 #ifdef XP_MACOSX
-// On macOS, if the wheel has one axis only, shift+wheel comes through as a
-// horizontal scroll event. Thus, we can't assign anything other than normal
-// scrolling to shift+wheel.
-pref("mousewheel.with_shift.action", 1);
-pref("mousewheel.with_alt.action", 2);
-// On MacOS X, control+wheel is typically handled by system and we don't
-// receive the event.  So, command key which is the main modifier key for
-// acceleration is the best modifier for zoom-in/out.  However, we should keep
-// the control key setting for backward compatibility.
-pref("mousewheel.with_meta.action", 3); // command key on Mac
-// Disable control-/meta-modified horizontal wheel events, since those are
-// used on Mac as part of modified swipe gestures (e.g. Left swipe+Cmd is
-// "go back" in a new tab).
-pref("mousewheel.with_control.action.override_x", 0);
-pref("mousewheel.with_meta.action.override_x", 0);
+  // On macOS, if the wheel has one axis only, shift+wheel comes through as a
+  // horizontal scroll event. Thus, we can't assign anything other than normal
+  // scrolling to shift+wheel.
+  pref("mousewheel.with_shift.action", 1);
+  pref("mousewheel.with_alt.action", 2);
+  // On MacOS X, control+wheel is typically handled by system and we don't
+  // receive the event.  So, command key which is the main modifier key for
+  // acceleration is the best modifier for zoom-in/out.  However, we should keep
+  // the control key setting for backward compatibility.
+  pref("mousewheel.with_meta.action", 3); // command key on Mac
+  // Disable control-/meta-modified horizontal wheel events, since those are
+  // used on Mac as part of modified swipe gestures (e.g. Left swipe+Cmd is
+  // "go back" in a new tab).
+  pref("mousewheel.with_control.action.override_x", 0);
+  pref("mousewheel.with_meta.action.override_x", 0);
 #else
-// On the other platforms (non-macOS), user may use legacy mouse which supports
-// only vertical wheel but want to scroll horizontally.  For such users, we
-// should provide horizontal scroll with shift+wheel (same as Chrome).
-// However, shift+wheel was used for navigating history.  For users who want
-// to keep using this feature, let's enable it with alt+wheel.  This is better
-// for consistency with macOS users.
-pref("mousewheel.with_shift.action", 4);
-pref("mousewheel.with_alt.action", 2);
-pref("mousewheel.with_meta.action", 1); // win key on Win, Super/Hyper on Linux
+  // On the other platforms (non-macOS), user may use legacy mouse which
+  // supports only vertical wheel but want to scroll horizontally.  For such
+  // users, we should provide horizontal scroll with shift+wheel (same as
+  // Chrome). However, shift+wheel was used for navigating history.  For users
+  // who want to keep using this feature, let's enable it with alt+wheel.  This
+  // is better for consistency with macOS users.
+  pref("mousewheel.with_shift.action", 4);
+  pref("mousewheel.with_alt.action", 2);
+  pref("mousewheel.with_meta.action", 1); // win key on Win, Super/Hyper on Linux
 #endif
 pref("mousewheel.with_control.action",3);
 pref("mousewheel.with_win.action", 1);
 
 pref("browser.xul.error_pages.expert_bad_cert", false);
 pref("browser.xul.error_pages.show_safe_browsing_details_on_load", false);
 
 // Enable captive portal detection.
@@ -681,26 +675,26 @@ pref("network.captive-portal-service.ena
 pref("network.manage-offline-status", true);
 
 // We want to make sure mail URLs are handled externally...
 pref("network.protocol-handler.external.mailto", true); // for mail
 pref("network.protocol-handler.external.news", true);   // for news
 pref("network.protocol-handler.external.snews", true);  // for secure news
 pref("network.protocol-handler.external.nntp", true);   // also news
 #ifdef XP_WIN
-pref("network.protocol-handler.external.ms-windows-store", true);
+  pref("network.protocol-handler.external.ms-windows-store", true);
 #endif
 
 // ...without warning dialogs
 pref("network.protocol-handler.warn-external.mailto", false);
 pref("network.protocol-handler.warn-external.news", false);
 pref("network.protocol-handler.warn-external.snews", false);
 pref("network.protocol-handler.warn-external.nntp", false);
 #ifdef XP_WIN
-pref("network.protocol-handler.warn-external.ms-windows-store", false);
+  pref("network.protocol-handler.warn-external.ms-windows-store", false);
 #endif
 
 // By default, all protocol handlers are exposed.  This means that
 // the browser will respond to openURL commands for all URL types.
 // It will also try to open link clicks inside the browser before
 // failing over to the system handlers.
 pref("network.protocol-handler.expose-all", true);
 pref("network.protocol-handler.expose.mailto", false);
@@ -718,65 +712,64 @@ pref("accessibility.support.url", "https
 pref("accessibility.indicator.enabled", false);
 
 pref("plugins.testmode", false);
 
 // Should plugins that are hidden show the infobar UI?
 pref("plugins.show_infobar", false);
 
 #if defined(_ARM64_) && defined(XP_WIN)
-pref("plugin.default.state", 0);
+  pref("plugin.default.state", 0);
 #else
-pref("plugin.default.state", 1);
+  pref("plugin.default.state", 1);
 #endif
 
 // Plugins bundled in XPIs are enabled by default.
 pref("plugin.defaultXpi.state", 2);
 
 // Flash is Click-to-Activate by default on all channels. Disabled for ARM builds.
 #if defined(_ARM64_) && defined(XP_WIN)
-pref("plugin.state.flash", 0);
+  pref("plugin.state.flash", 0);
 #else
-pref("plugin.state.flash", 1);
+  pref("plugin.state.flash", 1);
 #endif
 
 // Enables the download and use of the flash blocklists.
 pref("plugins.flashBlock.enabled", true);
 
 // Prefer HTML5 video over Flash content, and don't
 // load plugin instances with no src declared.
 // These prefs are documented in details on all.js.
 // With the "follow-ctp" setting, this will only
 // apply to users that have plugin.state.flash = 1.
 pref("plugins.favorfallback.mode", "follow-ctp");
 pref("plugins.favorfallback.rules", "nosrc,video");
 
-
 #ifdef XP_WIN
-pref("browser.preferences.instantApply", false);
+  pref("browser.preferences.instantApply", false);
 #else
-pref("browser.preferences.instantApply", true);
+  pref("browser.preferences.instantApply", true);
 #endif
 
 // Toggling Search bar on and off in about:preferences
 pref("browser.preferences.search", true);
 
 pref("browser.preferences.defaultPerformanceSettings.enabled", true);
 
 pref("browser.download.show_plugins_in_list", true);
 pref("browser.download.hide_plugins_without_extensions", true);
 
 // Backspace and Shift+Backspace behavior
 // 0 goes Back/Forward
 // 1 act like PgUp/PgDown
 // 2 and other values, nothing
 #ifdef UNIX_BUT_NOT_MAC
-pref("browser.backspace_action", 2);
+  pref("browser.backspace_action", 2);
 #else
-pref("browser.backspace_action", 0);
+  pref("browser.backspace_action", 0);
 #endif
 
 // this will automatically enable inline spellchecking (if it is available) for
 // editable elements in HTML
 // 0 = spellcheck nothing
 // 1 = check multi-line controls [default]
 // 2 = check multi/single line controls
 pref("layout.spellcheckDefault", 1);
@@ -972,19 +965,19 @@ pref("toolkit.datacollection.infoURL",
 pref("toolkit.crashreporter.infoURL",
      "https://www.mozilla.org/legal/privacy/firefox.html#crash-reporter");
 
 // base URL for web-based support pages
 pref("app.support.baseURL", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/");
 
 // base url for web-based feedback pages
 #ifdef MOZ_DEV_EDITION
-pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/firefoxdev/%VERSION%/");
+  pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/firefoxdev/%VERSION%/");
 #else
-pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/%APP%/%VERSION%/");
+  pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/%APP%/%VERSION%/");
 #endif
 
 // base URL for web-based marketing pages
 pref("app.productInfo.baseURL", "https://www.mozilla.org/firefox/features/");
 
 // Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
 pref("security.alternate_certificate_error_page", "certerror");
 
@@ -1014,169 +1007,169 @@ pref("browser.flash-protected-mode-flip.
 pref("browser.flash-protected-mode-flip.done", false);
 
 // Dark in-content pages
 pref("browser.in-content.dark-mode", true);
 
 pref("dom.ipc.shims.enabledWarnings", false);
 
 #if defined(XP_WIN) && defined(MOZ_SANDBOX)
-// Controls whether and how the Windows NPAPI plugin process is sandboxed.
-// To get a different setting for a particular plugin replace "default", with
-// the plugin's nice file name, see: nsPluginTag::GetNiceFileName.
-// On windows these levels are:
-// 0 - no sandbox
-// 1 - sandbox with USER_NON_ADMIN access token level
-// 2 - a more strict sandbox, which might cause functionality issues. This now
-//     includes running at low integrity.
-// 3 - the strongest settings we seem to be able to use without breaking
-//     everything, but will probably cause some functionality restrictions
-pref("dom.ipc.plugins.sandbox-level.default", 0);
-#if defined(_AMD64_)
-// The base sandbox level in nsPluginTag::InitSandboxLevel must be
-// updated to keep in sync with this value.
-pref("dom.ipc.plugins.sandbox-level.flash", 3);
-#else
-pref("dom.ipc.plugins.sandbox-level.flash", 0);
-#endif
+  // Controls whether and how the Windows NPAPI plugin process is sandboxed.
+  // To get a different setting for a particular plugin replace "default", with
+  // the plugin's nice file name, see: nsPluginTag::GetNiceFileName.
+  // On windows these levels are:
+  // 0 - no sandbox
+  // 1 - sandbox with USER_NON_ADMIN access token level
+  // 2 - a more strict sandbox, which might cause functionality issues. This now
+  //     includes running at low integrity.
+  // 3 - the strongest settings we seem to be able to use without breaking
+  //     everything, but will probably cause some functionality restrictions
+  pref("dom.ipc.plugins.sandbox-level.default", 0);
+  #if defined(_AMD64_)
+    // The base sandbox level in nsPluginTag::InitSandboxLevel must be
+    // updated to keep in sync with this value.
+    pref("dom.ipc.plugins.sandbox-level.flash", 3);
+  #else
+    pref("dom.ipc.plugins.sandbox-level.flash", 0);
+  #endif
 
-// This controls the strength of the Windows content process sandbox for testing
-// purposes. This will require a restart.
-// On windows these levels are:
-// See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
-// SetSecurityLevelForContentProcess() for what the different settings mean.
-pref("security.sandbox.content.level", 5);
+  // This controls the strength of the Windows content process sandbox for
+  // testing purposes. This will require a restart.
+  // On windows these levels are:
+  // See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
+  // SetSecurityLevelForContentProcess() for what the different settings mean.
+  pref("security.sandbox.content.level", 5);
 
-// This controls the depth of stack trace that is logged when Windows sandbox
-// logging is turned on.  This is only currently available for the content
-// process because the only other sandbox (for GMP) has too strict a policy to
-// allow stack tracing.  This does not require a restart to take effect.
-pref("security.sandbox.windows.log.stackTraceDepth", 0);
+  // This controls the depth of stack trace that is logged when Windows sandbox
+  // logging is turned on.  This is only currently available for the content
+  // process because the only other sandbox (for GMP) has too strict a policy to
+  // allow stack tracing.  This does not require a restart to take effect.
+  pref("security.sandbox.windows.log.stackTraceDepth", 0);
 
-// This controls the strength of the Windows GPU process sandbox.  Changes
-// will require restart.
-// For information on what the level number means, see
-// SetSecurityLevelForGPUProcess() in
-// security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
-pref("security.sandbox.gpu.level", 0);
+  // This controls the strength of the Windows GPU process sandbox.  Changes
+  // will require restart.
+  // For information on what the level number means, see
+  // SetSecurityLevelForGPUProcess() in
+  // security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
+  pref("security.sandbox.gpu.level", 0);
 
-// Controls whether we disable win32k for the processes.
-// true means that win32k system calls are not permitted.
-pref("security.sandbox.rdd.win32k-disable", true);
-// Note: win32k is currently _not_ disabled for GMP due to intermittent test
-// failures, where the GMP process fails very early. See bug 1449348.
-pref("security.sandbox.gmp.win32k-disable", false);
+  // Controls whether we disable win32k for the processes.
+  // true means that win32k system calls are not permitted.
+  pref("security.sandbox.rdd.win32k-disable", true);
+  // Note: win32k is currently _not_ disabled for GMP due to intermittent test
+  // failures, where the GMP process fails very early. See bug 1449348.
+  pref("security.sandbox.gmp.win32k-disable", false);
 #endif
 
 #if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
-// Start the Mac sandbox early during child process startup instead
-// of when messaged by the parent after the message loop is running.
-pref("security.sandbox.content.mac.earlyinit", true);
-// Remove this pref once RDD early init is stable on Release.
-pref("security.sandbox.rdd.mac.earlyinit", true);
-// Remove this pref once GMP early init is stable on Release.
-pref("security.sandbox.gmp.mac.earlyinit", true);
+  // Start the Mac sandbox early during child process startup instead
+  // of when messaged by the parent after the message loop is running.
+  pref("security.sandbox.content.mac.earlyinit", true);
+  // Remove this pref once RDD early init is stable on Release.
+  pref("security.sandbox.rdd.mac.earlyinit", true);
+  // Remove this pref once GMP early init is stable on Release.
+  pref("security.sandbox.gmp.mac.earlyinit", true);
 
-// This pref is discussed in bug 1083344, the naming is inspired from its
-// Windows counterpart, but on Mac it's an integer which means:
-// 0 -> "no sandbox" (nightly only)
-// 1 -> "preliminary content sandboxing enabled: write access to
-//       home directory is prevented"
-// 2 -> "preliminary content sandboxing enabled with profile protection:
-//       write access to home directory is prevented, read and write access
-//       to ~/Library and profile directories are prevented (excluding
-//       $PROFILE/{extensions,chrome})"
-// 3 -> "no global read/write access, read access permitted to
-//       $PROFILE/{extensions,chrome}"
-// This setting is read when the content process is started. On Mac the content
-// process is killed when all windows are closed, so a change will take effect
-// when the 1st window is opened.
-pref("security.sandbox.content.level", 3);
+  // This pref is discussed in bug 1083344, the naming is inspired from its
+  // Windows counterpart, but on Mac it's an integer which means:
+  // 0 -> "no sandbox" (nightly only)
+  // 1 -> "preliminary content sandboxing enabled: write access to
+  //       home directory is prevented"
+  // 2 -> "preliminary content sandboxing enabled with profile protection:
+  //       write access to home directory is prevented, read and write access
+  //       to ~/Library and profile directories are prevented (excluding
+  //       $PROFILE/{extensions,chrome})"
+  // 3 -> "no global read/write access, read access permitted to
+  //       $PROFILE/{extensions,chrome}"
+  // This setting is read when the content process is started. On Mac the
+  // content process is killed when all windows are closed, so a change will
+  // take effect when the 1st window is opened.
+  pref("security.sandbox.content.level", 3);
 
-// Prefs for controlling whether and how the Mac NPAPI Flash plugin process is
-// sandboxed. On Mac these levels are:
-// 0 - "no sandbox"
-// 1 - "global read access, limited write access for Flash functionality"
-// 2 - "read access triggered by file dialog activity, limited read/write"
-//     "access for Flash functionality"
-// 3 - "limited read/write access for Flash functionality"
-pref("dom.ipc.plugins.sandbox-level.flash", 1);
-// Controls the level used on older OS X versions. Is overriden when the
-// "dom.ipc.plugins.sandbox-level.flash" is set to 0.
-pref("dom.ipc.plugins.sandbox-level.flash.legacy", 1);
-// The max OS minor version where we use the above legacy sandbox level.
-pref("dom.ipc.plugins.sandbox-level.flash.max-legacy-os-minor", 10);
-// Controls the sandbox level used by plugins other than Flash. On Mac,
-// no other plugins are supported and this pref is only used for test
-// plugins used in automated tests.
-pref("dom.ipc.plugins.sandbox-level.default", 1);
+  // Prefs for controlling whether and how the Mac NPAPI Flash plugin process is
+  // sandboxed. On Mac these levels are:
+  // 0 - "no sandbox"
+  // 1 - "global read access, limited write access for Flash functionality"
+  // 2 - "read access triggered by file dialog activity, limited read/write"
+  //     "access for Flash functionality"
+  // 3 - "limited read/write access for Flash functionality"
+  pref("dom.ipc.plugins.sandbox-level.flash", 1);
+  // Controls the level used on older OS X versions. Is overriden when the
+  // "dom.ipc.plugins.sandbox-level.flash" is set to 0.
+  pref("dom.ipc.plugins.sandbox-level.flash.legacy", 1);
+  // The max OS minor version where we use the above legacy sandbox level.
+  pref("dom.ipc.plugins.sandbox-level.flash.max-legacy-os-minor", 10);
+  // Controls the sandbox level used by plugins other than Flash. On Mac,
+  // no other plugins are supported and this pref is only used for test
+  // plugins used in automated tests.
+  pref("dom.ipc.plugins.sandbox-level.default", 1);
 #endif
 
 #if defined(XP_LINUX) && defined(MOZ_SANDBOX)
-// This pref is introduced as part of bug 742434, the naming is inspired from
-// its Windows/Mac counterpart, but on Linux it's an integer which means:
-// 0 -> "no sandbox"
-// 1 -> "content sandbox using seccomp-bpf when available" + ipc restrictions
-// 2 -> "seccomp-bpf + write file broker"
-// 3 -> "seccomp-bpf + read/write file brokering"
-// 4 -> all of the above + network/socket restrictions + chroot
-//
-// The purpose of this setting is to allow Linux users or distros to disable
-// the sandbox while we fix their problems, or to allow running Firefox with
-// exotic configurations we can't reasonably support out of the box.
-//
-pref("security.sandbox.content.level", 4);
-pref("security.sandbox.content.write_path_whitelist", "");
-pref("security.sandbox.content.read_path_whitelist", "");
-pref("security.sandbox.content.syscall_whitelist", "");
+  // This pref is introduced as part of bug 742434, the naming is inspired from
+  // its Windows/Mac counterpart, but on Linux it's an integer which means:
+  // 0 -> "no sandbox"
+  // 1 -> "content sandbox using seccomp-bpf when available" + ipc restrictions
+  // 2 -> "seccomp-bpf + write file broker"
+  // 3 -> "seccomp-bpf + read/write file brokering"
+  // 4 -> all of the above + network/socket restrictions + chroot
+  //
+  // The purpose of this setting is to allow Linux users or distros to disable
+  // the sandbox while we fix their problems, or to allow running Firefox with
+  // exotic configurations we can't reasonably support out of the box.
+  //
+  pref("security.sandbox.content.level", 4);
+  pref("security.sandbox.content.write_path_whitelist", "");
+  pref("security.sandbox.content.read_path_whitelist", "");
+  pref("security.sandbox.content.syscall_whitelist", "");
 #endif
 
 #if defined(XP_OPENBSD) && defined(MOZ_SANDBOX)
-// default pledge strings for the main & content processes, cf bug 1457092
-// broad list for now, has to be refined over time
-pref("security.sandbox.pledge.main", "stdio rpath wpath cpath inet proc exec prot_exec flock ps sendfd recvfd dns vminfo tty drm unix fattr getpw mcast");
-pref("security.sandbox.content.level", 1);
-pref("security.sandbox.pledge.content", "stdio rpath wpath cpath inet recvfd sendfd prot_exec unix drm ps");
+  // default pledge strings for the main & content processes, cf bug 1457092
+  // broad list for now, has to be refined over time
+  pref("security.sandbox.pledge.main", "stdio rpath wpath cpath inet proc exec prot_exec flock ps sendfd recvfd dns vminfo tty drm unix fattr getpw mcast");
+  pref("security.sandbox.content.level", 1);
+  pref("security.sandbox.pledge.content", "stdio rpath wpath cpath inet recvfd sendfd prot_exec unix drm ps");
 #endif
 
 #if defined(MOZ_SANDBOX)
-// ID (a UUID when set by gecko) that is used to form the name of a
-// sandbox-writable temporary directory to be used by content processes
-// when a temporary writable file is required in a level 1 sandbox.
-pref("security.sandbox.content.tempDirSuffix", "");
-pref("security.sandbox.plugin.tempDirSuffix", "");
+  // ID (a UUID when set by gecko) that is used to form the name of a
+  // sandbox-writable temporary directory to be used by content processes
+  // when a temporary writable file is required in a level 1 sandbox.
+  pref("security.sandbox.content.tempDirSuffix", "");
+  pref("security.sandbox.plugin.tempDirSuffix", "");
 
-// This pref determines if messages relevant to sandbox violations are
-// logged.
-#if defined(XP_WIN) || defined(XP_MACOSX)
-pref("security.sandbox.logging.enabled", false);
-#endif
+  // This pref determines if messages relevant to sandbox violations are
+  // logged.
+  #if defined(XP_WIN) || defined(XP_MACOSX)
+    pref("security.sandbox.logging.enabled", false);
+  #endif
 #endif
 
 // This pref governs whether we attempt to work around problems caused by
 // plugins using OS calls to manipulate the cursor while running out-of-
 // process.  These workarounds all involve intercepting (hooking) certain
 // OS calls in the plugin process, then arranging to make certain OS calls
 // in the browser process.  Eventually plugins will be required to use the
 // NPAPI to manipulate the cursor, and these workarounds will be removed.
 // See bug 621117.
 #ifdef XP_MACOSX
-pref("dom.ipc.plugins.nativeCursorSupport", true);
+  pref("dom.ipc.plugins.nativeCursorSupport", true);
 #endif
 
 #ifdef XP_WIN
-pref("browser.taskbar.previews.enable", false);
-pref("browser.taskbar.previews.max", 20);
-pref("browser.taskbar.previews.cachetime", 5);
-pref("browser.taskbar.lists.enabled", true);
-pref("browser.taskbar.lists.frequent.enabled", true);
-pref("browser.taskbar.lists.recent.enabled", false);
-pref("browser.taskbar.lists.maxListItemCount", 7);
-pref("browser.taskbar.lists.tasks.enabled", true);
-pref("browser.taskbar.lists.refreshInSeconds", 120);
+  pref("browser.taskbar.previews.enable", false);
+  pref("browser.taskbar.previews.max", 20);
+  pref("browser.taskbar.previews.cachetime", 5);
+  pref("browser.taskbar.lists.enabled", true);
+  pref("browser.taskbar.lists.frequent.enabled", true);
+  pref("browser.taskbar.lists.recent.enabled", false);
+  pref("browser.taskbar.lists.maxListItemCount", 7);
+  pref("browser.taskbar.lists.tasks.enabled", true);
+  pref("browser.taskbar.lists.refreshInSeconds", 120);
 #endif
 
 // Preferences to be synced by default
 pref("services.sync.prefs.sync.accessibility.blockautorefresh", true);
 pref("services.sync.prefs.sync.accessibility.browsewithcaret", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
 pref("services.sync.prefs.sync.addons.ignoreUserEnabledChanges", true);
@@ -1287,17 +1280,17 @@ pref("prompts.tab_modal.enabled", true);
 // Activates preloading of the new tab url.
 pref("browser.newtab.preload", true);
 
 // Indicates if about:newtab shows content (enabled) or just blank
 pref("browser.newtabpage.enabled", true);
 
 // Activity Stream prefs that control to which page to redirect
 #ifndef RELEASE_OR_BETA
-pref("browser.newtabpage.activity-stream.debug", false);
+  pref("browser.newtabpage.activity-stream.debug", false);
 #endif
 
 pref("browser.library.activity-stream.enabled", true);
 
 // The remote FxA root content URL for the Activity Stream firstrun page.
 pref("browser.newtabpage.activity-stream.fxaccounts.endpoint", "https://accounts.firefox.com/");
 
 // The pref that controls if the search shortcuts experiment is on
@@ -1305,21 +1298,30 @@ pref("browser.newtabpage.activity-stream
 
 // ASRouter provider configuration
 pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":true,\"type\":\"remote-settings\",\"bucket\":\"cfr\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]},\"categories\":[\"cfrAddons\",\"cfrFeatures\"],\"updateCycleInMs\":3600000}");
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
 pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
 
+// These prefs control if Discovery Stream is enabled.
+#ifdef NIGHTLY_BUILD
+pref("browser.newtabpage.activity-stream.discoverystream.enabled", true);
+#else
+pref("browser.newtabpage.activity-stream.discoverystream.enabled", false);
+#endif
+pref("browser.newtabpage.activity-stream.discoverystream.hardcoded-basic-layout", false);
+pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint", "");
+
 // The pref controls if search hand-off is enabled for Activity Stream.
 #ifdef NIGHTLY_BUILD
-pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true);
+  pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true);
 #else
-pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
+  pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
 #endif
 
 pref("trailhead.firstrun.branches", "join-supercharge");
 
 // The pref that controls if the What's New panel is enabled.
 pref("browser.messaging-system.whatsNewPanel.enabled", false);
 // Whether to use Messaging System to add a badge to the FxA toolbar button
 pref("browser.messaging-system.fxatoolbarbadge.enabled", true);
@@ -1330,17 +1332,17 @@ pref("full-screen-api.enabled", true);
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
 
 // Whether to use RegisterApplicationRestart to restart the browser and resume
 // the session on next Windows startup
 #if defined(XP_WIN)
-pref("toolkit.winRegisterApplicationRestart", true);
+  pref("toolkit.winRegisterApplicationRestart", true);
 #endif
 
 // Completely disable pdf.js as an option to preview pdfs within firefox.
 // Note: if this is not disabled it does not necessarily mean pdf.js is the pdf
 // handler just that it is an option.
 pref("pdfjs.disabled", false);
 // Used by pdf.js to know the first time firefox is run with it installed so it
 // can become the default pdf viewer.
@@ -1385,33 +1387,33 @@ pref("security.cert_pinning.enforcement_
 
 // If this turns true, Moz*Gesture events are not called stopPropagation()
 // before content.
 pref("dom.debug.propagate_gesture_events_through_content", false);
 
 // All the Geolocation preferences are here.
 //
 #ifndef EARLY_BETA_OR_EARLIER
-pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_LOCATION_SERVICE_API_KEY%");
+  pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_LOCATION_SERVICE_API_KEY%");
 #else
-// Use MLS on Nightly and early Beta.
-pref("geo.wifi.uri", "https://location.services.mozilla.com/v1/geolocate?key=%MOZILLA_API_KEY%");
+  // Use MLS on Nightly and early Beta.
+  pref("geo.wifi.uri", "https://location.services.mozilla.com/v1/geolocate?key=%MOZILLA_API_KEY%");
 #endif
 
 #ifdef XP_MACOSX
-pref("geo.provider.use_corelocation", true);
+  pref("geo.provider.use_corelocation", true);
 #endif
 
 // Set to false if things are really broken.
 #ifdef XP_WIN
-pref("geo.provider.ms-windows-location", true);
+  pref("geo.provider.ms-windows-location", true);
 #endif
 
 #if defined(MOZ_WIDGET_GTK) && defined(MOZ_GPSD)
-pref("geo.provider.use_gpsd", true);
+  pref("geo.provider.use_gpsd", true);
 #endif
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
 // CustomizableUI state of the browser's user interface
 pref("browser.uiCustomization.state", "");
 
@@ -1449,69 +1451,69 @@ pref("identity.fxaccounts.autoconfig.uri
 // URLs for promo links to mobile browsers. Note that consumers are expected to
 // append a value for utm_campaign.
 pref("identity.mobilepromo.android", "https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
-pref("identity.fxaccounts.migrateToDevEdition", true);
+  pref("identity.fxaccounts.migrateToDevEdition", true);
 #else
-pref("identity.fxaccounts.migrateToDevEdition", false);
+  pref("identity.fxaccounts.migrateToDevEdition", false);
 #endif
 
 // If activated, send tab will use the new FxA commands backend.
 pref("identity.fxaccounts.commands.enabled", true);
 // How often should we try to fetch missed FxA commands on sync (in seconds).
 // Default is 24 hours.
 pref("identity.fxaccounts.commands.missed.fetch_interval", 86400);
 
 // On GTK, we now default to showing the menubar only when alt is pressed:
 #ifdef MOZ_WIDGET_GTK
-pref("ui.key.menuAccessKeyFocuses", true);
+  pref("ui.key.menuAccessKeyFocuses", true);
 #endif
 
 // Whether we should run a test-pattern through EME GMPs before assuming they'll
 // decode H.264.
 pref("media.gmp.trial-create.enabled", true);
 
 // Note: when media.gmp-*.visible is true, provided we're running on a
 // supported platform/OS version, the corresponding CDM appears in the
 // plugins list, Firefox will download the GMP/CDM if enabled, and our
 // UI to re-enable EME prompts the user to re-enable EME if it's disabled
 // and script requests EME. If *.visible is false, we won't show the UI
 // to enable the CDM if its disabled; it's as if the keysystem is completely
 // unsupported.
 
 #ifdef MOZ_WIDEVINE_EME
-pref("media.gmp-widevinecdm.visible", true);
-pref("media.gmp-widevinecdm.enabled", true);
+  pref("media.gmp-widevinecdm.visible", true);
+  pref("media.gmp-widevinecdm.enabled", true);
 #endif
 
 pref("media.gmp-gmpopenh264.visible", true);
 pref("media.gmp-gmpopenh264.enabled", true);
 
 // Switch block autoplay logic to v2, and enable UI.
 pref("media.autoplay.enabled.user-gestures-needed", true);
 // Set Firefox to block autoplay, asking for permission by default.
 pref("media.autoplay.default", 1); // 0=Allowed, 1=Blocked, 5=All Blocked
 
 #ifdef NIGHTLY_BUILD
-// Block WebAudio from playing automatically.
-pref("media.autoplay.block-webaudio", true);
+  // Block WebAudio from playing automatically.
+  pref("media.autoplay.block-webaudio", true);
 #else
-pref("media.autoplay.block-webaudio", false);
+  pref("media.autoplay.block-webaudio", false);
 #endif
 
 #if defined(XP_WIN)
-#if defined(EARLY_BETA_OR_EARLIER) || defined(MOZ_DEV_EDITION)
-pref("media.videocontrols.picture-in-picture.enabled", true);
-pref("media.videocontrols.picture-in-picture.video-toggle.enabled", true);
-#endif
+  #if defined(EARLY_BETA_OR_EARLIER) || defined(MOZ_DEV_EDITION)
+    pref("media.videocontrols.picture-in-picture.enabled", true);
+    pref("media.videocontrols.picture-in-picture.video-toggle.enabled", true);
+  #endif
 #endif
 
 // Play with different values of the decay time and get telemetry,
 // 0 means to randomize (and persist) the experiment value in users' profiles,
 // -1 means no experiment is run and we use the preferred value for frecency (6h)
 pref("browser.cache.frecency_experiment", 0);
 
 pref("browser.translation.detectLanguage", false);
@@ -1535,35 +1537,35 @@ pref("toolkit.telemetry.newProfilePing.e
 // Enables sending 'update' pings on Firefox updates.
 pref("toolkit.telemetry.updatePing.enabled", true);
 // Enables sending 'bhr' pings when the browser hangs.
 pref("toolkit.telemetry.bhrPing.enabled", true);
 // Enables using Hybrid Content Telemetry from Mozilla privileged pages.
 pref("toolkit.telemetry.hybridContent.enabled", true);
 // Whether to enable Ecosystem Telemetry, requires a restart.
 #ifdef NIGHTLY_BUILD
-pref("toolkit.telemetry.ecosystemtelemetry.enabled", true);
+  pref("toolkit.telemetry.ecosystemtelemetry.enabled", true);
 #else
-pref("toolkit.telemetry.ecosystemtelemetry.enabled", false);
+  pref("toolkit.telemetry.ecosystemtelemetry.enabled", false);
 #endif
 
 // Ping Centre Telemetry settings.
 pref("browser.ping-centre.telemetry", true);
 pref("browser.ping-centre.log", false);
 pref("browser.ping-centre.staging.endpoint", "https://onyx_tiles.stage.mozaws.net/v3/links/ping-centre");
 pref("browser.ping-centre.production.endpoint", "https://tiles.services.mozilla.com/v3/links/ping-centre");
 
 // Enable GMP support in the addon manager.
 pref("media.gmp-provider.enabled", true);
 
 // Enable blocking access to storage from tracking resources by default.
 pref("network.cookie.cookieBehavior", 4 /* BEHAVIOR_REJECT_TRACKER */);
 #ifdef EARLY_BETA_OR_EARLIER
-// Enable fingerprinting blocking by default only in nightly and early beta.
-pref("privacy.trackingprotection.fingerprinting.enabled", true);
+  // Enable fingerprinting blocking by default only in nightly and early beta.
+  pref("privacy.trackingprotection.fingerprinting.enabled", true);
 #endif
 
 // Enable cryptomining blocking by default for all channels, only on desktop.
 pref("privacy.trackingprotection.cryptomining.enabled", true);
 
 pref("browser.contentblocking.database.enabled", true);
 
 pref("dom.storage_access.enabled", true);
@@ -1609,72 +1611,75 @@ pref("browser.contentblocking.customBloc
 pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
 
 // Enable Protections report's Lockwise card by default.
 pref("browser.contentblocking.report.lockwise.enabled", true);
 
 // Enable Protections report's Monitor card by default.
 pref("browser.contentblocking.report.monitor.enabled", true);
 
-pref("browser.contentblocking.report.monitor.url", "https://monitor.firefox.com");
+pref("browser.contentblocking.report.monitor.url", "https://monitor.firefox.com/?entrypoint=protection_report_monitor&utm_source=about-protections");
+pref("browser.contentblocking.report.monitor.sign_in_url", "https://monitor.firefox.com/oauth/init?entrypoint=protection_report_monitor&utm_source=about-protections&email=");
 pref("browser.contentblocking.report.lockwise.url", "https://lockwise.firefox.com/");
 pref("browser.contentblocking.report.manage_devices.url", "https://accounts.firefox.com/settings/clients");
 
 // Protection Report's SUMO urls
 pref("browser.contentblocking.report.monitor.how_it_works.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/monitor-faq");
 pref("browser.contentblocking.report.lockwise.how_it_works.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/password-manager-report");
 pref("browser.contentblocking.report.social.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/social-media-tracking-report");
 pref("browser.contentblocking.report.cookie.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cross-site-tracking-report");
 pref("browser.contentblocking.report.tracker.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/tracking-content-report");
 pref("browser.contentblocking.report.fingerprinter.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/fingerprinters-report");
 pref("browser.contentblocking.report.cryptominer.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cryptominers-report");
 
 // Enables the new Protections Panel.
 #ifdef NIGHTLY_BUILD
-pref("browser.protections_panel.enabled", true);
-pref("browser.protections_panel.infoMessage.seen", false);
+  pref("browser.protections_panel.enabled", true);
+  pref("browser.protections_panel.infoMessage.seen", false);
 #endif
 
 // Always enable newtab segregation using containers
 pref("privacy.usercontext.about_newtab_segregation.enabled", true);
 // Enable Contextual Identity Containers
 #ifdef NIGHTLY_BUILD
-pref("privacy.userContext.enabled", true);
-pref("privacy.userContext.ui.enabled", true);
+  pref("privacy.userContext.enabled", true);
+  pref("privacy.userContext.ui.enabled", true);
 
-// 0 disables long press, 1 when clicked, the menu is shown, 2 the menu is shown after X milliseconds.
-pref("privacy.userContext.longPressBehavior", 2);
+  // 0 disables long press, 1 when clicked, the menu is shown, 2 the menu is
+  // shown after X milliseconds.
+  pref("privacy.userContext.longPressBehavior", 2);
 #else
-pref("privacy.userContext.enabled", false);
-pref("privacy.userContext.ui.enabled", false);
+  pref("privacy.userContext.enabled", false);
+  pref("privacy.userContext.ui.enabled", false);
 
-// 0 disables long press, 1 when clicked, the menu is shown, 2 the menu is shown after X milliseconds.
-pref("privacy.userContext.longPressBehavior", 0);
+  // 0 disables long press, 1 when clicked, the menu is shown, 2 the menu is
+  // shown after X milliseconds.
+  pref("privacy.userContext.longPressBehavior", 0);
 #endif
 pref("privacy.userContext.extension", "");
 
 // Start the browser in e10s mode
 pref("browser.tabs.remote.autostart", true);
 pref("browser.tabs.remote.desktopbehavior", true);
 
 // Run media transport in a separate process?
 #ifdef NIGHTLY_BUILD
-pref("media.peerconnection.mtransport_process", true);
+  pref("media.peerconnection.mtransport_process", true);
 #else
-pref("media.peerconnection.mtransport_process", false);
+  pref("media.peerconnection.mtransport_process", false);
 #endif
 
 // Start a separate socket process. Performing networking on the socket process
 // is control by a sepparate pref
 // ("network.http.network_access_on_socket_process.enabled").
 // Changing these prefs requires a restart.
 #ifdef NIGHTLY_BUILD
-pref("network.process.enabled", true);
+  pref("network.process.enabled", true);
 #else
-pref("network.process.enabled", false);
+  pref("network.process.enabled", false);
 #endif
 
 // For speculatively warming up tabs to improve perceived
 // performance while using the async tab switcher.
 pref("browser.tabs.remote.warmup.enabled", true);
 
 // Caches tab layers to improve perceived performance
 // of tab switches.
@@ -1699,26 +1704,26 @@ pref("dom.ipc.cpow.timeout", 500);
 // Causes access on unsafe CPOWs from browser code to throw by default.
 pref("dom.ipc.cpows.forbid-unsafe-from-browser", true);
 
 // Enable e10s hang monitoring (slow script checking and plugin hang
 // detection).
 pref("dom.ipc.processHangMonitor", true);
 
 #if defined(XP_WIN)
-// Allows us to deprioritize the processes of background tabs at an OS level
-pref("dom.ipc.processPriorityManager.enabled", true);
+  // Allows us to deprioritize the processes of background tabs at an OS level
+  pref("dom.ipc.processPriorityManager.enabled", true);
 #endif
 
 #ifdef DEBUG
-// Don't report hangs in DEBUG builds. They're too slow and often a
-// debugger is attached.
-pref("dom.ipc.reportProcessHangs", false);
+  // Don't report hangs in DEBUG builds. They're too slow and often a
+  // debugger is attached.
+  pref("dom.ipc.reportProcessHangs", false);
 #else
-pref("dom.ipc.reportProcessHangs", true);
+  pref("dom.ipc.reportProcessHangs", true);
 #endif
 
 // Don't limit how many nodes we care about on desktop:
 pref("reader.parse-node-limit", 0);
 
 // On desktop, we want the URLs to be included here for ease of debugging,
 // and because (normally) these errors are not persisted anywhere.
 pref("reader.errors.includeURLs", true);
@@ -1760,21 +1765,20 @@ pref("extensions.pocket.site", "getpocke
 pref("signon.generation.available", true);
 pref("signon.generation.enabled", true);
 pref("signon.schemeUpgrades", true);
 pref("signon.privateBrowsingCapture.enabled", true);
 pref("signon.showAutoCompleteFooter", true);
 pref("signon.management.page.enabled", true);
 pref("signon.management.page.breach-alerts.enabled", true);
 pref("signon.management.overrideURI", "about:logins?filter=%DOMAIN%");
-pref("signon.management.page.breach-alerts.enabled", false);
 #ifdef NIGHTLY_BUILD
-// Bug 1563330 tracks shipping this by default.
-pref("signon.showAutoCompleteOrigins", true);
-pref("signon.includeOtherSubdomainsInLookup", true);
+  // Bug 1563330 tracks shipping this by default.
+  pref("signon.showAutoCompleteOrigins", true);
+  pref("signon.includeOtherSubdomainsInLookup", true);
 #endif
 pref("signon.management.page.faqURL", "https://lockwise.firefox.com/faq.html");
 pref("signon.management.page.feedbackURL",
      "https://www.surveygizmo.com/s3/5036102/Lockwise-feedback?ver=%VERSION%");
 pref("signon.management.page.mobileAndroidURL", "https://app.adjust.com/6tteyjo?redirect=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dmozilla.lockbox&utm_campaign=Desktop&utm_adgroup=InProduct&utm_creative=Elipsis_Menu");
 pref("signon.management.page.mobileAppleURL", "https://app.adjust.com/6tteyjo?redirect=https%3A%2F%2Fitunes.apple.com%2Fapp%2Fid1314000270%3Fmt%3D8&utm_campaign=Desktop&utm_adgroup=InProduct&utm_creative=Elipsis_Menu");
 pref("signon.management.page.breachAlertUrl",
      "https://monitor.firefox.com/breach-details/");
@@ -1786,36 +1790,36 @@ pref("print.use_simplify_page", true);
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
 pref("webchannel.allowObject.urlWhitelist", "https://content.cdn.mozilla.net https://support.mozilla.org https://install.mozilla.org");
 
 // Whether or not the browser should scan for unsubmitted
 // crash reports, and then show a notification for submitting
 // those reports.
 #ifdef NIGHTLY_BUILD
-pref("browser.crashReports.unsubmittedCheck.enabled", true);
+  pref("browser.crashReports.unsubmittedCheck.enabled", true);
 #else
-pref("browser.crashReports.unsubmittedCheck.enabled", false);
+  pref("browser.crashReports.unsubmittedCheck.enabled", false);
 #endif
 
 // chancesUntilSuppress is how many times we'll show the unsubmitted
 // crash report notification across different days and shutdown
 // without a user choice before we suppress the notification for
 // some number of days.
 pref("browser.crashReports.unsubmittedCheck.chancesUntilSuppress", 4);
 pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false);
 
 // Preferences for the form autofill system extension
 // The truthy values of "extensions.formautofill.available" are "on" and "detect",
 // any other value means autofill isn't available.
 // "detect" means it's enabled if conditions defined in the extension are met.
 #ifdef NIGHTLY_BUILD
-pref("extensions.formautofill.available", "on");
+  pref("extensions.formautofill.available", "on");
 #else
-pref("extensions.formautofill.available", "detect");
+  pref("extensions.formautofill.available", "detect");
 #endif
 pref("extensions.formautofill.creditCards.available", false);
 pref("extensions.formautofill.addresses.enabled", true);
 pref("extensions.formautofill.creditCards.enabled", true);
 // Pref for shield/heartbeat to recognize users who have used Credit Card
 // Autofill. The valid values can be:
 // 0: none
 // 1: submitted a manually-filled credit card form (but didn't see the doorhanger
@@ -1826,22 +1830,22 @@ pref("extensions.formautofill.creditCard
 pref("extensions.formautofill.firstTimeUse", true);
 pref("extensions.formautofill.heuristics.enabled", true);
 // Whether the user enabled the OS re-auth dialog.
 pref("extensions.formautofill.reauth.enabled", false);
 pref("extensions.formautofill.section.enabled", true);
 pref("extensions.formautofill.loglevel", "Warn");
 
 #ifdef NIGHTLY_BUILD
-// Comma separated list of countries Form Autofill is available in.
-pref("extensions.formautofill.supportedCountries", "US,CA,DE");
-pref("extensions.formautofill.supportRTL", true);
+  // Comma separated list of countries Form Autofill is available in.
+  pref("extensions.formautofill.supportedCountries", "US,CA,DE");
+  pref("extensions.formautofill.supportRTL", true);
 #else
-pref("extensions.formautofill.supportedCountries", "US");
-pref("extensions.formautofill.supportRTL", false);
+  pref("extensions.formautofill.supportedCountries", "US");
+  pref("extensions.formautofill.supportRTL", false);
 #endif
 
 // Whether or not to restore a session with lazy-browser tabs.
 pref("browser.sessionstore.restore_tabs_lazily", true);
 
 pref("browser.suppress_first_window_animation", true);
 
 // Preference that allows individual users to disable Screenshots.
@@ -1858,29 +1862,29 @@ pref("browser.chrome.errorReporter.infoU
 pref("app.normandy.api_url", "https://normandy.cdn.mozilla.net/api/v1");
 pref("app.normandy.dev_mode", false);
 pref("app.normandy.enabled", true);
 pref("app.normandy.first_run", true);
 pref("app.normandy.logging.level", 50); // Warn
 pref("app.normandy.run_interval_seconds", 21600); // 6 hours
 pref("app.normandy.shieldLearnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield");
 #ifdef MOZ_DATA_REPORTING
-pref("app.shield.optoutstudies.enabled", true);
+  pref("app.shield.optoutstudies.enabled", true);
 #else
-pref("app.shield.optoutstudies.enabled", false);
+  pref("app.shield.optoutstudies.enabled", false);
 #endif
 
 // Multi-lingual preferences
 #if defined(RELEASE_OR_BETA) && !defined(MOZ_DEV_EDITION)
-pref("intl.multilingual.enabled", true);
-pref("intl.multilingual.downloadEnabled", true);
+  pref("intl.multilingual.enabled", true);
+  pref("intl.multilingual.downloadEnabled", true);
 #else
-pref("intl.multilingual.enabled", false);
-// AMO only serves language packs for release and beta versions.
-pref("intl.multilingual.downloadEnabled", false);
+  pref("intl.multilingual.enabled", false);
+  // AMO only serves language packs for release and beta versions.
+  pref("intl.multilingual.downloadEnabled", false);
 #endif
 
 // Simulate conditions that will happen when the browser
 // is running with Fission enabled. This is meant to assist
 // development and testing of Fission.
 // The current simulated conditions are:
 // - Don't propagate events from subframes to JS child actors
 pref("fission.frontend.simulate-events", false);
@@ -1911,19 +1915,19 @@ pref("identity.fxaccounts.toolbar.enable
 pref("identity.fxaccounts.toolbar.accessed", false);
 
 // Prefs for different services supported by Firefox Account
 pref("identity.fxaccounts.service.sendLoginUrl", "https://send.firefox.com/login/");
 pref("identity.fxaccounts.service.monitorLoginUrl", "https://monitor.firefox.com/");
 
 // Check bundled JAR and XPI files for corruption.
 #ifdef RELEASE_OR_BETA
-pref("corroborator.enabled", false);
+  pref("corroborator.enabled", false);
 #else
-pref("corroborator.enabled", true);
+  pref("corroborator.enabled", true);
 #endif
 
 // Show notification popup for social tracking protection.
 pref("privacy.socialtracking.notification.enabled", true);
 // minimum number of page loads until showing popup.
 pref("privacy.socialtracking.notification.session.pageload.min", 4);
 // timestamp of last popup was shown.
 pref("privacy.socialtracking.notification.lastShown", "0");
--- a/browser/base/content/test/alerts/browser_notification_close.js
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -1,14 +1,18 @@
 "use strict";
 
 const { PlacesTestUtils } = ChromeUtils.import(
   "resource://testing-common/PlacesTestUtils.jsm"
 );
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 let notificationURL =
   "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
 let oldShowFavicons;
 
 add_task(async function test_notificationClose() {
   let notificationURI = makeURI(notificationURL);
   await addNotificationPermission(notificationURL);
 
@@ -92,13 +96,13 @@ add_task(async function test_notificatio
           ", actually closed at " +
           currentTime
       );
     }
   );
 });
 
 add_task(async function cleanup() {
-  Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+  PermissionTestUtils.remove(notificationURL, "desktop-notification");
   if (typeof oldShowFavicons == "boolean") {
     Services.prefs.setBoolPref("alerts.showFavicons", oldShowFavicons);
   }
 });
--- a/browser/base/content/test/alerts/browser_notification_remove_permission.js
+++ b/browser/base/content/test/alerts/browser_notification_remove_permission.js
@@ -1,10 +1,14 @@
 "use strict";
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 var tab;
 var notificationURL =
   "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
 var alertWindowClosed = false;
 var permRemoved = false;
 
 function test() {
   waitForExplicitFinish();
@@ -30,18 +34,18 @@ function onAlertShowing() {
 
   let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
   if (!alertWindow) {
     ok(true, "Notifications don't use XUL windows on all platforms.");
     closeNotification(tab.linkedBrowser).then(finish);
     return;
   }
   ok(
-    Services.perms.testExactPermission(
-      makeURI(notificationURL),
+    PermissionTestUtils.testExactPermission(
+      notificationURL,
       "desktop-notification"
     ),
     "Permission should exist prior to removal"
   );
   let disableForOriginMenuItem = alertWindow.document.getElementById(
     "disableForOriginMenuItem"
   );
   is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
--- a/browser/base/content/test/alerts/browser_notification_tab_switching.js
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -1,14 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 "use strict";
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 var tab;
 var notification;
 var notificationURL =
   "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
 var newWindowOpenedFromTab;
 
 add_task(async function test_notificationPreventDefaultAndSwitchTabs() {
   await addNotificationPermission(notificationURL);
@@ -99,10 +103,10 @@ add_task(async function test_notificatio
       notificationClosed = promiseNotificationEvent("close");
       await closeNotification(aBrowser);
       await notificationClosed;
     }
   );
 });
 
 add_task(async function cleanup() {
-  Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+  PermissionTestUtils.remove(notificationURL, "desktop-notification");
 });
--- a/browser/base/content/test/general/browser_bug578534.js
+++ b/browser/base/content/test/general/browser_bug578534.js
@@ -1,27 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 add_task(async function test() {
   let uriString = "http://example.com/";
   let cookieBehavior = "network.cookie.cookieBehavior";
-  let uriObj = Services.io.newURI(uriString);
 
   await SpecialPowers.pushPrefEnv({ set: [[cookieBehavior, 2]] });
-  Services.perms.add(uriObj, "cookie", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(uriString, "cookie", Services.perms.ALLOW_ACTION);
 
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: uriString },
     async function(browser) {
       await ContentTask.spawn(browser, null, function() {
         is(
           content.navigator.cookieEnabled,
           true,
           "navigator.cookieEnabled should be true"
         );
       });
     }
   );
 
-  Services.perms.add(uriObj, "cookie", Services.perms.UNKNOWN_ACTION);
+  PermissionTestUtils.add(uriString, "cookie", Services.perms.UNKNOWN_ACTION);
 });
--- a/browser/base/content/test/general/browser_remoteTroubleshoot.js
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -1,15 +1,18 @@
 /* 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/. */
 
 var { WebChannel } = ChromeUtils.import(
   "resource://gre/modules/WebChannel.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const TEST_URL_TAIL =
   "example.com/browser/browser/base/content/test/general/test_remoteTroubleshoot.html";
 const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URL_TAIL);
 const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URL_TAIL);
 const TEST_URI_GOOD_OBJECT = Services.io.newURI(
   "https://" + TEST_URL_TAIL + "?object"
 );
@@ -49,23 +52,23 @@ add_task(async function() {
   let got = await promiseNewChannelResponse(TEST_URI_GOOD);
   // Should return an error.
   Assert.ok(
     got.message.errno === 2,
     "should have failed with errno 2, no such channel"
   );
 
   // Add a permission manager entry for our URI.
-  Services.perms.add(
+  PermissionTestUtils.add(
     TEST_URI_GOOD,
     "remote-troubleshooting",
     Services.perms.ALLOW_ACTION
   );
   registerCleanupFunction(() => {
-    Services.perms.remove(TEST_URI_GOOD, "remote-troubleshooting");
+    PermissionTestUtils.remove(TEST_URI_GOOD, "remote-troubleshooting");
   });
 
   // Try again - now we are expecting a response with the actual data.
   got = await promiseNewChannelResponse(TEST_URI_GOOD);
 
   // Check some keys we expect to always get.
   Assert.ok(got.message.extensions, "should have extensions");
   Assert.ok(got.message.graphics, "should have graphics");
--- a/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js
@@ -1,11 +1,14 @@
 const { SitePermissions } = ChromeUtils.import(
   "resource:///modules/SitePermissions.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const TEST_ORIGIN = "https://example.com";
 const TEST_ORIGIN_CERT_ERROR = "https://expired.example.com";
 const LOW_TLS_VERSION = "https://tls1.example.com/";
 
 async function testPermissions(defaultPermission) {
   await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
     let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
@@ -17,64 +20,66 @@ async function testPermissions(defaultPe
     let radioGroup = pageInfo.document.getElementById("geoRadioGroup");
     let defaultRadioButton = pageInfo.document.getElementById(
       "geo#" + defaultPermission
     );
     let blockRadioButton = pageInfo.document.getElementById("geo#2");
 
     ok(defaultCheckbox.checked, "The default checkbox should be checked.");
 
-    SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "geo",
+      Services.perms.DENY_ACTION
+    );
 
     ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
 
     defaultCheckbox.checked = true;
     defaultCheckbox.dispatchEvent(new Event("command"));
 
-    is(
-      SitePermissions.get(gBrowser.currentURI, "geo").state,
-      defaultPermission,
+    ok(
+      !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
       "Checking the default checkbox should reset the permission."
     );
 
     defaultCheckbox.checked = false;
     defaultCheckbox.dispatchEvent(new Event("command"));
 
-    is(
-      SitePermissions.get(gBrowser.currentURI, "geo").state,
-      defaultPermission,
+    ok(
+      !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
       "Unchecking the default checkbox should pick the default permission."
     );
     is(
       radioGroup.selectedItem,
       defaultRadioButton,
       "The unknown radio button should be selected."
     );
 
     radioGroup.selectedItem = blockRadioButton;
     blockRadioButton.dispatchEvent(new Event("command"));
 
     is(
-      SitePermissions.get(gBrowser.currentURI, "geo").state,
-      SitePermissions.BLOCK,
+      PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo")
+        .capability,
+      Services.perms.DENY_ACTION,
       "Selecting a value in the radio group should set the corresponding permission"
     );
 
     radioGroup.selectedItem = defaultRadioButton;
     defaultRadioButton.dispatchEvent(new Event("command"));
 
-    is(
-      SitePermissions.get(gBrowser.currentURI, "geo").state,
-      defaultPermission,
+    ok(
+      !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
       "Selecting the default value should reset the permission."
     );
     ok(defaultCheckbox.checked, "The default checkbox should be checked.");
 
     pageInfo.close();
-    SitePermissions.remove(gBrowser.currentURI, "geo");
+    PermissionTestUtils.remove(gBrowser.currentURI, "geo");
   });
 }
 
 // Test displaying website permissions on certificate error pages.
 add_task(async function test_CertificateError() {
   let browser;
   let pageLoaded;
   await BrowserTestUtils.openNewForegroundTab(
@@ -173,26 +178,26 @@ add_task(async function test_NetworkErro
   );
 
   pageInfo.close();
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 // Test some standard operations in the permission tab.
 add_task(async function test_geo_permission() {
-  await testPermissions(SitePermissions.UNKNOWN);
+  await testPermissions(Services.perms.UNKNOWN_ACTION);
 });
 
 // Test some standard operations in the permission tab, falling back to a custom
 // default permission instead of UNKNOWN.
 add_task(async function test_default_geo_permission() {
   await SpecialPowers.pushPrefEnv({
     set: [["permissions.default.geo", SitePermissions.ALLOW]],
   });
-  await testPermissions(SitePermissions.ALLOW);
+  await testPermissions(Services.perms.ALLOW_ACTION);
 });
 
 // Test special behavior for cookie permissions.
 add_task(async function test_cookie_permission() {
   await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
     let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
     await BrowserTestUtils.waitForEvent(pageInfo, "load");
 
@@ -204,55 +209,55 @@ add_task(async function test_cookie_perm
     let blockRadioButton = pageInfo.document.getElementById("cookie#2");
 
     ok(defaultCheckbox.checked, "The default checkbox should be checked.");
 
     defaultCheckbox.checked = false;
     defaultCheckbox.dispatchEvent(new Event("command"));
 
     is(
-      Services.perms.testPermission(gBrowser.currentURI, "cookie"),
+      PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
       SitePermissions.ALLOW,
       "Unchecking the default checkbox should pick the default permission."
     );
     is(
       radioGroup.selectedItem,
       allowRadioButton,
       "The unknown radio button should be selected."
     );
 
     radioGroup.selectedItem = blockRadioButton;
     blockRadioButton.dispatchEvent(new Event("command"));
 
     is(
-      Services.perms.testPermission(gBrowser.currentURI, "cookie"),
+      PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
       SitePermissions.BLOCK,
       "Selecting a value in the radio group should set the corresponding permission"
     );
 
     radioGroup.selectedItem = allowRadioButton;
     allowRadioButton.dispatchEvent(new Event("command"));
 
     is(
-      Services.perms.testPermission(gBrowser.currentURI, "cookie"),
+      PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
       SitePermissions.ALLOW,
       "Selecting a value in the radio group should set the corresponding permission"
     );
     ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
 
     defaultCheckbox.checked = true;
     defaultCheckbox.dispatchEvent(new Event("command"));
 
     is(
-      Services.perms.testPermission(gBrowser.currentURI, "cookie"),
+      PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
       SitePermissions.UNKNOWN,
       "Checking the default checkbox should reset the permission."
     );
     is(
       radioGroup.selectedItem,
       null,
       "For cookies, no item should be selected when the checkbox is checked."
     );
 
     pageInfo.close();
-    SitePermissions.remove(gBrowser.currentURI, "cookie");
+    PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
   });
 });
--- a/browser/base/content/test/performance/browser_startup_syncIPC.js
+++ b/browser/base/content/test/performance/browser_startup_syncIPC.js
@@ -89,16 +89,22 @@ const startupPhases = {
     },
     {
       name: "PCompositorBridge::Msg_MakeSnapshot",
       condition: WIN && !WEBRENDER,
       ignoreIfUnused: true, // Only on Win10 64
       maxCount: 1,
     },
     {
+      name: "PWebRenderBridge::Msg_GetSnapshot",
+      condition: WIN && WEBRENDER,
+      ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+      maxCount: 1,
+    },
+    {
       name: "PCompositorBridge::Msg_WillClose",
       condition: WIN,
       ignoreIfUnused: true, // Only on Win10 64
       maxCount: 2,
     },
     {
       name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
       condition: WIN,
--- a/browser/base/content/test/permissions/browser_autoplay_blocked.js
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js
@@ -133,47 +133,71 @@ add_task(async function testMainViewVisi
     let menuitem = menulist.getElementsByTagName("menuitem")[0];
     Assert.equal(menuitem.getAttribute("label"), "Allow Audio and Video");
 
     menuitem.click();
     menulist.menupopup.hidePopup();
     await closeIdentityPopup();
 
     let uri = Services.io.newURI(AUTOPLAY_PAGE);
-    let state = SitePermissions.get(uri, AUTOPLAY_PERM).state;
-    Assert.equal(state, SitePermissions.ALLOW);
+    let state = PermissionTestUtils.getPermissionObject(uri, AUTOPLAY_PERM)
+      .capability;
+    Assert.equal(state, Services.perms.ALLOW_ACTION);
   });
 
   Services.perms.removeAll();
 });
 
 add_task(async function testGloballyBlockedOnNewWindow() {
   Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
 
-  let uri = Services.io.newURI(AUTOPLAY_PAGE);
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    AUTOPLAY_PAGE
+  );
 
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    AUTOPLAY_PAGE
+  );
   await blockedIconShown();
 
-  Assert.deepEqual(SitePermissions.get(uri, AUTOPLAY_PERM, tab.linkedBrowser), {
-    state: SitePermissions.BLOCK,
-    scope: SitePermissions.SCOPE_PERSISTENT,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(
+      principal,
+      AUTOPLAY_PERM,
+      tab.linkedBrowser
+    ),
+    {
+      state: SitePermissions.BLOCK,
+      scope: SitePermissions.SCOPE_PERSISTENT,
+    }
+  );
 
   let promiseWin = BrowserTestUtils.waitForNewWindow();
   gBrowser.replaceTabWithWindow(tab);
   let win = await promiseWin;
   tab = win.gBrowser.selectedTab;
 
-  Assert.deepEqual(SitePermissions.get(uri, AUTOPLAY_PERM, tab.linkedBrowser), {
-    state: SitePermissions.BLOCK,
-    scope: SitePermissions.SCOPE_PERSISTENT,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(
+      principal,
+      AUTOPLAY_PERM,
+      tab.linkedBrowser
+    ),
+    {
+      state: SitePermissions.BLOCK,
+      scope: SitePermissions.SCOPE_PERSISTENT,
+    }
+  );
 
-  SitePermissions.remove(uri, AUTOPLAY_PERM, tab.linkedBrowser);
+  SitePermissions.removeFromPrincipal(
+    principal,
+    AUTOPLAY_PERM,
+    tab.linkedBrowser
+  );
   await BrowserTestUtils.closeWindow(win);
 });
 
 add_task(async function testBFCache() {
   Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
 
   await BrowserTestUtils.withNewTab("about:home", async function(browser) {
     await BrowserTestUtils.loadURI(browser, AUTOPLAY_PAGE);
--- a/browser/base/content/test/permissions/browser_permissions.js
+++ b/browser/base/content/test/permissions/browser_permissions.js
@@ -37,17 +37,21 @@ add_task(async function testMainViewVisi
     let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
 
     await openIdentityPopup();
 
     ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
 
     await closeIdentityPopup();
 
-    SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "camera",
+      Services.perms.ALLOW_ACTION
+    );
 
     await openIdentityPopup();
 
     ok(
       BrowserTestUtils.is_hidden(emptyLabel),
       "List of permissions is not empty"
     );
 
@@ -61,75 +65,91 @@ add_task(async function testMainViewVisi
     let img = permissionsList.querySelector(
       "image.identity-popup-permission-icon"
     );
     ok(img, "There is an image for the permissions");
     ok(img.classList.contains("camera-icon"), "proper class is in image class");
 
     await closeIdentityPopup();
 
-    SitePermissions.remove(gBrowser.currentURI, "camera");
+    PermissionTestUtils.remove(gBrowser.currentURI, "camera");
 
     await openIdentityPopup();
 
     ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
 
     await closeIdentityPopup();
   });
 });
 
 add_task(async function testIdentityIcon() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function() {
-    SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
-
-    ok(
-      gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-      "identity-box signals granted permissions"
-    );
-
-    SitePermissions.remove(gBrowser.currentURI, "geo");
-
-    ok(
-      !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-      "identity-box doesn't signal granted permissions"
-    );
-
-    SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
-
-    ok(
-      !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-      "identity-box doesn't signal granted permissions"
-    );
-
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
-      "cookie",
-      SitePermissions.ALLOW_COOKIES_FOR_SESSION
+      "geo",
+      Services.perms.ALLOW_ACTION
     );
 
     ok(
       gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
       "identity-box signals granted permissions"
     );
 
-    SitePermissions.remove(gBrowser.currentURI, "geo");
-    SitePermissions.remove(gBrowser.currentURI, "camera");
-    SitePermissions.remove(gBrowser.currentURI, "cookie");
+    PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+
+    ok(
+      !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+      "identity-box doesn't signal granted permissions"
+    );
+
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "camera",
+      Services.perms.DENY_ACTION
+    );
+
+    ok(
+      !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+      "identity-box doesn't signal granted permissions"
+    );
+
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "cookie",
+      Ci.nsICookiePermission.ACCESS_SESSION
+    );
+
+    ok(
+      gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+      "identity-box signals granted permissions"
+    );
+
+    PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+    PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+    PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
   });
 });
 
 add_task(async function testCancelPermission() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function() {
     let permissionsList = document.getElementById(
       "identity-popup-permission-list"
     );
     let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
 
-    SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
-    SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "geo",
+      Services.perms.ALLOW_ACTION
+    );
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "camera",
+      Services.perms.DENY_ACTION
+    );
 
     await openIdentityPopup();
 
     ok(
       BrowserTestUtils.is_hidden(emptyLabel),
       "List of permissions is not empty"
     );
 
@@ -173,28 +193,36 @@ add_task(async function testPermissionHi
 
     await openIdentityPopup();
 
     ok(!BrowserTestUtils.is_hidden(emptyHint), "Empty hint is visible");
     ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
 
     await closeIdentityPopup();
 
-    SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
-    SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "geo",
+      Services.perms.ALLOW_ACTION
+    );
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "camera",
+      Services.perms.DENY_ACTION
+    );
 
     await openIdentityPopup();
 
     ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
     ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
 
     let cancelButtons = permissionsList.querySelectorAll(
       ".identity-popup-permission-remove-button"
     );
-    SitePermissions.remove(gBrowser.currentURI, "camera");
+    PermissionTestUtils.remove(gBrowser.currentURI, "camera");
 
     cancelButtons[0].click();
     ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
     ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
 
     cancelButtons[1].click();
     ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
     ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
@@ -215,40 +243,48 @@ add_task(async function testPermissionHi
     );
 
     await closeIdentityPopup();
   });
 });
 
 add_task(async function testPermissionIcons() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function() {
-    SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
-    SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "camera",
+      Services.perms.ALLOW_ACTION
+    );
+    PermissionTestUtils.add(
+      gBrowser.currentURI,
+      "geo",
+      Services.perms.DENY_ACTION
+    );
 
     let geoIcon = gIdentityHandler._identityBox.querySelector(
       ".blocked-permission-icon[data-permission-id='geo']"
     );
     ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
 
     let cameraIcon = gIdentityHandler._identityBox.querySelector(
       ".blocked-permission-icon[data-permission-id='camera']"
     );
     ok(
       !cameraIcon.hasAttribute("showing"),
       "allowed permission icon is not shown"
     );
 
-    SitePermissions.remove(gBrowser.currentURI, "geo");
+    PermissionTestUtils.remove(gBrowser.currentURI, "geo");
 
     ok(
       !geoIcon.hasAttribute("showing"),
       "blocked permission icon is not shown after reset"
     );
 
-    SitePermissions.remove(gBrowser.currentURI, "camera");
+    PermissionTestUtils.remove(gBrowser.currentURI, "camera");
   });
 });
 
 add_task(async function testPermissionShortcuts() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
     browser.focus();
 
     await new Promise(r => {
@@ -283,72 +319,72 @@ add_task(async function testPermissionSh
           expectedValue,
           "keypress event should be fired even for shortcut key, " + desc
         );
       }
     }
 
     await tryKey("pressed with default permissions", 1);
 
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
       "shortcuts",
-      SitePermissions.BLOCK
+      Services.perms.DENY_ACTION
     );
     await tryKey("pressed when site blocked", 1);
 
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
       "shortcuts",
-      SitePermissions.ALLOW
+      PermissionTestUtils.ALLOW
     );
     await tryKey("pressed when site allowed", 2);
 
-    SitePermissions.remove(gBrowser.currentURI, "shortcuts");
+    PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts");
     await new Promise(r => {
       SpecialPowers.pushPrefEnv(
         { set: [["permissions.default.shortcuts", 2]] },
         r
       );
     });
 
     await tryKey("pressed when globally blocked", 2);
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
       "shortcuts",
-      SitePermissions.ALLOW
+      Services.perms.ALLOW_ACTION
     );
     await tryKey("pressed when globally blocked but site allowed", 3);
 
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
       "shortcuts",
-      SitePermissions.BLOCK
+      Services.perms.DENY_ACTION
     );
     await tryKey("pressed when globally blocked and site blocked", 3);
 
-    SitePermissions.remove(gBrowser.currentURI, "shortcuts");
+    PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts");
   });
 });
 
 // Test the control center UI when policy permissions are set.
 add_task(async function testPolicyPermission() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function() {
     await SpecialPowers.pushPrefEnv({
       set: [["dom.disable_open_during_load", true]],
     });
 
     let permissionsList = document.getElementById(
       "identity-popup-permission-list"
     );
-    SitePermissions.set(
+    PermissionTestUtils.add(
       gBrowser.currentURI,
       "popup",
-      SitePermissions.ALLOW,
-      SitePermissions.SCOPE_POLICY
+      Services.perms.ALLOW_ACTION,
+      Services.perms.EXPIRE_POLICY
     );
 
     await openIdentityPopup();
 
     // Check if the icon, nameLabel and stateLabel are visible.
     let img, labelText, labels;
 
     img = permissionsList.querySelector("image.identity-popup-permission-icon");
--- a/browser/base/content/test/permissions/browser_permissions_event_telemetry.js
+++ b/browser/base/content/test/permissions/browser_permissions_event_telemetry.js
@@ -100,21 +100,25 @@ add_task(async function setup() {
   Services.telemetry.canRecordExtended = true;
 
   Services.prefs.setBoolPref("permissions.eventTelemetry.enabled", true);
 
   // Add some example permissions.
   let uri = Services.io.newURI(PERMISSIONS_PAGE);
   let uri2 = Services.io.newURI("https://example.org");
   let uri3 = Services.io.newURI("http://sub.example.org");
-  Services.perms.add(uri, "geo", Services.perms.ALLOW_ACTION);
-  Services.perms.add(uri3, "desktop-notification", Services.perms.ALLOW_ACTION);
-  Services.perms.add(uri2, "microphone", Services.perms.DENY_ACTION);
-  Services.perms.add(uri, "camera", Services.perms.DENY_ACTION);
-  Services.perms.add(uri2, "geo", Services.perms.DENY_ACTION);
+  PermissionTestUtils.add(uri, "geo", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(
+    uri3,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
+  PermissionTestUtils.add(uri2, "microphone", Services.perms.DENY_ACTION);
+  PermissionTestUtils.add(uri, "camera", Services.perms.DENY_ACTION);
+  PermissionTestUtils.add(uri2, "geo", Services.perms.DENY_ACTION);
 
   registerCleanupFunction(() => {
     Services.perms.removeAll();
     Services.prefs.clearUserPref("permissions.eventTelemetry.enabled");
     Services.telemetry.canRecordExtended = oldCanRecord;
   });
 
   Services.telemetry.clearEvents();
@@ -127,39 +131,33 @@ add_task(async function testAccept() {
     checkEventTelemetry("show");
 
     let notification = PopupNotifications.panel.firstElementChild;
     EventUtils.synthesizeMouseAtCenter(notification.button, {});
 
     checkEventTelemetry("accept");
 
     Services.telemetry.clearEvents();
-    Services.perms.remove(
-      Services.io.newURI(PERMISSIONS_PAGE),
-      "desktop-notification"
-    );
+    PermissionTestUtils.remove(PERMISSIONS_PAGE, "desktop-notification");
   });
 });
 
 add_task(async function testDeny() {
   await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
     await showPermissionPrompt(browser);
 
     checkEventTelemetry("show");
 
     let notification = PopupNotifications.panel.firstElementChild;
     EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
 
     checkEventTelemetry("deny");
 
     Services.telemetry.clearEvents();
-    Services.perms.remove(
-      Services.io.newURI(PERMISSIONS_PAGE),
-      "desktop-notification"
-    );
+    PermissionTestUtils.remove(PERMISSIONS_PAGE, "desktop-notification");
   });
 });
 
 add_task(async function testLeave() {
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     PERMISSIONS_PAGE
   );
@@ -169,13 +167,10 @@ add_task(async function testLeave() {
 
   let tabClosed = BrowserTestUtils.waitForTabClosing(tab);
   await BrowserTestUtils.removeTab(tab);
   await tabClosed;
 
   checkEventTelemetry("leave");
 
   Services.telemetry.clearEvents();
-  Services.perms.remove(
-    Services.io.newURI(PERMISSIONS_PAGE),
-    "desktop-notification"
-  );
+  PermissionTestUtils.remove(PERMISSIONS_PAGE, "desktop-notification");
 });
--- a/browser/base/content/test/permissions/browser_permissions_postPrompt.js
+++ b/browser/base/content/test/permissions/browser_permissions_postPrompt.js
@@ -36,22 +36,22 @@ function testPostPrompt(task) {
     await popupshown;
 
     ok(true, "Notification permission prompt was shown");
 
     let notification = PopupNotifications.panel.firstElementChild;
     EventUtils.synthesizeMouseAtCenter(notification.button, {});
 
     is(
-      Services.perms.testPermission(uri, "desktop-notification"),
+      PermissionTestUtils.testPermission(uri, "desktop-notification"),
       Ci.nsIPermissionManager.ALLOW_ACTION,
       "User can override the default deny by using the prompt"
     );
 
-    Services.perms.remove(uri, "desktop-notification");
+    PermissionTestUtils.remove(uri, "desktop-notification");
   });
 }
 
 add_task(async function testNotificationPermission() {
   Services.prefs.setBoolPref(
     "dom.webnotifications.requireuserinteraction",
     true
   );
--- a/browser/base/content/test/permissions/browser_temporary_permissions.js
+++ b/browser/base/content/test/permissions/browser_temporary_permissions.js
@@ -12,56 +12,62 @@ const PERMISSIONS_PAGE =
   getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
   "permissions.html";
 const SUBFRAME_PAGE =
   getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
   "temporary_permissions_subframe.html";
 
 // Test that setting temp permissions triggers a change in the identity block.
 add_task(async function testTempPermissionChangeEvents() {
-  let uri = NetUtil.newURI(ORIGIN);
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    ORIGIN
+  );
   let id = "geo";
 
-  await BrowserTestUtils.withNewTab(uri.spec, function(browser) {
-    SitePermissions.set(
-      uri,
+  await BrowserTestUtils.withNewTab(ORIGIN, function(browser) {
+    SitePermissions.setForPrincipal(
+      principal,
       id,
       SitePermissions.BLOCK,
       SitePermissions.SCOPE_TEMPORARY,
       browser
     );
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
     let geoIcon = document.querySelector(
       ".blocked-permission-icon[data-permission-id=geo]"
     );
 
     Assert.notEqual(
       geoIcon.getBoundingClientRect().width,
       0,
       "geo anchor should be visible"
     );
 
-    SitePermissions.remove(uri, id, browser);
+    SitePermissions.removeFromPrincipal(principal, id, browser);
 
     Assert.equal(
       geoIcon.getBoundingClientRect().width,
       0,
       "geo anchor should not be visible"
     );
   });
 });
 
 // Test that temp blocked permissions requested by subframes (with a different URI) affect the whole page.
 add_task(async function testTempPermissionSubframes() {
   let uri = NetUtil.newURI(ORIGIN);
+  let principal = Services.scriptSecurityManager.createContentPrincipal(
+    uri,
+    {}
+  );
   let id = "geo";
 
   await BrowserTestUtils.withNewTab(SUBFRAME_PAGE, async function(browser) {
     let popupshown = BrowserTestUtils.waitForEvent(
       PopupNotifications.panel,
       "popupshown"
     );
 
@@ -85,14 +91,14 @@ add_task(async function testTempPermissi
       "popuphidden"
     );
 
     let notification = PopupNotifications.panel.firstElementChild;
     EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
 
     await popuphidden;
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
   });
 });
--- a/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
@@ -23,51 +23,59 @@ const TIMEOUT_MS = 500;
 add_task(async function testTempPermissionRequestAfterExpiry() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS],
       ["media.navigator.permission.fake", true],
     ],
   });
 
-  let uri = NetUtil.newURI(ORIGIN);
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    ORIGIN
+  );
   let ids = ["geo", "camera"];
 
   for (let id of ids) {
     await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(
       browser
     ) {
       let blockedIcon = gIdentityHandler._identityBox.querySelector(
         `.blocked-permission-icon[data-permission-id='${id}']`
       );
 
-      SitePermissions.set(
-        uri,
+      SitePermissions.setForPrincipal(
+        principal,
         id,
         SitePermissions.BLOCK,
         SitePermissions.SCOPE_TEMPORARY,
         browser
       );
 
-      Assert.deepEqual(SitePermissions.get(uri, id, browser), {
-        state: SitePermissions.BLOCK,
-        scope: SitePermissions.SCOPE_TEMPORARY,
-      });
+      Assert.deepEqual(
+        SitePermissions.getForPrincipal(principal, id, browser),
+        {
+          state: SitePermissions.BLOCK,
+          scope: SitePermissions.SCOPE_TEMPORARY,
+        }
+      );
 
       ok(
         blockedIcon.hasAttribute("showing"),
         "blocked permission icon is shown"
       );
 
       await new Promise(c => setTimeout(c, TIMEOUT_MS));
 
-      Assert.deepEqual(SitePermissions.get(uri, id, browser), {
-        state: SitePermissions.UNKNOWN,
-        scope: SitePermissions.SCOPE_PERSISTENT,
-      });
+      Assert.deepEqual(
+        SitePermissions.getForPrincipal(principal, id, browser),
+        {
+          state: SitePermissions.UNKNOWN,
+          scope: SitePermissions.SCOPE_PERSISTENT,
+        }
+      );
 
       let popupshown = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
 
       // Request a permission;
       await BrowserTestUtils.synthesizeMouseAtCenter(`#${id}`, {}, browser);
@@ -84,12 +92,12 @@ add_task(async function testTempPermissi
         "popuphidden"
       );
 
       let notification = PopupNotifications.panel.firstElementChild;
       EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
 
       await popuphidden;
 
-      SitePermissions.remove(uri, id, browser);
+      SitePermissions.removeFromPrincipal(principal, id, browser);
     });
   }
 });
--- a/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js
@@ -1,66 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that temporary permissions are removed on user initiated reload only.
 add_task(async function testTempPermissionOnReload() {
-  let uri = NetUtil.newURI("https://example.com");
+  let origin = "https://example.com/";
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    origin
+  );
   let id = "geo";
 
-  await BrowserTestUtils.withNewTab(uri.spec, async function(browser) {
+  await BrowserTestUtils.withNewTab(origin, async function(browser) {
     let reloadButton = document.getElementById("reload-button");
 
-    SitePermissions.set(
-      uri,
+    SitePermissions.setForPrincipal(
+      principal,
       id,
       SitePermissions.BLOCK,
       SitePermissions.SCOPE_TEMPORARY,
       browser
     );
 
-    let reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+    let reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
     // Reload through the page (should not remove the temp permission).
     await ContentTask.spawn(browser, {}, () =>
       content.document.location.reload()
     );
 
     await reloaded;
     await BrowserTestUtils.waitForCondition(() => {
       return !reloadButton.disabled;
     });
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
-    reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+    reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
 
     // Reload as a user (should remove the temp permission).
     EventUtils.synthesizeMouseAtCenter(reloadButton, {});
 
     await reloaded;
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.UNKNOWN,
       scope: SitePermissions.SCOPE_PERSISTENT,
     });
 
     // Set the permission again.
-    SitePermissions.set(
-      uri,
+    SitePermissions.setForPrincipal(
+      principal,
       id,
       SitePermissions.BLOCK,
       SitePermissions.SCOPE_TEMPORARY,
       browser
     );
 
     // Open the tab context menu.
     let contextMenu = document.getElementById("tabContextMenu");
@@ -74,40 +77,43 @@ add_task(async function testTempPermissi
     EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
       type: "contextmenu",
       button: 2,
     });
     await popupShownPromise;
 
     let reloadMenuItem = document.getElementById("context_reloadTab");
 
-    reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+    reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
 
     // Reload as a user through the context menu (should remove the temp permission).
     EventUtils.synthesizeMouseAtCenter(reloadMenuItem, {});
 
     await reloaded;
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.UNKNOWN,
       scope: SitePermissions.SCOPE_PERSISTENT,
     });
 
-    SitePermissions.remove(uri, id, browser);
+    SitePermissions.removeFromPrincipal(principal, id, browser);
   });
 });
 
 // Test that temporary permissions are not removed when reloading all tabs.
 add_task(async function testTempPermissionOnReloadAllTabs() {
-  let uri = NetUtil.newURI("https://example.com");
+  let origin = "https://example.com/";
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    origin
+  );
   let id = "geo";
 
-  await BrowserTestUtils.withNewTab(uri.spec, async function(browser) {
-    SitePermissions.set(
-      uri,
+  await BrowserTestUtils.withNewTab(origin, async function(browser) {
+    SitePermissions.setForPrincipal(
+      principal,
       id,
       SitePermissions.BLOCK,
       SitePermissions.SCOPE_TEMPORARY,
       browser
     );
 
     // Select all tabs before opening the context menu.
     gBrowser.selectAllTabs();
@@ -132,40 +138,43 @@ add_task(async function testTempPermissi
     let reloaded = Promise.all(
       gBrowser.visibleTabs.map(tab =>
         BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab))
       )
     );
     EventUtils.synthesizeMouseAtCenter(reloadMenuItem, {});
     await reloaded;
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
-    SitePermissions.remove(uri, id, browser);
+    SitePermissions.removeFromPrincipal(principal, id, browser);
   });
 });
 
 // Test that temporary permissions are persisted through navigation in a tab.
 add_task(async function testTempPermissionOnNavigation() {
-  let uri = NetUtil.newURI("https://example.com/");
+  let origin = "https://example.com/";
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    origin
+  );
   let id = "geo";
 
-  await BrowserTestUtils.withNewTab(uri.spec, async function(browser) {
-    SitePermissions.set(
-      uri,
+  await BrowserTestUtils.withNewTab(origin, async function(browser) {
+    SitePermissions.setForPrincipal(
+      principal,
       id,
       SitePermissions.BLOCK,
       SitePermissions.SCOPE_TEMPORARY,
       browser
     );
 
-    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+    Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
     let loaded = BrowserTestUtils.browserLoaded(
       browser,
       false,
       "https://example.org/"
@@ -176,33 +185,39 @@ add_task(async function testTempPermissi
       browser,
       {},
       () => (content.document.location = "https://example.org/")
     );
 
     await loaded;
 
     // The temporary permissions for the current URI should be reset.
-    Assert.deepEqual(SitePermissions.get(browser.currentURI, id, browser), {
-      state: SitePermissions.UNKNOWN,
-      scope: SitePermissions.SCOPE_PERSISTENT,
-    });
+    Assert.deepEqual(
+      SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser),
+      {
+        state: SitePermissions.UNKNOWN,
+        scope: SitePermissions.SCOPE_PERSISTENT,
+      }
+    );
 
-    loaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+    loaded = BrowserTestUtils.browserLoaded(browser, false, origin);
 
     // Navigate to the original domain.
     await ContentTask.spawn(
       browser,
       {},
       () => (content.document.location = "https://example.com/")
     );
 
     await loaded;
 
     // The temporary permissions for the original URI should still exist.
-    Assert.deepEqual(SitePermissions.get(browser.currentURI, id, browser), {
-      state: SitePermissions.BLOCK,
-      scope: SitePermissions.SCOPE_TEMPORARY,
-    });
+    Assert.deepEqual(
+      SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser),
+      {
+        state: SitePermissions.BLOCK,
+        scope: SitePermissions.SCOPE_TEMPORARY,
+      }
+    );
 
-    SitePermissions.remove(uri, id, browser);
+    SitePermissions.removeFromPrincipal(browser.contentPrincipal, id, browser);
   });
 });
--- a/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js
@@ -1,72 +1,90 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that temp permissions are persisted through moving tabs to new windows.
 add_task(async function testTempPermissionOnTabMove() {
-  let uri = NetUtil.newURI("https://example.com");
+  let origin = "https://example.com/";
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    origin
+  );
   let id = "geo";
 
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
 
-  SitePermissions.set(
-    uri,
+  SitePermissions.setForPrincipal(
+    principal,
     id,
     SitePermissions.BLOCK,
     SitePermissions.SCOPE_TEMPORARY,
     tab.linkedBrowser
   );
 
-  Assert.deepEqual(SitePermissions.get(uri, id, tab.linkedBrowser), {
-    state: SitePermissions.BLOCK,
-    scope: SitePermissions.SCOPE_TEMPORARY,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser),
+    {
+      state: SitePermissions.BLOCK,
+      scope: SitePermissions.SCOPE_TEMPORARY,
+    }
+  );
 
   let promiseWin = BrowserTestUtils.waitForNewWindow();
   gBrowser.replaceTabWithWindow(tab);
   let win = await promiseWin;
   tab = win.gBrowser.selectedTab;
 
-  Assert.deepEqual(SitePermissions.get(uri, id, tab.linkedBrowser), {
-    state: SitePermissions.BLOCK,
-    scope: SitePermissions.SCOPE_TEMPORARY,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser),
+    {
+      state: SitePermissions.BLOCK,
+      scope: SitePermissions.SCOPE_TEMPORARY,
+    }
+  );
 
-  SitePermissions.remove(uri, id, tab.linkedBrowser);
+  SitePermissions.removeFromPrincipal(principal, id, tab.linkedBrowser);
   await BrowserTestUtils.closeWindow(win);
 });
 
 // Test that temp permissions don't affect other tabs of the same URI.
 add_task(async function testTempPermissionMultipleTabs() {
-  let uri = NetUtil.newURI("https://example.com");
+  let origin = "https://example.com/";
+  let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+    origin
+  );
   let id = "geo";
 
-  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
-  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
 
-  SitePermissions.set(
-    uri,
+  SitePermissions.setForPrincipal(
+    principal,
     id,
     SitePermissions.BLOCK,
     SitePermissions.SCOPE_TEMPORARY,
     tab2.linkedBrowser
   );
 
-  Assert.deepEqual(SitePermissions.get(uri, id, tab2.linkedBrowser), {
-    state: SitePermissions.BLOCK,
-    scope: SitePermissions.SCOPE_TEMPORARY,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(principal, id, tab2.linkedBrowser),
+    {
+      state: SitePermissions.BLOCK,
+      scope: SitePermissions.SCOPE_TEMPORARY,
+    }
+  );
 
-  Assert.deepEqual(SitePermissions.get(uri, id, tab1.linkedBrowser), {
-    state: SitePermissions.UNKNOWN,
-    scope: SitePermissions.SCOPE_PERSISTENT,
-  });
+  Assert.deepEqual(
+    SitePermissions.getForPrincipal(principal, id, tab1.linkedBrowser),
+    {
+      state: SitePermissions.UNKNOWN,
+      scope: SitePermissions.SCOPE_PERSISTENT,
+    }
+  );
 
   let geoIcon = document.querySelector(
     ".blocked-permission-icon[data-permission-id=geo]"
   );
 
   Assert.notEqual(
     geoIcon.getBoundingClientRect().width,
     0,
@@ -76,12 +94,12 @@ add_task(async function testTempPermissi
   await BrowserTestUtils.switchTab(gBrowser, tab1);
 
   Assert.equal(
     geoIcon.getBoundingClientRect().width,
     0,
     "geo anchor should not be visible"
   );
 
-  SitePermissions.remove(uri, id, tab2.linkedBrowser);
+  SitePermissions.removeFromPrincipal(principal, id, tab2.linkedBrowser);
   BrowserTestUtils.removeTab(tab1);
   BrowserTestUtils.removeTab(tab2);
 });
--- a/browser/base/content/test/permissions/head.js
+++ b/browser/base/content/test/permissions/head.js
@@ -1,1 +1,4 @@
 ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
--- a/browser/base/content/test/popups/browser_popup_blocker_identity_block.js
+++ b/browser/base/content/test/popups/browser_popup_blocker_identity_block.js
@@ -2,23 +2,30 @@
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { SitePermissions } = ChromeUtils.import(
   "resource:///modules/SitePermissions.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const baseURL = getRootDirectory(gTestPath).replace(
   "chrome://mochitests/content",
   "http://example.com"
 );
 const URL = baseURL + "popup_blocker2.html";
 const URI = Services.io.newURI(URL);
+const PRINCIPAL = Services.scriptSecurityManager.createContentPrincipal(
+  URI,
+  {}
+);
 
 function openIdentityPopup() {
   let promise = BrowserTestUtils.waitForEvent(
     gIdentityHandler._identityPopup,
     "popupshown"
   );
   gIdentityHandler._identityBox.click();
   return promise;
@@ -130,17 +137,18 @@ add_task(async function check_popup_show
   gBrowser.removeTab(tab);
 });
 
 // Test if changing menulist values of blocked popup indicator changes permission state and popup behavior.
 add_task(async function check_permission_state_change() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
 
   // Initially the permission state is BLOCK for popups (set by the prefs).
-  let state = SitePermissions.get(URI, "popup", gBrowser).state;
+  let state = SitePermissions.getForPrincipal(PRINCIPAL, "popup", gBrowser)
+    .state;
   Assert.equal(state, SitePermissions.BLOCK);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async () => {
     let open = content.document.getElementById("pop");
     open.click();
   });
 
   // Wait for popup block.
@@ -151,17 +159,17 @@ add_task(async function check_permission
   // Open identity popup and change permission state to allow.
   await openIdentityPopup();
   let menulist = document.getElementById("identity-popup-popup-menulist");
   menulist.menupopup.openPopup(); // Open the allow/block menu
   let menuitem = menulist.getElementsByTagName("menuitem")[0];
   menuitem.click();
   await closeIdentityPopup();
 
-  state = SitePermissions.get(URI, "popup", gBrowser).state;
+  state = SitePermissions.getForPrincipal(PRINCIPAL, "popup", gBrowser).state;
   Assert.equal(state, SitePermissions.ALLOW);
 
   // Store the popup that opens in this array.
   let popup;
   function onTabOpen(event) {
     popup = event.target;
   }
   gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
@@ -191,40 +199,40 @@ add_task(async function check_permission
   menulist = document.getElementById("identity-popup-popup-menulist");
   menulist.menupopup.openPopup(); // Open the allow/block menu
   menuitem = menulist.getElementsByTagName("menuitem")[1];
   menuitem.click();
   await closeIdentityPopup();
 
   // Clicking on the "Block" menuitem should remove the permission object(same behavior as UNKNOWN state).
   // We have already confirmed that popups are blocked when the permission state is BLOCK.
-  state = SitePermissions.get(URI, "popup", gBrowser).state;
+  state = SitePermissions.getForPrincipal(PRINCIPAL, "popup", gBrowser).state;
   Assert.equal(state, SitePermissions.BLOCK);
 
   gBrowser.removeTab(tab);
 });
 
 // Explicitly set the permission to the otherwise default state and check that
 // the label still displays correctly.
 add_task(async function check_explicit_default_permission() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
 
   // DENY only works if triggered through Services.perms (it's very edge-casey),
   // since SitePermissions.jsm considers setting default permissions to be removal.
-  Services.perms.add(URI, "popup", Ci.nsIPermissionManager.DENY_ACTION);
+  PermissionTestUtils.add(URI, "popup", Ci.nsIPermissionManager.DENY_ACTION);
 
   await openIdentityPopup();
   let menulist = document.getElementById("identity-popup-popup-menulist");
   Assert.equal(menulist.value, "0");
   Assert.equal(menulist.label, "Block");
   await closeIdentityPopup();
 
-  SitePermissions.set(URI, "popup", SitePermissions.ALLOW);
+  PermissionTestUtils.add(URI, "popup", Services.perms.ALLOW_ACTION);
 
   await openIdentityPopup();
   menulist = document.getElementById("identity-popup-popup-menulist");
   Assert.equal(menulist.value, "1");
   Assert.equal(menulist.label, "Allow");
   await closeIdentityPopup();
 
-  SitePermissions.remove(URI, "popup");
+  PermissionTestUtils.remove(URI, "popup");
   gBrowser.removeTab(tab);
 });
--- a/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
@@ -72,17 +72,17 @@ add_task(async function deleteStorageOnl
     set: [
       ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_NORMALLY],
       ["browser.sanitizer.loglevel", "All"],
     ],
   });
 
   // Custom permission without considering OriginAttributes
   let uri = Services.io.newURI("about:newtab");
-  Services.perms.add(uri, "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
+  PermissionTestUtils.add(uri, "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
 
   // Let's create a tab with some data.
   await SiteDataTestUtils.addToIndexedDB("about:newtab", "foo", "bar", {});
 
   ok(await checkDataForAboutURL(), "We have data for about:newtab");
 
   // Cleaning up.
   await Sanitizer.runSanitizeOnShutdown();
@@ -97,10 +97,10 @@ add_task(async function deleteStorageOnl
   );
   await new Promise(aResolve => {
     let req = Services.qms.clearStoragesForPrincipal(principal);
     req.callback = () => {
       aResolve();
     };
   });
 
-  Services.perms.remove(uri, "cookie");
+  PermissionTestUtils.remove(uri, "cookie");
 });
--- a/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
@@ -17,17 +17,21 @@ add_task(async function subDomains() {
     set: [
       ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_NORMALLY],
       ["browser.sanitizer.loglevel", "All"],
     ],
   });
 
   // Domains and data
   let uriA = Services.io.newURI("https://www.mozilla.org");
-  Services.perms.add(uriA, "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
+  PermissionTestUtils.add(
+    uriA,
+    "cookie",
+    Ci.nsICookiePermission.ACCESS_SESSION
+  );
 
   Services.cookies.add(
     uriA.host,
     "/test",
     "a",
     "b",
     false,
     false,
@@ -35,17 +39,17 @@ add_task(async function subDomains() {
     Date.now() + 24000 * 60 * 60,
     {},
     Ci.nsICookie.SAMESITE_NONE
   );
 
   await createIndexedDB(uriA.host, {});
 
   let uriB = Services.io.newURI("https://mozilla.org");
-  Services.perms.add(uriB, "cookie", Ci.nsICookiePermission.ACCESS_ALLOW);
+  PermissionTestUtils.add(uriB, "cookie", Ci.nsICookiePermission.ACCESS_ALLOW);
 
   Services.cookies.add(
     uriB.host,
     "/test",
     "c",
     "d",
     false,
     false,
@@ -80,18 +84,18 @@ add_task(async function subDomains() {
     "We should have cookies for URI: " + uriB.host
   );
   ok(
     await checkIndexedDB(uriB.host, {}),
     "We should have IDB for URI: " + uriB.host
   );
 
   // Cleaning up permissions
-  Services.perms.remove(uriA, "cookie");
-  Services.perms.remove(uriB, "cookie");
+  PermissionTestUtils.remove(uriA, "cookie");
+  PermissionTestUtils.remove(uriB, "cookie");
 });
 
 // session only cookie life-time, 2 domains (mozilla.org, www.mozilla.org),
 // only the latter has a cookie permission.
 add_task(async function subDomains() {
   info("Test subdomains and custom setting with cookieBehavior == 2");
 
   // Let's clean up all the data.
@@ -103,17 +107,17 @@ add_task(async function subDomains() {
     set: [
       ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_SESSION],
       ["browser.sanitizer.loglevel", "All"],
     ],
   });
 
   // Domains and data
   let uriA = Services.io.newURI("https://sub.mozilla.org");
-  Services.perms.add(uriA, "cookie", Ci.nsICookiePermission.ACCESS_ALLOW);
+  PermissionTestUtils.add(uriA, "cookie", Ci.nsICookiePermission.ACCESS_ALLOW);
 
   Services.cookies.add(
     uriA.host,
     "/test",
     "a",
     "b",
     false,
     false,
@@ -165,10 +169,10 @@ add_task(async function subDomains() {
     "We should not have cookies for URI: " + uriB.host
   );
   ok(
     !(await checkIndexedDB(uriB.host, {})),
     "We should not have IDB for URI: " + uriB.host
   );
 
   // Cleaning up permissions
-  Services.perms.remove(uriA, "cookie");
+  PermissionTestUtils.remove(uriA, "cookie");
 });
--- a/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
+++ b/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
@@ -14,22 +14,25 @@ add_task(async function test() {
   // sanitize before we start so we have a good baseline.
   await Sanitizer.sanitize(["siteSettings"], { ignoreTimespan: false });
 
   // Count how many permissions we start with - some are defaults that
   // will not be sanitized.
   let numAtStart = countPermissions();
 
   // Add a permission entry
-  var pm = Services.perms;
-  pm.add(Services.io.newURI("http://example.com"), "testing", pm.ALLOW_ACTION);
+  PermissionTestUtils.add(
+    "http://example.com",
+    "testing",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Sanity check
   ok(
-    pm.enumerator.hasMoreElements(),
+    Services.perms.enumerator.hasMoreElements(),
     "Permission manager should have elements, since we just added one"
   );
 
   // Clear it
   await Sanitizer.sanitize(["siteSettings"], { ignoreTimespan: false });
 
   // Make sure it's gone
   is(
--- a/browser/base/content/test/sanitize/head.js
+++ b/browser/base/content/test/sanitize/head.js
@@ -3,16 +3,17 @@ var { XPCOMUtils } = ChromeUtils.import(
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Downloads: "resource://gre/modules/Downloads.jsm",
   FormHistory: "resource://gre/modules/FormHistory.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.jsm",
+  PermissionTestUtils: "resource://testing-common/PermissionTestUtils.jsm",
 });
 
 function createIndexedDB(host, originAttributes) {
   let uri = Services.io.newURI("https://" + host);
   let principal = Services.scriptSecurityManager.createContentPrincipal(
     uri,
     originAttributes
   );
@@ -93,17 +94,17 @@ async function deleteOnShutdown(opt) {
       ["network.cookie.lifetimePolicy", opt.lifetimePolicy],
       ["browser.sanitizer.loglevel", "All"],
     ],
   });
 
   // Custom permission without considering OriginAttributes
   if (opt.cookiePermission !== undefined) {
     let uri = Services.io.newURI("https://www.example.com");
-    Services.perms.add(uri, "cookie", opt.cookiePermission);
+    PermissionTestUtils.add(uri, "cookie", opt.cookiePermission);
   }
 
   // Let's create a tab with some data.
   await opt.createData(
     (opt.fullHost ? "www." : "") + "example.org",
     opt.originAttributes
   );
   ok(
@@ -146,17 +147,17 @@ async function deleteOnShutdown(opt) {
     "Do we have data for www.example.com?"
   );
 
   // Clean up.
   await Sanitizer.sanitize(["cookies", "offlineApps"]);
 
   if (opt.cookiePermission !== undefined) {
     let uri = Services.io.newURI("https://www.example.com");
-    Services.perms.remove(uri, "cookie");
+    PermissionTestUtils.remove(uri, "cookie");
   }
 }
 
 function runAllCookiePermissionTests(originAttributes) {
   let tests = [
     { name: "IDB", createData: createIndexedDB, checkData: checkIndexedDB },
     {
       name: "Host Cookie",
--- a/browser/base/content/test/siteIdentity/browser_geolocation_indicator.js
+++ b/browser/base/content/test/siteIdentity/browser_geolocation_indicator.js
@@ -1,22 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ChromeUtils.import("resource:///modules/PermissionUI.jsm", this);
 ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const CP = Cc["@mozilla.org/content-pref/service;1"].getService(
   Ci.nsIContentPrefService2
 );
 
 const EXAMPLE_PAGE_URL = "https://example.com";
 const EXAMPLE_PAGE_URI = Services.io.newURI(EXAMPLE_PAGE_URL);
+const EXAMPLE_PAGE_PRINCIPAL = Services.scriptSecurityManager.createContentPrincipal(
+  EXAMPLE_PAGE_URI,
+  {}
+);
 const GEO_CONTENT_PREF_KEY = "permissions.geoLocation.lastAccess";
 const POLL_INTERVAL_FALSE_STATE = 50;
 
 async function testGeoSharingIconVisible(state = true) {
   let sharingIcon = document.getElementById("geo-sharing-icon");
   ok(sharingIcon, "Geo sharing icon exists");
 
   try {
@@ -223,21 +230,21 @@ add_task(function test_indicator_and_tim
   return testIndicatorExplicitAllow(false);
 });
 add_task(function test_indicator_and_timestamp_after_explicit_allow_remember() {
   return testIndicatorExplicitAllow(true);
 });
 
 // Indicator and identity popup entry shown after auto PermissionUI geolocation allow
 add_task(async function test_indicator_and_timestamp_after_implicit_allow() {
-  SitePermissions.set(
+  PermissionTestUtils.add(
     EXAMPLE_PAGE_URI,
     "geo",
-    SitePermissions.ALLOW,
-    SitePermissions.SCOPE_PERSISTENT
+    Services.perms.ALLOW_ACTION,
+    Services.perms.EXPIRE_NEVER
   );
   let tab = await openExamplePage();
   let result = await requestGeoLocation(tab.linkedBrowser);
   ok(result, "Request should be allowed");
 
   await Promise.all([
     testGeoSharingIconVisible(true),
     testIdentityPopupGeoContainer(true, true),
@@ -254,21 +261,21 @@ add_task(function test_indicator_sharing
 
 // Indicator not shown when manually setting sharing state to false
 add_task(function test_indicator_sharing_state_inactive() {
   return testIndicatorGeoSharingState(false);
 });
 
 // Identity popup shows permission if geo permission is set to persistent allow
 add_task(async function test_identity_popup_permission_scope_permanent() {
-  SitePermissions.set(
+  PermissionTestUtils.add(
     EXAMPLE_PAGE_URI,
     "geo",
-    SitePermissions.ALLOW,
-    SitePermissions.SCOPE_PERSISTENT
+    Services.perms.ALLOW_ACTION,
+    Services.perms.EXPIRE_NEVER
   );
   let tab = await openExamplePage();
 
   await testIdentityPopupGeoContainer(true, false); // Expect permission to be visible, but not lastAccess indicator
 
   await cleanup(tab);
 });
 
@@ -291,21 +298,21 @@ add_task(
     await testIdentityPopupGeoContainer(true, true);
 
     await cleanup(tab);
   }
 );
 
 // Clicking permission clear button clears permission and resets geo sharing state
 add_task(async function test_identity_popup_permission_clear() {
-  SitePermissions.set(
+  PermissionTestUtils.add(
     EXAMPLE_PAGE_URI,
     "geo",
-    SitePermissions.ALLOW,
-    SitePermissions.SCOPE_PERSISTENT
+    Services.perms.ALLOW_ACTION,
+    Services.perms.EXPIRE_NEVER
   );
   let tab = await openExamplePage();
   gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
 
   await openIdentityPopup();
 
   let clearButton = document.querySelector(
     "#identity-popup-geo-container button"
--- a/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
+++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
@@ -1,10 +1,14 @@
 "use strict";
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 const ROOT = getRootDirectory(gTestPath).replace(
   "chrome://mochitests/content/",
   "http://example.com/"
 );
 let pageWithAlert = ROOT + "openPromptOffTimeout.html";
 
 registerCleanupFunction(function() {
   Services.perms.removeAll();
@@ -61,20 +65,19 @@ add_task(async function() {
     ourPromptElement
   );
   ourPrompt.onButtonClick(0);
   // Wait for that click to actually be handled completely.
   await new Promise(function(resolve) {
     Services.tm.dispatchToMainThread(resolve);
   });
   // check permission is set
-  let ps = Services.perms;
   is(
-    ps.ALLOW_ACTION,
-    ps.testPermission(makeURI(pageWithAlert), "focus-tab-by-prompt"),
+    Services.perms.ALLOW_ACTION,
+    PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"),
     "Tab switching should now be allowed"
   );
 
   // Check if the control center shows the correct permission.
   let shown = BrowserTestUtils.waitForEvent(
     gIdentityHandler._identityPopup,
     "popupshown"
   );
--- a/browser/base/content/test/trackingUI/browser_trackingUI_trackers_subview.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_trackers_subview.js
@@ -1,14 +1,18 @@
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 const TRACKING_PAGE =
   "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
 
 const TP_PREF = "privacy.trackingprotection.enabled";
 
 add_task(async function setup() {
   await UrlClassifierTestUtils.addTestTrackers();
 
@@ -108,16 +112,20 @@ add_task(async function testTrackersSubV
   info("Testing trackers subview with TP disabled.");
   Services.prefs.setBoolPref(TP_PREF, false);
   await assertSitesListed(false);
   info("Testing trackers subview with TP enabled.");
   Services.prefs.setBoolPref(TP_PREF, true);
   await assertSitesListed(true);
   info("Testing trackers subview with TP enabled and a CB exception.");
   let uri = Services.io.newURI("https://tracking.example.org");
-  Services.perms.add(uri, "trackingprotection", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(
+    uri,
+    "trackingprotection",
+    Services.perms.ALLOW_ACTION
+  );
   await assertSitesListed(false);
   info("Testing trackers subview with TP enabled and a CB exception removed.");
-  Services.perms.remove(uri, "trackingprotection");
+  PermissionTestUtils.remove(uri, "trackingprotection");
   await assertSitesListed(true);
 
   Services.prefs.clearUserPref(TP_PREF);
 });
--- a/browser/base/content/test/webextensions/browser_permissions_unsigned.js
+++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js
@@ -7,18 +7,18 @@ add_task(async function test_unsigned() 
   await SpecialPowers.pushPrefEnv({
     set: [
       ["extensions.webapi.testing", true],
       ["extensions.install.requireBuiltInCerts", false],
     ],
   });
 
   let testURI = makeURI("https://example.com/");
-  Services.perms.add(testURI, "install", Services.perms.ALLOW_ACTION);
-  registerCleanupFunction(() => Services.perms.remove(testURI, "install"));
+  PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+  registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
 
   BrowserTestUtils.loadURI(
     gBrowser.selectedBrowser,
     `${BASE}/file_install_extensions.html`
   );
   await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
--- a/browser/base/content/test/webextensions/head.js
+++ b/browser/base/content/test/webextensions/head.js
@@ -22,16 +22,20 @@ XPCOMUtils.defineLazyGetter(this, "Manag
 });
 
 ChromeUtils.import(
   "resource://testing-common/CustomizableUITestUtils.jsm",
   this
 );
 let gCUITestUtils = new CustomizableUITestUtils(window);
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 /**
  * Wait for the given PopupNotification to display
  *
  * @param {string} name
  *        The name of the notification to wait for.
  *
  * @returns {Promise}
  *          Resolves with the notification window.
@@ -304,18 +308,18 @@ async function testInstallMethod(install
     ],
   });
 
   if (telemetryBase !== undefined) {
     hookExtensionsTelemetry();
   }
 
   let testURI = makeURI("https://example.com/");
-  Services.perms.add(testURI, "install", Services.perms.ALLOW_ACTION);
-  registerCleanupFunction(() => Services.perms.remove(testURI, "install"));
+  PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+  registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
 
   async function runOnce(filename, cancel) {
     let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
 
     let installPromise = new Promise(resolve => {
       let listener = {
         onDownloadCancelled() {
           AddonManager.removeInstallListener(listener);
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -138,27 +138,35 @@ var gTests = [
       await checkNotSharing();
 
       // Verify that we set 'Temporarily blocked' permissions.
       let browser = gBrowser.selectedBrowser;
       let blockedPerms = document.getElementById(
         "blocked-permissions-container"
       );
 
-      let { state, scope } = SitePermissions.get(null, "camera", browser);
+      let { state, scope } = SitePermissions.getForPrincipal(
+        null,
+        "camera",
+        browser
+      );
       Assert.equal(state, SitePermissions.BLOCK);
       Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
       ok(
         blockedPerms.querySelector(
           ".blocked-permission-icon.camera-icon[showing=true]"
         ),
         "the blocked camera icon is shown"
       );
 
-      ({ state, scope } = SitePermissions.get(null, "microphone", browser));
+      ({ state, scope } = SitePermissions.getForPrincipal(
+        null,
+        "microphone",
+        browser
+      ));
       Assert.equal(state, SitePermissions.BLOCK);
       Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
       ok(
         blockedPerms.querySelector(
           ".blocked-permission-icon.microphone-icon[showing=true]"
         ),
         "the blocked microphone icon is shown"
       );
@@ -167,18 +175,26 @@ var gTests = [
       promise = promiseMessage(permissionError);
       await promiseRequestDevice(true, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
 
-      SitePermissions.remove(browser.currentURI, "camera", browser);
-      SitePermissions.remove(browser.currentURI, "microphone", browser);
+      SitePermissions.removeFromPrincipal(
+        browser.contentPrincipal,
+        "camera",
+        browser
+      );
+      SitePermissions.removeFromPrincipal(
+        browser.contentPrincipal,
+        "microphone",
+        browser
+      );
     },
   },
 
   {
     desc: "getUserMedia audio+video: stop sharing",
     run: async function checkStopSharing() {
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
@@ -215,19 +231,31 @@ var gTests = [
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia audio+video: reloading the page removes all gUM UI",
     run: async function checkReloading() {
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
@@ -261,19 +289,31 @@ var gTests = [
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia prompt: Always/Never Share",
     run: async function checkRememberCheckbox() {
       let elt = id => document.getElementById(id);
 
@@ -323,44 +363,48 @@ var gTests = [
         }
         Assert.deepEqual(
           await getMediaCaptureState(),
           expected,
           "expected " + Object.keys(expected).join(" and ") + " to be shared"
         );
 
         function checkDevicePermissions(aDevice, aExpected) {
-          let Perms = Services.perms;
           let uri = gBrowser.selectedBrowser.documentURI;
-          let devicePerms = Perms.testExactPermission(uri, aDevice);
+          let devicePerms = PermissionTestUtils.testExactPermission(
+            uri,
+            aDevice
+          );
           if (aExpected === undefined) {
             is(
               devicePerms,
-              Perms.UNKNOWN_ACTION,
+              Services.perms.UNKNOWN_ACTION,
               "no " + aDevice + " persistent permissions"
             );
           } else {
             is(
               devicePerms,
-              aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION,
+              aExpected
+                ? Services.perms.ALLOW_ACTION
+                : Services.perms.DENY_ACTION,
               aDevice + " persistently " + (aExpected ? "allowed" : "denied")
             );
           }
-          Perms.remove(uri, aDevice);
+          PermissionTestUtils.remove(uri, aDevice);
         }
         checkDevicePermissions("microphone", aExpectedAudioPerm);
         checkDevicePermissions("camera", aExpectedVideoPerm);
 
         if (expectedMessage == "ok") {
           await closeStream();
         }
       }
 
       // 3 cases where the user accepts the device prompt.
-      info("audio+video, user grants, expect both perms set to allow");
+      info("audio+video, user grants, expect both Services.perms set to allow");
       await checkPerm(true, true, true, true);
       info(
         "audio only, user grants, check audio perm set to allow, video perm not set"
       );
       await checkPerm(true, false, true, undefined);
       info(
         "video only, user grants, check video perm set to allow, audio perm not set"
       );
@@ -370,46 +414,49 @@ var gTests = [
       info(
         "audio only, user denies, expect audio perm set to deny, video not set"
       );
       await checkPerm(true, false, false, undefined, true);
       info(
         "video only, user denies, expect video perm set to deny, audio perm not set"
       );
       await checkPerm(false, true, undefined, false, true);
-      info("audio+video, user denies, expect both perms set to deny");
+      info("audio+video, user denies, expect both Services.perms set to deny");
       await checkPerm(true, true, false, false, true);
     },
   },
 
   {
     desc: "getUserMedia without prompt: use persistent permissions",
     run: async function checkUsePersistentPermissions() {
       async function usePerm(
         aAllowAudio,
         aAllowVideo,
         aRequestAudio,
         aRequestVideo,
         aExpectStream
       ) {
-        let Perms = Services.perms;
         let uri = gBrowser.selectedBrowser.documentURI;
 
         if (aAllowAudio !== undefined) {
-          Perms.add(
+          PermissionTestUtils.add(
             uri,
             "microphone",
-            aAllowAudio ? Perms.ALLOW_ACTION : Perms.DENY_ACTION
+            aAllowAudio
+              ? Services.perms.ALLOW_ACTION
+              : Services.perms.DENY_ACTION
           );
         }
         if (aAllowVideo !== undefined) {
-          Perms.add(
+          PermissionTestUtils.add(
             uri,
             "camera",
-            aAllowVideo ? Perms.ALLOW_ACTION : Perms.DENY_ACTION
+            aAllowVideo
+              ? Services.perms.ALLOW_ACTION
+              : Services.perms.DENY_ACTION
           );
         }
 
         if (aExpectStream === undefined) {
           // Check that we get a prompt.
           let promise = promisePopupNotificationShown("webRTC-shareDevices");
           await promiseRequestDevice(aRequestAudio, aRequestVideo);
           await promise;
@@ -417,18 +464,18 @@ var gTests = [
 
           // Deny the request to cleanup...
           await promiseMessage(permissionError, () => {
             activateSecondaryAction(kActionDeny);
           });
           await expectObserverCalled("getUserMedia:response:deny");
           await expectObserverCalled("recording-window-ended");
           let browser = gBrowser.selectedBrowser;
-          SitePermissions.remove(null, "camera", browser);
-          SitePermissions.remove(null, "microphone", browser);
+          SitePermissions.removeFromPrincipal(null, "camera", browser);
+          SitePermissions.removeFromPrincipal(null, "microphone", browser);
         } else {
           let expectedMessage = aExpectStream ? "ok" : permissionError;
           let promise = promiseMessage(expectedMessage);
           await promiseRequestDevice(aRequestAudio, aRequestVideo);
           await promise;
 
           if (expectedMessage == "ok") {
             await expectObserverCalled("getUserMedia:request");
@@ -453,18 +500,18 @@ var gTests = [
             );
 
             await closeStream();
           } else {
             await expectObserverCalled("recording-window-ended");
           }
         }
 
-        Perms.remove(uri, "camera");
-        Perms.remove(uri, "microphone");
+        PermissionTestUtils.remove(uri, "camera");
+        PermissionTestUtils.remove(uri, "microphone");
       }
 
       // Set both permissions identically
       info("allow audio+video, request audio+video, expect ok (audio+video)");
       await usePerm(true, true, true, true, true);
       info("deny audio+video, request audio+video, expect denied");
       await usePerm(false, false, true, true, false);
 
@@ -517,57 +564,75 @@ var gTests = [
       await usePerm(undefined, false, false, true, false);
     },
   },
 
   {
     desc: "Stop Sharing removes persistent permissions",
     run: async function checkStopSharingRemovesPersistentPermissions() {
       async function stopAndCheckPerm(aRequestAudio, aRequestVideo) {
-        let Perms = Services.perms;
         let uri = gBrowser.selectedBrowser.documentURI;
 
         // Initially set both permissions to 'allow'.
-        Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
-        Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+        PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+        PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
 
         let indicator = promiseIndicatorWindow();
         // Start sharing what's been requested.
         let promise = promiseMessage("ok");
         await promiseRequestDevice(aRequestAudio, aRequestVideo);
         await promise;
 
         await expectObserverCalled("getUserMedia:request");
         await expectObserverCalled("getUserMedia:response:allow");
         await expectObserverCalled("recording-device-events");
         await indicator;
         await checkSharingUI({ video: aRequestVideo, audio: aRequestAudio });
 
         await stopSharing(aRequestVideo ? "camera" : "microphone");
 
         // Check that permissions have been removed as expected.
-        let audioPerm = Perms.testExactPermission(uri, "microphone");
+        let audioPerm = PermissionTestUtils.testExactPermission(
+          uri,
+          "microphone"
+        );
         if (aRequestAudio) {
-          is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed");
+          is(
+            audioPerm,
+            Services.perms.UNKNOWN_ACTION,
+            "microphone permissions removed"
+          );
         } else {
-          is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched");
+          is(
+            audioPerm,
+            Services.perms.ALLOW_ACTION,
+            "microphone permissions untouched"
+          );
         }
 
-        let videoPerm = Perms.testExactPermission(uri, "camera");
+        let videoPerm = PermissionTestUtils.testExactPermission(uri, "camera");
         if (aRequestVideo) {
-          is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed");
+          is(
+            videoPerm,
+            Services.perms.UNKNOWN_ACTION,
+            "camera permissions removed"
+          );
         } else {
-          is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched");
+          is(
+            videoPerm,
+            Services.perms.ALLOW_ACTION,
+            "camera permissions untouched"
+          );
         }
 
         // Cleanup.
         await closeStream(true);
 
-        Perms.remove(uri, "camera");
-        Perms.remove(uri, "microphone");
+        PermissionTestUtils.remove(uri, "camera");
+        PermissionTestUtils.remove(uri, "microphone");
       }
 
       info("request audio+video, stop sharing resets both");
       await stopAndCheckPerm(true, true);
       info("request audio, stop sharing resets audio only");
       await stopAndCheckPerm(true, false);
       info("request video, stop sharing resets video only");
       await stopAndCheckPerm(false, true);
@@ -640,20 +705,19 @@ var gTests = [
       let browser = gBrowser.selectedBrowser;
       BrowserTestUtils.loadURI(
         browser,
         browser.documentURI.spec.replace("https://", "http://")
       );
       await BrowserTestUtils.browserLoaded(browser);
 
       // Initially set both permissions to 'allow'.
-      let Perms = Services.perms;
       let uri = browser.documentURI;
-      Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
-      Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+      PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+      PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
 
       // Request devices and expect a prompt despite the saved 'Allow' permission,
       // because the connection isn't secure.
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
 
@@ -668,17 +732,17 @@ var gTests = [
       ok(notification.button.disabled, "Allow button is disabled");
       ok(
         !notification.hasAttribute("warninghidden"),
         "warning message is shown"
       );
 
       // Cleanup.
       await closeStream(true);
-      Perms.remove(uri, "camera");
-      Perms.remove(uri, "microphone");
+      PermissionTestUtils.remove(uri, "camera");
+      PermissionTestUtils.remove(uri, "microphone");
     },
   },
 ];
 
 add_task(async function test() {
   await runTests(gTests);
 });
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
@@ -66,32 +66,36 @@ var gTests = [
   },
 
   {
     desc: "getUserMedia video: globally blocking camera + local exception",
     run: async function checkAudioVideo() {
       let browser = gBrowser.selectedBrowser;
       Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
       // Overwrite the permission for that URI, requesting video should work again.
-      SitePermissions.set(browser.currentURI, "camera", SitePermissions.ALLOW);
+      PermissionTestUtils.add(
+        browser.currentURI,
+        "camera",
+        Services.perms.ALLOW_ACTION
+      );
 
       // Requesting video should work.
       let indicator = promiseIndicatorWindow();
       let promise = promiseMessage("ok");
       await promiseRequestDevice(false, true);
       await promise;
 
       await expectObserverCalled("getUserMedia:request");
       await expectObserverCalled("getUserMedia:response:allow");
       await expectObserverCalled("recording-device-events");
       await indicator;
       await checkSharingUI({ video: true });
       await closeStream();
 
-      SitePermissions.remove(browser.currentURI, "camera");
+      PermissionTestUtils.remove(browser.currentURI, "camera");
       Services.prefs.clearUserPref(CAMERA_PREF);
     },
   },
 
   {
     desc: "getUserMedia audio+video: globally blocking microphone",
     run: async function checkAudioVideo() {
       Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
@@ -147,35 +151,35 @@ var gTests = [
   },
 
   {
     desc: "getUserMedia audio: globally blocking microphone + local exception",
     run: async function checkAudioVideo() {
       let browser = gBrowser.selectedBrowser;
       Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
       // Overwrite the permission for that URI, requesting video should work again.
-      SitePermissions.set(
+      PermissionTestUtils.add(
         browser.currentURI,
         "microphone",
-        SitePermissions.ALLOW
+        Services.perms.ALLOW_ACTION
       );
 
       // Requesting audio should work.
       let indicator = promiseIndicatorWindow();
       let promise = promiseMessage("ok");
       await promiseRequestDevice(true);
       await promise;
 
       await expectObserverCalled("getUserMedia:request");
       await expectObserverCalled("getUserMedia:response:allow");
       await expectObserverCalled("recording-device-events");
       await indicator;
       await checkSharingUI({ audio: true });
       await closeStream();
 
-      SitePermissions.remove(browser.currentURI, "microphone");
+      PermissionTestUtils.remove(browser.currentURI, "microphone");
       Services.prefs.clearUserPref(MICROPHONE_PREF);
     },
   },
 ];
 add_task(async function test() {
   await runTests(gTests);
 });
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -60,40 +60,39 @@ var gTests = [
         await getMediaCaptureState(),
         { audio: true, video: true },
         "expected camera and microphone to be shared"
       );
 
       await indicator;
       await checkSharingUI({ video: true, audio: true });
 
-      let Perms = Services.perms;
       let uri = Services.io.newURI("https://example.com/");
       is(
-        Perms.testExactPermission(uri, "microphone"),
-        Perms.ALLOW_ACTION,
+        PermissionTestUtils.testExactPermission(uri, "microphone"),
+        Services.perms.ALLOW_ACTION,
         "microphone persistently allowed"
       );
       is(
-        Perms.testExactPermission(uri, "camera"),
-        Perms.ALLOW_ACTION,
+        PermissionTestUtils.testExactPermission(uri, "camera"),
+        Services.perms.ALLOW_ACTION,
         "camera persistently allowed"
       );
 
       await stopSharing();
 
       // The persistent permissions for the frame should have been removed.
       is(
-        Perms.testExactPermission(uri, "microphone"),
-        Perms.UNKNOWN_ACTION,
+        PermissionTestUtils.testExactPermission(uri, "microphone"),
+        Services.perms.UNKNOWN_ACTION,
         "microphone not persistently allowed"
       );
       is(
-        Perms.testExactPermission(uri, "camera"),
-        Perms.UNKNOWN_ACTION,
+        PermissionTestUtils.testExactPermission(uri, "camera"),
+        Services.perms.UNKNOWN_ACTION,
         "camera not persistently allowed"
       );
 
       // the stream is already closed, but this will do some cleanup anyway
       await closeStream(true, "frame1");
     },
   },
 
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
@@ -39,17 +39,21 @@ var gTests = [
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(true, false);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // close all streams
       await closeStream();
     },
   },
 
   {
     desc: "test queueing allow video behind deny audio",
@@ -83,17 +87,21 @@ var gTests = [
       Assert.deepEqual(
         await getMediaCaptureState(),
         { video: true },
         "expected camera to be shared"
       );
       await indicator;
       await checkSharingUI({ audio: false, video: true });
 
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // close all streams
       await closeStream();
     },
   },
 
   {
     desc: "test queueing allow audio behind allow video with error",
@@ -158,16 +166,20 @@ var gTests = [
       promise = promiseSpecificMessageReceived(permissionError, 2);
       activateSecondaryAction(kActionDeny);
       await promise;
 
       await expectObserverCalled("getUserMedia:request");
       await expectObserverCalled("getUserMedia:response:deny", 2);
       await expectObserverCalled("recording-window-ended");
 
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 ];
 
 add_task(async function test() {
   await runTests(gTests);
 });
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -173,17 +173,21 @@ var gTests = [
       );
       checkDeviceSelectors(false, false, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
       await closeStream();
     },
   },
 
   {
     desc: "getUserMedia window/screen picking window",
     run: async function checkWindowOrScreen() {
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
@@ -457,18 +461,26 @@ var gTests = [
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia audio + window/screen: stop sharing",
     run: async function checkStopSharing() {
       if (AppConstants.platform == "macosx") {
         todo(
@@ -631,18 +643,21 @@ var gTests = [
       await closeStream();
     },
   },
 
   {
     desc: "Only persistent block is possible for screen sharing",
     run: async function checkPersistentPermissions() {
       let browser = gBrowser.selectedBrowser;
-      let uri = browser.documentURI;
-      let devicePerms = SitePermissions.get(uri, "screen", browser);
+      let devicePerms = SitePermissions.getForPrincipal(
+        browser.contentPrincipal,
+        "screen",
+        browser
+      );
       is(
         devicePerms.state,
         SitePermissions.UNKNOWN,
         "starting without screen persistent permissions"
       );
 
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(false, true, null, "screen");
@@ -675,32 +690,40 @@ var gTests = [
       // Click "Don't Allow" to save a persistent block permission.
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
 
-      let permission = SitePermissions.get(uri, "screen", browser);
+      let permission = SitePermissions.getForPrincipal(
+        browser.contentPrincipal,
+        "screen",
+        browser
+      );
       is(permission.state, SitePermissions.BLOCK, "screen sharing is blocked");
       is(
         permission.scope,
         SitePermissions.SCOPE_PERSISTENT,
         "screen sharing is persistently blocked"
       );
 
       // Request screensharing again, expect an immediate failure.
       promise = promiseMessage(permissionError);
       await promiseRequestDevice(false, true, null, "screen");
       await promise;
       await expectObserverCalled("recording-window-ended");
 
       // Now set the permission to allow and expect a prompt.
-      SitePermissions.set(uri, "screen", SitePermissions.ALLOW);
+      SitePermissions.setForPrincipal(
+        browser.contentPrincipal,
+        "screen",
+        SitePermissions.ALLOW
+      );
 
       // Request devices and expect a prompt despite the saved 'Allow' permission.
       promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(false, true, null, "screen");
       await promise;
       await expectObserverCalled("getUserMedia:request");
 
       // The 'remember' checkbox shouldn't be checked anymore.
@@ -714,17 +737,21 @@ var gTests = [
       ok(!checkbox.checked, "checkbox is not checked");
 
       // Deny the request to cleanup...
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
-      SitePermissions.remove(uri, "screen", browser);
+      SitePermissions.removeFromPrincipal(
+        browser.contentPrincipal,
+        "screen",
+        browser
+      );
     },
   },
 
   {
     desc:
       "Switching between menu options maintains correct main action state while window sharing",
     run: async function checkDoorhangerState() {
       let win = await BrowserTestUtils.openNewBrowserWindow();
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
@@ -61,38 +61,62 @@ var gTests = [
       );
       checkDeviceSelectors(false, false, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // After closing all streams, gUM(audio+camera) causes a prompt.
       await closeStream();
       promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(true, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
       await checkNotSharing();
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia camera",
     run: async function checkAudioVideoWhileLiveTracksExist_camera() {
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(false, true);
@@ -122,35 +146,59 @@ var gTests = [
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(true, false);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // gUM(audio+camera) causes a prompt;
       promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(true, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // gUM(screen) causes a prompt;
       promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(false, true, null, "screen");
       await promise;
       await expectObserverCalled("getUserMedia:request");
 
       is(
@@ -160,19 +208,31 @@ var gTests = [
       );
       checkDeviceSelectors(false, false, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // gUM(camera) returns a stream without prompting.
       promise = promiseMessage("ok");
       await promiseRequestDevice(false, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       await promiseNoPopupNotification("webRTC-shareDevices");
       await expectObserverCalled("getUserMedia:response:allow");
@@ -222,35 +282,59 @@ var gTests = [
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(false, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // gUM(audio+camera) causes a prompt;
       promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       checkDeviceSelectors(true, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // gUM(audio) returns a stream without prompting.
       promise = promiseMessage("ok");
       await promiseRequestDevice(true, false);
       await promise;
       await expectObserverCalled("getUserMedia:request");
       await promiseNoPopupNotification("webRTC-shareDevices");
       await expectObserverCalled("getUserMedia:response:allow");
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
@@ -40,19 +40,31 @@ var gTests = [
       checkDeviceSelectors(true, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
 
       // If there's an active audio+camera stream in frame 1,
       // gUM(audio+camera) in frame 1 returns a stream without prompting;
       promise = promiseMessage("ok");
       await promiseRequestDevice(true, true, "frame1");
       await promise;
       await expectObserverCalled("getUserMedia:request");
       await promiseNoPopupNotification("webRTC-shareDevices");
@@ -101,19 +113,31 @@ var gTests = [
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
 
       // close the stream
       await closeStream(false, "frame1");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia audio+camera in frame 1 - reload",
     run: async function checkAudioVideoWhileLiveTracksExist_frame_reload() {
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
       await promiseRequestDevice(true, true, "frame1");
@@ -156,19 +180,31 @@ var gTests = [
       checkDeviceSelectors(true, true);
 
       await promiseMessage(permissionError, () => {
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 
   {
     desc: "getUserMedia audio+camera at the top level window",
     run: async function checkAudioVideoWhileLiveTracksExist_topLevel() {
       // create an active audio+camera stream at the top level window
       let promise = promisePopupNotificationShown("webRTC-shareDevices");
@@ -205,18 +241,30 @@ var gTests = [
         activateSecondaryAction(kActionDeny);
       });
 
       await expectObserverCalled("getUserMedia:response:deny");
       await expectObserverCalled("recording-window-ended");
 
       // close the stream
       await closeStream();
-      SitePermissions.remove(null, "screen", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "camera", gBrowser.selectedBrowser);
-      SitePermissions.remove(null, "microphone", gBrowser.selectedBrowser);
+      SitePermissions.removeFromPrincipal(
+        null,
+        "screen",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "camera",
+        gBrowser.selectedBrowser
+      );
+      SitePermissions.removeFromPrincipal(
+        null,
+        "microphone",
+        gBrowser.selectedBrowser
+      );
     },
   },
 ];
 
 add_task(async function test() {
   await runTests(gTests, { relativeURI: "get_user_media_in_frame.html" });
 });
--- a/browser/base/content/test/webrtc/head.js
+++ b/browser/base/content/test/webrtc/head.js
@@ -1,14 +1,17 @@
 var { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 var { SitePermissions } = ChromeUtils.import(
   "resource:///modules/SitePermissions.jsm"
 );
+var { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
 const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
 const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
 const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
 const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
 const CONTENT_SCRIPT_HELPER =
   getRootDirectory(gTestPath) + "get_user_media_content_script.js";
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -10,23 +10,23 @@ pref("app.update.interval", 43200); // 1
 // Give the user x seconds to react before showing the big UI. default=192 hours
 pref("app.update.promptWaitTime", 691200);
 // app.update.url.manual: URL user can browse to manually if for some reason
 // all update installation attempts fail.
 // app.update.url.details: a default value for the "More information about this
 // update" link supplied in the "An update is available" page of the update
 // wizard.
 #if MOZ_UPDATE_CHANNEL == beta
-pref("app.update.url.manual", "https://www.mozilla.org/firefox/beta");
-pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/beta/notes");
-pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%beta/releasenotes/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=whatsnew");
+  pref("app.update.url.manual", "https://www.mozilla.org/firefox/beta");
+  pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/beta/notes");
+  pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%beta/releasenotes/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=whatsnew");
 #else
-pref("app.update.url.manual", "https://www.mozilla.org/firefox/");
-pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
-pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/releasenotes/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=whatsnew");
+  pref("app.update.url.manual", "https://www.mozilla.org/firefox/");
+  pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
+  pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/releasenotes/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=whatsnew");
 #endif
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 63);
 
 // Give the user x seconds to reboot before showing a badge on the hamburger
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -130,16 +130,17 @@ let LEGACY_ACTORS = {
   AboutLogins: {
     child: {
       matches: ["about:logins", "about:logins?*"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
         AboutLoginsCopyLoginDetail: { wantUntrusted: true },
         AboutLoginsCreateLogin: { wantUntrusted: true },
         AboutLoginsDeleteLogin: { wantUntrusted: true },
+        AboutLoginsDismissBreachAlert: { wantUntrusted: true },
         AboutLoginsImport: { wantUntrusted: true },
         AboutLoginsInit: { wantUntrusted: true },
         AboutLoginsOpenFAQ: { wantUntrusted: true },
         AboutLoginsOpenFeedback: { wantUntrusted: true },
         AboutLoginsOpenMobileAndroid: { wantUntrusted: true },
         AboutLoginsOpenMobileIos: { wantUntrusted: true },
         AboutLoginsOpenPreferences: { wantUntrusted: true },
         AboutLoginsOpenSite: { wantUntrusted: true },
@@ -631,16 +632,17 @@ const listeners = {
 
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
     "AboutLogins:CreateLogin": ["AboutLoginsParent"],
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
+    "AboutLogins:DismissBreachAlert": ["AboutLoginsParent"],
     "AboutLogins:Import": ["AboutLoginsParent"],
     "AboutLogins:MasterPasswordRequest": ["AboutLoginsParent"],
     "AboutLogins:OpenFAQ": ["AboutLoginsParent"],
     "AboutLogins:OpenFeedback": ["AboutLoginsParent"],
     "AboutLogins:OpenPreferences": ["AboutLoginsParent"],
     "AboutLogins:OpenMobileAndroid": ["AboutLoginsParent"],
     "AboutLogins:OpenMobileIos": ["AboutLoginsParent"],
     "AboutLogins:OpenSite": ["AboutLoginsParent"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -85,16 +85,22 @@ class AboutLoginsChild extends ActorChil
         break;
       }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {
           login: event.detail,
         });
         break;
       }
+      case "AboutLoginsDismissBreachAlert": {
+        this.mm.sendAsyncMessage("AboutLogins:DismissBreachAlert", {
+          login: event.detail,
+        });
+        break;
+      }
       case "AboutLoginsImport": {
         this.mm.sendAsyncMessage("AboutLogins:Import");
         break;
       }
       case "AboutLoginsOpenFAQ": {
         this.mm.sendAsyncMessage("AboutLogins:OpenFAQ");
         break;
       }
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -118,16 +118,31 @@ var AboutLoginsParent = {
         Services.logins.addLogin(LoginHelper.vanillaObjectToLogin(newLogin));
         break;
       }
       case "AboutLogins:DeleteLogin": {
         let login = LoginHelper.vanillaObjectToLogin(message.data.login);
         Services.logins.removeLogin(login);
         break;
       }
+      case "AboutLogins:DismissBreachAlert": {
+        const login = message.data.login;
+
+        await LoginHelper.recordBreachAlertDismissal(login.guid);
+        const logins = await this.getAllLogins();
+        const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+          logins
+        );
+        const messageManager = message.target.messageManager;
+        messageManager.sendAsyncMessage(
+          "AboutLogins:UpdateBreaches",
+          breachesByLoginGUID
+        );
+        break;
+      }
       case "AboutLogins:SyncEnable": {
         message.target.ownerGlobal.gSync.openFxAEmailFirstPage(
           "password-manager"
         );
         break;
       }
       case "AboutLogins:SyncOptions": {
         message.target.ownerGlobal.gSync.openFxAManagePage("password-manager");
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -132,16 +132,17 @@
 
     <template id="login-item-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
       <div class="breach-alert">
         <span class="breach-alert-text" data-l10n-id="breach-alert-text"></span>
         <a class="breach-alert-link" data-l10n-id="breach-alert-link" href="#" rel="noopener noreferer" target="_blank"></a>
+        <button class="dismiss-breach-alert"></button>
       </div>
       <div class="header">
         <div class="login-item-favicon-wrapper">
           <img class="login-item-favicon" src="" alt=""/>
         </div>
         <h2 class="title">
           <span class="login-item-title"></span>
           <span class="new-login-title" data-l10n-id="login-item-new-login-title"></span>
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -196,16 +196,17 @@ input[type="url"][readOnly]:hover:active
 .login-item-favicon {
   width: 24px;
 }
 
 .login-item-favicon-wrapper {
   margin-inline-end: 12px;
   height: 24px;
   width: 24px;
+  flex-shrink: 0;
   background-image: url("chrome://mozapps/skin/places/defaultFavicon.svg");
   background-repeat: no-repeat;
   background-size: contain;
   -moz-context-properties: fill;
 }
 
 .login-item-favicon-wrapper.hide-default-favicon {
   background-image: none;
@@ -223,28 +224,51 @@ input[type="url"][readOnly]:hover:active
   box-shadow: 0 2px 8px 0 rgba(12,12,13,0.1);
   font-size: .9em;
   font-weight: 300;
   line-height: 1.4;
   padding-block: 12px;
   padding-inline-start: 36px;
   padding-inline-end: 92px;
   margin-block-end: 40px;
+  position: relative;
 }
 
 .breach-alert:dir(rtl) {
   background-position: right 10px top 10px;
 }
 
 a.breach-alert-link {
   color: inherit;
   text-decoration: underline;
   font-weight: 500;
 }
 
+.dismiss-breach-alert {
+  border: none;
+  padding: 0;
+  margin: 0;
+  position: absolute;
+  background-image: url("chrome://global/skin/icons/close.svg");
+  background-repeat: no-repeat;
+  background-size: contain;
+  min-height: 16px;
+  min-width: 16px;
+  -moz-context-properties: fill, fill-opacity;
+  fill-opacity: 0;
+  fill: var(--grey-90);
+  inset-inline-end: 12px;
+  inset-block-start: 12px
+}
+
+.dismiss-breach-alert,
+.dismiss-breach-alert:hover {
+  background-color: transparent;
+}
+
 @supports -moz-bool-pref("browser.in-content.dark-mode") {
   @media (prefers-color-scheme: dark) {
     :host {
       --reveal-checkbox-opacity: .8;
       --reveal-checkbox-opacity-hover: 1;
       --reveal-checkbox-opacity-active: .6;
       --success-color: #86DE74;
     }
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -60,24 +60,28 @@ export default class LoginItem extends H
     this._faviconWrapper = this.shadowRoot.querySelector(
       ".login-item-favicon-wrapper"
     );
     this._title = this.shadowRoot.querySelector(".login-item-title");
     this._timeCreated = this.shadowRoot.querySelector(".time-created");
     this._timeChanged = this.shadowRoot.querySelector(".time-changed");
     this._timeUsed = this.shadowRoot.querySelector(".time-used");
     this._breachAlert = this.shadowRoot.querySelector(".breach-alert");
+    this._dismissBreachAlert = this.shadowRoot.querySelector(
+      ".dismiss-breach-alert"
+    );
 
     this.render();
 
     this._originInput.addEventListener("blur", this);
     this._cancelButton.addEventListener("click", this);
     this._copyPasswordButton.addEventListener("click", this);
     this._copyUsernameButton.addEventListener("click", this);
     this._deleteButton.addEventListener("click", this);
+    this._dismissBreachAlert.addEventListener("click", this);
     this._editButton.addEventListener("click", this);
     this._form.addEventListener("submit", this);
     this._openSiteButton.addEventListener("click", this);
     this._originInput.addEventListener("click", this);
     this._revealCheckbox.addEventListener("click", this);
     window.addEventListener("AboutLoginsInitialLoginSelected", this);
     window.addEventListener("AboutLoginsLoadInitialFavicon", this);
     window.addEventListener("AboutLoginsLoginSelected", this);
@@ -87,16 +91,20 @@ export default class LoginItem extends H
   async render() {
     this._breachAlert.hidden = true;
     if (this._breachesMap && this._breachesMap.has(this._login.guid)) {
       const breachDetails = this._breachesMap.get(this._login.guid);
       const breachAlertLink = this._breachAlert.querySelector(
         ".breach-alert-link"
       );
       breachAlertLink.href = breachDetails.breachAlertURL;
+      document.l10n.setAttributes(
+        this._dismissBreachAlert,
+        "breach-alert-dismiss"
+      );
       this._breachAlert.hidden = false;
     }
     document.l10n.setAttributes(this._timeCreated, "login-item-time-created", {
       timeCreated: this._login.timeCreated || "",
     });
     document.l10n.setAttributes(this._timeChanged, "login-item-time-changed", {
       timeChanged: this._login.timePasswordChanged || "",
     });
@@ -130,16 +138,25 @@ export default class LoginItem extends H
     await this._updatePasswordRevealState();
   }
 
   updateBreaches(breachesByLoginGUID) {
     this._breachesMap = breachesByLoginGUID;
     this.render();
   }
 
+  dismissBreachAlert() {
+    document.dispatchEvent(
+      new CustomEvent("AboutLoginsDismissBreachAlert", {
+        bubbles: true,
+        detail: this._login,
+      })
+    );
+  }
+
   async handleEvent(event) {
     switch (event.type) {
       case "AboutLoginsInitialLoginSelected": {
         this.setLogin(event.detail, { skipFocusChange: true });
         break;
       }
       case "AboutLoginsLoadInitialFavicon": {
         this.render();
@@ -246,16 +263,20 @@ export default class LoginItem extends H
               new CustomEvent("AboutLoginsDeleteLogin", {
                 bubbles: true,
                 detail: this._login,
               })
             );
           });
           return;
         }
+        if (classList.contains("dismiss-breach-alert")) {
+          this.dismissBreachAlert();
+          return;
+        }
         if (classList.contains("edit-button")) {
           this._toggleEditing();
 
           recordTelemetryEvent({ object: "existing_login", method: "edit" });
           return;
         }
         if (
           classList.contains("open-site-button") ||
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -85,26 +85,25 @@ export default class LoginList extends H
       });
       fragment.appendChild(listItem);
     }
     this._list.appendChild(fragment);
 
     // Show, hide, and update state of the list items per the applied search filter.
     for (let guid of this._loginGuidsSortedOrder) {
       let { listItem } = this._logins[guid];
+
       if (guid == this._selectedGuid) {
         this._setListItemAsSelected(listItem);
       }
-      if (
+      listItem.classList.toggle(
+        "breached",
         this._breachesByLoginGUID &&
-        this._breachesByLoginGUID.has(listItem.dataset.guid)
-      ) {
-        listItem.classList.add("breached");
-      }
-
+          this._breachesByLoginGUID.has(listItem.dataset.guid)
+      );
       listItem.hidden = !visibleLoginGuids.has(listItem.dataset.guid);
     }
 
     let createLoginSelected =
       this._selectedGuid == null && Object.keys(this._logins).length > 0;
     this.classList.toggle("create-login-selected", createLoginSelected);
     this._createLoginButton.disabled = createLoginSelected;
 
@@ -269,25 +268,27 @@ export default class LoginList extends H
 
   /**
    * @param {Map} breachesByLoginGUID A Map of breaches by login GUIDs used
    *                                  for displaying breached login indicators.
    */
   updateBreaches(breachesByLoginGUID) {
     this._breachesByLoginGUID = breachesByLoginGUID;
     if (this._breachesByLoginGUID.size === 0) {
+      this.render();
       return;
     }
     const breachedSortOptionElement = this._sortSelect.namedItem("breached");
     breachedSortOptionElement.hidden = false;
     this._sortSelect.selectedIndex = breachedSortOptionElement.index;
     this._sortSelect.dispatchEvent(new CustomEvent("input", { bubbles: true }));
     this._sortSelect.dispatchEvent(
       new CustomEvent("change", { bubbles: true })
     );
+    this.render();
   }
 
   /**
    * @param {login} login A login that was added to storage.
    */
   loginAdded(login) {
     this._logins[login.guid] = { login };
     this._loginGuidsSortedOrder.push(login.guid);
@@ -467,27 +468,29 @@ export default class LoginList extends H
       }
       default:
         return;
     }
     event.preventDefault();
     this._list.setAttribute("aria-activedescendant", newlyFocusedItem.id);
     activeDescendant.classList.remove("keyboard-selected");
     newlyFocusedItem.classList.add("keyboard-selected");
-    newlyFocusedItem.scrollIntoView(false);
+    newlyFocusedItem.scrollIntoView({ block: "nearest" });
   }
 
   _setListItemAsSelected(listItem) {
     let oldSelectedItem = this._list.querySelector(".selected");
     if (oldSelectedItem) {
       oldSelectedItem.classList.remove("selected");
       oldSelectedItem.removeAttribute("aria-selected");
     }
     this.classList.toggle("create-login-selected", !listItem.dataset.guid);
     this._createLoginButton.disabled = !listItem.dataset.guid;
     listItem.classList.add("selected");
     listItem.setAttribute("aria-selected", "true");
-    listItem.scrollIntoView();
     this._list.setAttribute("aria-activedescendant", listItem.id);
     this._selectedGuid = listItem.dataset.guid;
+
+    // Scroll item into view if it isn't visible
+    listItem.scrollIntoView({ block: "nearest" });
   }
 }
 customElements.define("login-list", LoginList);
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -3,16 +3,17 @@ prefs =
   signon.management.page.enabled=true
 support-files =
   head.js
 
 # Run first so content events from previous tests won't trickle in.
 # Skip ASAN and debug since waiting for content events is already slow.
 [browser_aaa_eventTelemetry_run_first.js]
 skip-if = asan || debug
+[browser_breachAlertDismissals.js]
 [browser_confirmDeleteDialog.js]
 [browser_copyToClipboardButton.js]
 [browser_createLogin.js]
 [browser_deleteLogin.js]
 [browser_fxAccounts.js]
 [browser_loginListChanges.js]
 [browser_masterPassword.js]
 skip-if = (os == 'linux') # bug 1569789
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_breachAlertDismissals.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_BREACHES = [
+  {
+    AddedDate: "2019-12-20T23:56:26Z",
+    BreachDate: "2018-12-16",
+    Domain: "breached.com",
+    Name: "Breached",
+    PwnCount: 1643100,
+    DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+    _status: "synced",
+    id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+    last_modified: "1541615610052",
+    schema: "1541615609018",
+  },
+];
+
+add_task(async function setup() {
+  TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+  await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url: "about:logins",
+  });
+  registerCleanupFunction(() => {
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    Services.logins.removeAllLogins();
+  });
+});
+
+add_task(async function test_show_login() {
+  let browser = gBrowser.selectedBrowser;
+  TEST_LOGIN3.timePasswordChanged = 12345;
+  let testBreaches = await LoginHelper.getBreachesForLogins(
+    [TEST_LOGIN3],
+    TEST_BREACHES
+  );
+  browser.messageManager.sendAsyncMessage(
+    "AboutLogins:UpdateBreaches",
+    testBreaches
+  );
+  await ContentTask.spawn(browser, TEST_LOGIN3, async () => {
+    let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+    let breachAlert = loginItem.shadowRoot.querySelector(".breach-alert");
+    let breachAlertVisible = await ContentTaskUtils.waitForCondition(() => {
+      return !breachAlert.hidden;
+    }, "Waiting for breach alert to be visible");
+    ok(
+      breachAlertVisible,
+      "Breach alert should be visible for a breached login."
+    );
+
+    let breachAlertDismissalButton = breachAlert.querySelector(
+      ".dismiss-breach-alert"
+    );
+    breachAlertDismissalButton.click();
+
+    let breachAlertDismissed = await ContentTaskUtils.waitForCondition(() => {
+      return breachAlert.hidden;
+    }, "Waiting for breach alert to be dismissed");
+    ok(
+      breachAlertDismissed,
+      "Breach alert should not be visible after alert dismissal."
+    );
+  });
+});
--- a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
@@ -1,21 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(async function setup() {
+  TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+  TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
   await BrowserTestUtils.openNewForegroundTab({
     gBrowser,
     url: "about:logins",
   });
   registerCleanupFunction(() => {
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   });
-  TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
-  TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
 });
 
 add_task(async function test_show_logins() {
   let browser = gBrowser.selectedBrowser;
 
   await ContentTask.spawn(browser, [TEST_LOGIN1, TEST_LOGIN2], async logins => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginFound = await ContentTaskUtils.waitForCondition(() => {
--- a/browser/components/aboutlogins/tests/browser/head.js
+++ b/browser/components/aboutlogins/tests/browser/head.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let nsLoginInfo = new Components.Constructor(
   "@mozilla.org/login-manager/loginInfo;1",
   Ci.nsILoginInfo,
   "init"
 );
+
 let TEST_LOGIN1 = new nsLoginInfo(
   "https://example.com/",
   "https://example.com/",
   null,
   "user1",
   "pass1",
   "username",
   "password"
@@ -20,16 +21,26 @@ let TEST_LOGIN2 = new nsLoginInfo(
   "https://2.example.com/",
   null,
   "user2",
   "pass2",
   "username",
   "password"
 );
 
+let TEST_LOGIN3 = new nsLoginInfo(
+  "https://breached.com/",
+  "https://breached.com/",
+  null,
+  "breachedLogin1",
+  "pass3",
+  "breachedLogin",
+  "password"
+);
+
 async function addLogin(login) {
   let storageChangedPromised = TestUtils.topicObserved(
     "passwordmgr-storage-changed",
     (_, data) => data == "addLogin"
   );
   login = Services.logins.addLogin(login);
   await storageChangedPromised;
   registerCleanupFunction(() => {
--- a/browser/components/aboutlogins/tests/chrome/test_login_item.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_item.html
@@ -96,27 +96,27 @@ add_task(async function test_set_login()
 
 add_task(async function test_update_breaches() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   gLoginItem.updateBreaches(TEST_BREACHES_MAP);
   await asyncElementRendered();
 
   let correspondingBreach = TEST_BREACHES_MAP.get(gLoginItem._login.guid);
   let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
-  ok(!breachAlert.hidden, "Breach alert should be visible");
+  ok(!isHidden(breachAlert), "Breach alert should be visible");
   is(breachAlert.querySelector(".breach-alert-link").href, correspondingBreach.breachAlertURL, "Breach alert link should be equal to the correspondingBreach.breachAlertURL.");
 });
 
 add_task(async function test_breach_alert_is_correctly_hidden() {
   gLoginItem.setLogin(TEST_LOGIN_2);
   gLoginItem.updateBreaches(TEST_BREACHES_MAP);
   await asyncElementRendered();
 
   let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
-  ok(breachAlert.hidden, "Breach alert should not be visible on login without an associated breach.");
+  ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach.");
 });
 
 add_task(async function test_edit_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   gLoginItem.shadowRoot.querySelector(".edit-button").click();
   await asyncElementRendered();
 
   ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
--- a/browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
+++ b/browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
@@ -158,8 +158,46 @@ add_task(
     Assert.strictEqual(
       breachesByLoginGUID.size,
       0,
       "Should be 0 breached login: " +
         LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
     );
   }
 );
+
+add_task(
+  async function test_getBreachesForLogins_breachAlertHiddenAfterDismissal() {
+    BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}";
+
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+
+    storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid);
+
+    const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+      [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
+      TEST_BREACHES
+    );
+    Assert.strictEqual(
+      breachesByLoginGUID.size,
+      0,
+      "Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin
+    );
+  }
+);
+
+add_task(async function test_getBreachesForLogins_newBreachAfterDismissal() {
+  TEST_BREACHES[0].AddedDate = new Date().toISOString();
+
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+    [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
+    TEST_BREACHES
+  );
+
+  Assert.strictEqual(
+    breachesByLoginGUID.size,
+    1,
+    "Should be 1 breached login after new breach following the dismissal of a previous breach: " +
+      BREACHED_LOGIN.origin
+  );
+});
--- a/browser/components/enterprisepolicies/tests/xpcshell/head.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/head.js
@@ -10,16 +10,19 @@ const { Preferences } = ChromeUtils.impo
   "resource://gre/modules/Preferences.jsm"
 );
 const { updateAppInfo, getAppInfo } = ChromeUtils.import(
   "resource://testing-common/AppInfo.jsm"
 );
 const { FileTestUtils } = ChromeUtils.import(
   "resource://testing-common/FileTestUtils.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 updateAppInfo({
   name: "XPCShell",
   ID: "xpcshell@tests.mozilla.org",
   version: "48",
   platformVersion: "48",
 });
 
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js
@@ -9,64 +9,64 @@ function URI(str) {
 
 add_task(async function test_setup_preexisting_permissions() {
   // Pre-existing ALLOW permissions that should be overriden
   // with DENY.
 
   // No ALLOW -> DENY override for popup and install permissions,
   // because their policies only supports the Allow parameter.
 
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "camera",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "microphone",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "geo",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "desktop-notification",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
   // Pre-existing DENY permissions that should be overriden
   // with ALLOW.
 
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "camera",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "microphone",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "geo",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "desktop-notification",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 });
 
 add_task(async function test_setup_activate_policies() {
   await setupPolicyEngineWithJson({
@@ -98,23 +98,23 @@ add_task(async function test_setup_activ
   );
 });
 
 function checkPermission(url, expected, permissionName) {
   let expectedValue = Ci.nsIPermissionManager[`${expected}_ACTION`];
   let uri = Services.io.newURI(`https://www.${url}`);
 
   equal(
-    Services.perms.testPermission(uri, permissionName),
+    PermissionTestUtils.testPermission(uri, permissionName),
     expectedValue,
     `Correct (${permissionName}=${expected}) for URL ${url}`
   );
 
   if (expected != "UNKNOWN") {
-    let permission = Services.perms.getPermissionObjectForURI(
+    let permission = PermissionTestUtils.getPermissionObject(
       uri,
       permissionName,
       true
     );
     ok(permission, "Permission object exists");
     equal(
       permission.expireType,
       Ci.nsIPermissionManager.EXPIRE_POLICY,
@@ -148,65 +148,65 @@ add_task(async function test_location_po
 
 add_task(async function test_notifications_policy() {
   checkAllPermissionsForType("desktop-notification");
 });
 
 add_task(async function test_change_permission() {
   // Checks that changing a permission will still retain the
   // value set through the engine.
-  Services.perms.add(
-    URI("https://www.allow.com"),
+  PermissionTestUtils.add(
+    "https://www.allow.com",
     "camera",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.allow.com"),
+  PermissionTestUtils.add(
+    "https://www.allow.com",
     "microphone",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.allow.com"),
+  PermissionTestUtils.add(
+    "https://www.allow.com",
     "geo",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.allow.com"),
+  PermissionTestUtils.add(
+    "https://www.allow.com",
     "desktop-notification",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
   checkPermission("allow.com", "ALLOW", "camera");
   checkPermission("allow.com", "ALLOW", "microphone");
   checkPermission("allow.com", "ALLOW", "geo");
   checkPermission("allow.com", "ALLOW", "desktop-notification");
 
   // Also change one un-managed permission to make sure it doesn't
   // cause any problems to the policy engine or the permission manager.
-  Services.perms.add(
-    URI("https://www.unmanaged.com"),
+  PermissionTestUtils.add(
+    "https://www.unmanaged.com",
     "camera",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.unmanaged.com"),
+  PermissionTestUtils.add(
+    "https://www.unmanaged.com",
     "microphone",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.unmanaged.com"),
+  PermissionTestUtils.add(
+    "https://www.unmanaged.com",
     "geo",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
-  Services.perms.add(
-    URI("https://www.unmanaged.com"),
+  PermissionTestUtils.add(
+    "https://www.unmanaged.com",
     "desktop-notification",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 });
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_popups_cookies_addons_flash.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_popups_cookies_addons_flash.js
@@ -9,55 +9,55 @@ function URI(str) {
 
 add_task(async function test_setup_preexisting_permissions() {
   // Pre-existing ALLOW permissions that should be overriden
   // with DENY.
 
   // No ALLOW -> DENY override for popup and install permissions,
   // because their policies only supports the Allow parameter.
 
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "cookie",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
-  Services.perms.add(
-    URI("https://www.pre-existing-allow.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-allow.com",
     "plugin:flash",
     Ci.nsIPermissionManager.ALLOW_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
   // Pre-existing DENY permissions that should be overriden
   // with ALLOW.
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "popup",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "install",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "cookie",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
-  Services.perms.add(
-    URI("https://www.pre-existing-deny.com"),
+  PermissionTestUtils.add(
+    "https://www.pre-existing-deny.com",
     "plugin:flash",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 });
 
 add_task(async function test_setup_activate_policies() {
   await setupPolicyEngineWithJson("config_popups_cookies_addons_flash.json");
@@ -68,23 +68,23 @@ add_task(async function test_setup_activ
   );
 });
 
 function checkPermission(url, expected, permissionName) {
   let expectedValue = Ci.nsIPermissionManager[`${expected}_ACTION`];
   let uri = Services.io.newURI(`https://www.${url}`);
 
   equal(
-    Services.perms.testPermission(uri, permissionName),
+    PermissionTestUtils.testPermission(uri, permissionName),
     expectedValue,
     `Correct (${permissionName}=${expected}) for URL ${url}`
   );
 
   if (expected != "UNKNOWN") {
-    let permission = Services.perms.getPermissionObjectForURI(
+    let permission = PermissionTestUtils.getPermissionObject(
       uri,
       permissionName,
       true
     );
     ok(permission, "Permission object exists");
     equal(
       permission.expireType,
       Ci.nsIPermissionManager.EXPIRE_POLICY,
@@ -118,26 +118,26 @@ add_task(async function test_cookies_pol
 
 add_task(async function test_flash_policy() {
   checkAllPermissionsForType("plugin:flash");
 });
 
 add_task(async function test_change_permission() {
   // Checks that changing a permission will still retain the
   // value set through the engine.
-  Services.perms.add(
-    URI("https://www.allow.com"),
+  PermissionTestUtils.add(
+    "https://www.allow.com",
     "cookie",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 
   checkPermission("allow.com", "ALLOW", "cookie");
 
   // Also change one un-managed permission to make sure it doesn't
   // cause any problems to the policy engine or the permission manager.
-  Services.perms.add(
-    URI("https://www.unmanaged.com"),
+  PermissionTestUtils.add(
+    "https://www.unmanaged.com",
     "cookie",
     Ci.nsIPermissionManager.DENY_ACTION,
     Ci.nsIPermissionManager.EXPIRE_SESSION
   );
 });
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -50,23 +50,27 @@ const SPOCS_FEEDS_UPDATE_TIME = 30 * 60 
 const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
 const MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const DEFAULT_MAX_HISTORY_QUERY_RESULTS = 1000;
 const FETCH_TIMEOUT = 45 * 1000;
 const PREF_CONFIG = "discoverystream.config";
 const PREF_ENDPOINTS = "discoverystream.endpoints";
 const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
+const PREF_ENABLED = "discoverystream.enabled";
+const PREF_HARDCODED_BASIC_LAYOUT = "discoverystream.hardcoded-basic-layout";
+const PREF_SPOCS_ENDPOINT = "discoverystream.spocs-endpoint";
 const PREF_TOPSTORIES = "feeds.section.topstories";
 const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear";
 const PREF_SHOW_SPONSORED = "showSponsored";
 const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
 const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
 
 let defaultLayoutResp;
+let basicLayoutResp;
 
 this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
   constructor() {
     // Internal state for checking if we've intialized all our data
     this.loaded = false;
 
     // Persistent cache for remote endpoint data.
     this.cache = new PersistentCache(CACHE_KEY, true);
@@ -142,16 +146,20 @@ this.DiscoveryStreamFeed = class Discove
     } catch (e) {
       // istanbul ignore next
       this._prefCache.config = {};
       // istanbul ignore next
       Cu.reportError(
         `Could not parse preference. Try resetting ${PREF_CONFIG} in about:config. ${e}`
       );
     }
+    this._prefCache.config.enabled =
+      this._prefCache.config.enabled &&
+      this.store.getState().Prefs.values[PREF_ENABLED];
+
     return this._prefCache.config;
   }
 
   resetConfigDefauts() {
     this.store.dispatch({
       type: at.CLEAR_PREF,
       data: {
         name: PREF_CONFIG,
@@ -325,17 +333,34 @@ this.DiscoveryStreamFeed = class Discove
 
   async loadLayout(sendUpdate, isStartup) {
     let layout = {};
     if (!this.config.hardcoded_layout) {
       layout = await this.fetchLayout(isStartup);
     }
 
     if (!layout || !layout.layout) {
-      layout = { lastUpdate: Date.now(), ...defaultLayoutResp };
+      if (
+        this.config.hardcoded_basic_layout ||
+        this.store.getState().Prefs.values[PREF_HARDCODED_BASIC_LAYOUT]
+      ) {
+        layout = { lastUpdate: Date.now(), ...basicLayoutResp };
+      } else {
+        layout = { lastUpdate: Date.now(), ...defaultLayoutResp };
+      }
+    }
+
+    if (
+      layout.spocs &&
+      (this.store.getState().Prefs.values[PREF_SPOCS_ENDPOINT] ||
+        this.config.spocs_endpoint)
+    ) {
+      layout.spocs.url =
+        this.store.getState().Prefs.values[PREF_SPOCS_ENDPOINT] ||
+        this.config.spocs_endpoint;
     }
 
     sendUpdate({
       type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
       data: layout,
     });
     if (
       layout.spocs &&
@@ -1233,16 +1258,19 @@ this.DiscoveryStreamFeed = class Discove
         break;
       case at.UNINIT:
         // When this feed is shutting down:
         this.uninitPrefs();
         break;
       case at.PREF_CHANGED:
         switch (action.data.name) {
           case PREF_CONFIG:
+          case PREF_ENABLED:
+          case PREF_HARDCODED_BASIC_LAYOUT:
+          case PREF_SPOCS_ENDPOINT:
             // Clear the cached config and broadcast the newly computed value
             this._prefCache.config = null;
             this.store.dispatch(
               ac.BroadcastToContent({
                 type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
                 data: this.config,
               })
             );
@@ -1382,9 +1410,101 @@ defaultLayoutResp = {
             ".ds-navigation": "margin-top: -10px;",
           },
         },
       ],
     },
   ],
 };
 
+// Hardcoded version of layout_variant `basic`
+basicLayoutResp = {
+  spocs: {
+    url: "https://spocs.getpocket.com/spocs",
+    spocs_per_domain: 1,
+  },
+  layout: [
+    {
+      width: 12,
+      components: [
+        {
+          type: "TopSites",
+          header: {
+            title: "Top Sites",
+          },
+          properties: {},
+        },
+        {
+          type: "Message",
+          header: {
+            title: "Recommended by Pocket",
+            subtitle: "",
+            link_text: "How it works",
+            link_url: "https://getpocket.com/firefox/new_tab_learn_more",
+            icon:
+              "resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
+          },
+          properties: {},
+          styles: {
+            ".ds-message": "margin-bottom: -20px",
+          },
+        },
+        {
+          type: "CardGrid",
+          properties: {
+            items: 3,
+          },
+          header: {
+            title: "",
+          },
+          feed: {
+            embed_reference: null,
+            url:
+              "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=default_spocs_on",
+          },
+          spocs: {
+            probability: 1,
+            positions: [
+              {
+                index: 2,
+              },
+            ],
+          },
+        },
+        {
+          type: "Navigation",
+          properties: {
+            alignment: "left-align",
+            links: [
+              {
+                name: "Must Reads",
+                url: "https://getpocket.com/explore/must-reads?src=fx_new_tab",
+              },
+              {
+                name: "Productivity",
+                url:
+                  "https://getpocket.com/explore/productivity?src=fx_new_tab",
+              },
+              {
+                name: "Health",
+                url: "https://getpocket.com/explore/health?src=fx_new_tab",
+              },
+              {
+                name: "Finance",
+                url: "https://getpocket.com/explore/finance?src=fx_new_tab",
+              },
+              {
+                name: "Technology",
+                url: "https://getpocket.com/explore/technology?src=fx_new_tab",
+              },
+              {
+                name: "More Recommendations ›",
+                url: "https://getpocket.com/explore/trending?src=fx_new_tab",
+              },
+            ],
+          },
+        },
+      ],
+    },
+  ],
+};
+
 const EXPORTED_SYMBOLS = ["DiscoveryStreamFeed"];
--- a/browser/components/newtab/lib/PrefsFeed.jsm
+++ b/browser/components/newtab/lib/PrefsFeed.jsm
@@ -79,16 +79,43 @@ this.PrefsFeed = class PrefsFeed {
     let handoffToAwesomebarPrefValue = Services.prefs.getBoolPref(
       "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar"
     );
     values["improvesearch.handoffToAwesomebar"] = handoffToAwesomebarPrefValue;
     this._prefMap.set("improvesearch.handoffToAwesomebar", {
       value: handoffToAwesomebarPrefValue,
     });
 
+    let discoveryStreamEnabled = Services.prefs.getBoolPref(
+      "browser.newtabpage.activity-stream.discoverystream.enabled",
+      false
+    );
+    let discoveryStreamHardcodedBasicLayout = Services.prefs.getBoolPref(
+      "browser.newtabpage.activity-stream.discoverystream.hardcoded-basic-layout",
+      false
+    );
+    let discoveryStreamSpocsEndpoint = Services.prefs.getStringPref(
+      "browser.newtabpage.activity-stream.discoverystream.spocs-endpoint",
+      ""
+    );
+    values["discoverystream.enabled"] = discoveryStreamEnabled;
+    this._prefMap.set("discoverystream.enabled", {
+      value: discoveryStreamEnabled,
+    });
+    values[
+      "discoverystream.hardcoded-basic-layout"
+    ] = discoveryStreamHardcodedBasicLayout;
+    this._prefMap.set("discoverystream.hardcoded-basic-layout", {
+      value: discoveryStreamHardcodedBasicLayout,
+    });
+    values["discoverystream.spocs-endpoint"] = discoveryStreamSpocsEndpoint;
+    this._prefMap.set("discoverystream.spocs-endpoint", {
+      value: discoveryStreamSpocsEndpoint,
+    });
+
     // Set the initial state of all prefs in redux
     this.store.dispatch(
       ac.BroadcastToContent({ type: at.PREFS_INITIAL_VALUES, data: values })
     );
   }
 
   removeListeners() {
     this._prefs.ignoreBranch(this);
--- a/browser/components/newtab/lib/TopStoriesFeed.jsm
+++ b/browser/components/newtab/lib/TopStoriesFeed.jsm
@@ -49,28 +49,34 @@ const STORIES_UPDATE_TIME = 30 * 60 * 10
 const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
 const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours
 const MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours
 const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
 const SECTION_ID = "topstories";
 const IMPRESSION_SOURCE = "TOP_STORIES";
 const SPOC_IMPRESSION_TRACKING_PREF =
   "feeds.section.topstories.spoc.impressions";
+const DISCOVERY_STREAM_PREF_ENABLED = "discoverystream.enabled";
+const DISCOVERY_STREAM_PREF_ENABLED_PATH =
+  "browser.newtabpage.activity-stream.discoverystream.enabled";
 const REC_IMPRESSION_TRACKING_PREF = "feeds.section.topstories.rec.impressions";
 const OPTIONS_PREF = "feeds.section.topstories.options";
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const DISCOVERY_STREAM_PREF = "discoverystream.config";
 
 this.TopStoriesFeed = class TopStoriesFeed {
   constructor(ds) {
     // Use discoverystream config pref default values for fast path and
     // if needed lazy load activity stream top stories feed based on
     // actual user preference when INIT and PREF_CHANGED is invoked
     this.discoveryStreamEnabled =
-      ds && ds.value && JSON.parse(ds.value).enabled;
+      ds &&
+      ds.value &&
+      JSON.parse(ds.value).enabled &&
+      Services.prefs.getBoolPref(DISCOVERY_STREAM_PREF_ENABLED_PATH, false);
     if (!this.discoveryStreamEnabled) {
       this.initializeProperties();
     }
   }
 
   initializeProperties() {
     this.contentUpdateQueue = [];
     this.spocCampaignMap = new Map();
@@ -779,17 +785,19 @@ this.TopStoriesFeed = class TopStoriesFe
 
   lazyLoadTopStories(dsPref) {
     let _dsPref = dsPref;
     if (!_dsPref) {
       _dsPref = this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF];
     }
 
     try {
-      this.discoveryStreamEnabled = JSON.parse(_dsPref).enabled;
+      this.discoveryStreamEnabled =
+        JSON.parse(_dsPref).enabled &&
+        this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF_ENABLED];
     } catch (e) {
       // Load activity stream top stories if fail to determine discovery stream state
       this.discoveryStreamEnabled = false;
     }
 
     // Return without invoking initialization if top stories are loaded
     if (this.storiesLoaded) {
       return;
@@ -805,16 +813,19 @@ this.TopStoriesFeed = class TopStoriesFe
     switch (action.type) {
       case at.INIT:
         this.lazyLoadTopStories();
         break;
       case at.PREF_CHANGED:
         if (action.data.name === DISCOVERY_STREAM_PREF) {
           this.lazyLoadTopStories(action.data.value);
         }
+        if (action.data.name === DISCOVERY_STREAM_PREF_ENABLED) {
+          this.lazyLoadTopStories();
+        }
         break;
       case at.UNINIT:
         this.uninit();
         break;
     }
   }
 
   async onAction(action) {
--- a/browser/components/newtab/test/browser/browser.ini
+++ b/browser/components/newtab/test/browser/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   blue_page.html
   red_page.html
   head.js
 prefs =
   browser.newtabpage.activity-stream.debug=false
+  browser.newtabpage.activity-stream.discoverystream.enabled=true
   browser.newtabpage.activity-stream.discoverystream.endpoints=data:
   browser.newtabpage.activity-stream.feeds.section.topstories=true
   browser.newtabpage.activity-stream.feeds.section.topstories.options={"provider_name":""}
 
 [browser_aboutwelcome.js]
 [browser_as_load_location.js]
 [browser_as_render.js]
 [browser_asrouter_snippets.js]
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -74,27 +74,33 @@ describe("DiscoveryStreamFeed", () => {
       "lib/UserDomainAffinityProvider.jsm": {
         UserDomainAffinityProvider: FakeUserDomainAffinityProvider,
       },
     }));
 
     globals = new GlobalOverrider();
     globals.set("gUUIDGenerator", { generateUUID: () => FAKE_UUID });
 
+    sandbox
+      .stub(global.Services.prefs, "getBoolPref")
+      .withArgs("browser.newtabpage.activity-stream.discoverystream.enabled")
+      .returns(true);
+
     // Feed
     feed = new DiscoveryStreamFeed();
     feed.store = createStore(combineReducers(reducers), {
       Prefs: {
         values: {
           [CONFIG_PREF_NAME]: JSON.stringify({
             enabled: false,
             show_spocs: false,
             layout_endpoint: DUMMY_ENDPOINT,
           }),
           [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
+          "discoverystream.enabled": true,
         },
       },
     });
     global.fetch.resetHistory();
 
     sandbox.stub(feed, "_maybeUpdateCachedData").resolves();
 
     globals.set("setTimeout", callback => {
@@ -289,16 +295,94 @@ describe("DiscoveryStreamFeed", () => {
       await feed.loadLayout(feed.store.dispatch);
 
       assert.notCalled(feed.fetchLayout);
       assert.equal(
         feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
         "https://spocs.getpocket.com/spocs"
       );
     });
+    it("should use local basic layout with hardcoded_layout and hardcoded_basic_layout being true", async () => {
+      feed.config.hardcoded_layout = true;
+      feed.config.hardcoded_basic_layout = true;
+      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));
+
+      await feed.loadLayout(feed.store.dispatch);
+
+      assert.notCalled(feed.fetchLayout);
+      assert.equal(
+        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
+        "https://spocs.getpocket.com/spocs"
+      );
+      const { layout } = feed.store.getState().DiscoveryStream;
+      assert.equal(layout[0].components[2].properties.items, 3);
+    });
+    it("should use new spocs endpoint if in the config", async () => {
+      feed.config.spocs_endpoint = "https://spocs.getpocket.com/spocs2";
+
+      await feed.loadLayout(feed.store.dispatch);
+
+      assert.equal(
+        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
+        "https://spocs.getpocket.com/spocs2"
+      );
+    });
+    it("should use local basic layout with hardcoded_layout and FF pref hardcoded_basic_layout", async () => {
+      feed.config.hardcoded_layout = true;
+      feed.store = createStore(combineReducers(reducers), {
+        Prefs: {
+          values: {
+            [CONFIG_PREF_NAME]: JSON.stringify({
+              enabled: false,
+              show_spocs: false,
+              layout_endpoint: DUMMY_ENDPOINT,
+            }),
+            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
+            "discoverystream.enabled": true,
+            "discoverystream.hardcoded-basic-layout": true,
+          },
+        },
+      });
+
+      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));
+
+      await feed.loadLayout(feed.store.dispatch);
+
+      assert.notCalled(feed.fetchLayout);
+      assert.equal(
+        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
+        "https://spocs.getpocket.com/spocs"
+      );
+      const { layout } = feed.store.getState().DiscoveryStream;
+      assert.equal(layout[0].components[2].properties.items, 3);
+    });
+    it("should use new spocs endpoint if in a FF pref", async () => {
+      feed.store = createStore(combineReducers(reducers), {
+        Prefs: {
+          values: {
+            [CONFIG_PREF_NAME]: JSON.stringify({
+              enabled: false,
+              show_spocs: false,
+              layout_endpoint: DUMMY_ENDPOINT,
+            }),
+            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
+            "discoverystream.enabled": true,
+            "discoverystream.spocs-endpoint":
+              "https://spocs.getpocket.com/spocs2",
+          },
+        },
+      });
+
+      await feed.loadLayout(feed.store.dispatch);
+
+      assert.equal(
+        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
+        "https://spocs.getpocket.com/spocs2"
+      );
+    });
     it("should fetch local layout for invalid layout endpoint or when fetch layout fails", async () => {
       feed.config.hardcoded_layout = false;
       fetchStub.resolves({ ok: false });
 
       await feed.loadLayout(feed.store.dispatch, true);
 
       assert.calledOnce(fetchStub);
       assert.equal(
--- a/browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js
@@ -166,19 +166,21 @@ describe("Top Stories Feed", () => {
     });
     it("should handle limited actions when discoverystream is enabled", async () => {
       sinon.spy(instance, "handleDisabled");
       sinon.stub(instance, "getPocketState");
       instance.store.getState = () => ({
         Prefs: {
           values: {
             "discoverystream.config": JSON.stringify({ enabled: true }),
+            "discoverystream.enabled": true,
           },
         },
       });
+
       instance.onAction({ type: at.INIT, data: {} });
 
       assert.calledOnce(instance.handleDisabled);
       instance.onAction({
         type: at.NEW_TAB_REHYDRATED,
         meta: { fromTarget: {} },
       });
       assert.notCalled(instance.getPocketState);
@@ -211,16 +213,24 @@ describe("Top Stories Feed", () => {
     it("should fire init on PREF_CHANGED", () => {
       sinon.stub(instance, "onInit");
       instance.onAction({
         type: at.PREF_CHANGED,
         data: { name: "discoverystream.config", value: {} },
       });
       assert.calledOnce(instance.onInit);
     });
+    it("should fire init on DISCOVERY_STREAM_PREF_ENABLED", () => {
+      sinon.stub(instance, "onInit");
+      instance.onAction({
+        type: at.PREF_CHANGED,
+        data: { name: "discoverystream.enabled", value: true },
+      });
+      assert.calledOnce(instance.onInit);
+    });
     it("should not fire init on PREF_CHANGED if stories are loaded", () => {
       sinon.stub(instance, "onInit");
       sinon.spy(instance, "lazyLoadTopStories");
       instance.storiesLoaded = true;
       instance.onAction({
         type: at.PREF_CHANGED,
         data: { name: "discoverystream.config", value: {} },
       });
--- a/browser/components/originattributes/test/browser/browser_permissions.js
+++ b/browser/components/originattributes/test/browser/browser_permissions.js
@@ -1,20 +1,24 @@
 /**
  * Bug 1282655 - Test if site permissions are universal across origin attributes.
  *
  * This test is testing the cookie "permission" for a specific URI.
  */
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 const TEST_PAGE = "http://example.net";
 const uri = Services.io.newURI(TEST_PAGE);
 
 function disableCookies() {
   Services.cookies.removeAll();
-  Services.perms.add(uri, "cookie", Services.perms.DENY_ACTION);
+  PermissionTestUtils.add(uri, "cookie", Services.perms.DENY_ACTION);
 }
 
 function ensureCookieNotSet(aBrowser) {
   ContentTask.spawn(aBrowser, null, async function() {
     content.document.cookie = "key=value";
     is(
       content.document.cookie,
       "",
@@ -28,17 +32,17 @@ IsolationTestTools.runTests(
   TEST_PAGE,
   ensureCookieNotSet,
   () => true,
   disableCookies
 );
 
 function enableCookies() {
   Services.cookies.removeAll();
-  Services.perms.add(uri, "cookie", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(uri, "cookie", Services.perms.ALLOW_ACTION);
 }
 
 function ensureCookieSet(aBrowser) {
   ContentTask.spawn(aBrowser, null, function() {
     content.document.cookie = "key=value";
     is(
       content.document.cookie,
       "key=value",
--- a/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js
+++ b/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js
@@ -133,25 +133,29 @@ add_task(async function testRemove() {
     }
   );
 });
 
 add_task(async function testAdd() {
   await runTest(
     async (params, observeAllPromise, apply) => {
       let uri = Services.io.newURI("http://test.com");
-      Services.perms.add(uri, "popup", Ci.nsIPermissionManager.DENY_ACTION);
+      PermissionTestUtils.add(
+        uri,
+        "popup",
+        Ci.nsIPermissionManager.DENY_ACTION
+      );
 
       info("Adding unrelated permission should not change display.");
       assertListContents(params, []);
 
       apply();
       await observeAllPromise;
 
-      Services.perms.remove(uri, "popup");
+      PermissionTestUtils.remove(uri, "popup");
     },
     params => {
       return [
         {
           type: "popup",
           origin: "http://test.com",
           data: "added",
           capability: Ci.nsIPermissionManager.DENY_ACTION,
@@ -407,17 +411,21 @@ add_task(async function testSort() {
       EventUtils.synthesizeMouseAtCenter(
         params.doc.getElementById("siteCol"),
         {},
         params.doc.defaultView
       );
 
       for (let URL of ["http://a", "http://z", "http://b"]) {
         let URI = Services.io.newURI(URL);
-        Services.perms.add(URI, "cookie", Ci.nsIPermissionManager.ALLOW_ACTION);
+        PermissionTestUtils.add(
+          URI,
+          "cookie",
+          Ci.nsIPermissionManager.ALLOW_ACTION
+        );
       }
 
       assertListContents(params, [
         ["http://a", params.allowL10nId],
         ["http://b", params.allowL10nId],
         ["http://z", params.allowL10nId],
       ]);
 
@@ -434,17 +442,17 @@ add_task(async function testSort() {
         ["http://a", params.allowL10nId],
       ]);
 
       apply();
       await observeAllPromise;
 
       for (let URL of ["http://a", "http://z", "http://b"]) {
         let uri = Services.io.newURI(URL);
-        Services.perms.remove(uri, "cookie");
+        PermissionTestUtils.remove(uri, "cookie");
       }
     },
     params => {
       return [
         {
           type: "cookie",
           origin: "http://a",
           data: "added",
--- a/browser/components/preferences/in-content/tests/browser_permissions_dialog.js
+++ b/browser/components/preferences/in-content/tests/browser_permissions_dialog.js
@@ -2,16 +2,17 @@
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var { SitePermissions } = ChromeUtils.import(
   "resource:///modules/SitePermissions.jsm"
 );
+
 var { Services } = ChromeUtils.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;
 
@@ -50,240 +51,305 @@ add_task(async function addPermission() 
   // First item in the richlistbox contains column headers.
   Assert.equal(
     richlistbox.itemCount,
     0,
     "Number of permission items is 0 initially"
   );
 
   // Add notification permission for a website.
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Observe the added permission changes in the dialog UI.
   Assert.equal(richlistbox.itemCount, 1);
   checkPermissionItem(URL, Services.perms.ALLOW_ACTION);
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
 });
 
 add_task(async function observePermissionChange() {
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Change the permission.
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.BLOCK);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.DENY_ACTION
+  );
 
   checkPermissionItem(URL, Services.perms.DENY_ACTION);
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
 });
 
 add_task(async function observePermissionDelete() {
   let doc = sitePermissionsDialog.document;
   let richlistbox = doc.getElementById("permissionsBox");
 
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   Assert.equal(
     richlistbox.itemCount,
     1,
     "The box contains one permission item initially"
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
 
   Assert.equal(richlistbox.itemCount, 0);
 });
 
 add_task(async function onPermissionChange() {
   let doc = sitePermissionsDialog.document;
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Change the permission state in the UI.
   doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
 
   Assert.equal(
-    SitePermissions.get(URI, "desktop-notification").state,
-    SitePermissions.ALLOW,
+    PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION,
     "Permission state does not change before saving changes"
   );
 
   doc.getElementById("btnApplyChanges").click();
 
   await TestUtils.waitForCondition(
     () =>
-      SitePermissions.get(URI, "desktop-notification").state ==
-      SitePermissions.BLOCK
+      PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+        .capability == Services.perms.DENY_ACTION
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.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);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   richlistbox.selectItem(richlistbox.getItemAtIndex(0));
   doc.getElementById("removePermission").click();
 
   await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
 
   Assert.equal(
-    SitePermissions.get(URI, "desktop-notification").state,
-    SitePermissions.ALLOW,
+    PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION,
     "Permission is not deleted before saving changes"
   );
 
   doc.getElementById("btnApplyChanges").click();
 
   await TestUtils.waitForCondition(
     () =>
-      SitePermissions.get(URI, "desktop-notification").state ==
-      SitePermissions.UNKNOWN
+      PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+      null
   );
 });
 
 add_task(async function onAllPermissionsDelete() {
   await openPermissionsDialog();
 
   let doc = sitePermissionsDialog.document;
   let richlistbox = doc.getElementById("permissionsBox");
 
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
   let u = Services.io.newURI("http://www.test.com");
-  SitePermissions.set(u, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    u,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   doc.getElementById("removeAllPermissions").click();
   await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
 
   Assert.equal(
-    SitePermissions.get(URI, "desktop-notification").state,
-    SitePermissions.ALLOW
+    PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION
   );
   Assert.equal(
-    SitePermissions.get(u, "desktop-notification").state,
-    SitePermissions.ALLOW,
+    PermissionTestUtils.getPermissionObject(u, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION,
     "Permissions are not deleted before saving changes"
   );
 
   doc.getElementById("btnApplyChanges").click();
 
   await TestUtils.waitForCondition(
     () =>
-      SitePermissions.get(URI, "desktop-notification").state ==
-        SitePermissions.UNKNOWN &&
-      SitePermissions.get(u, "desktop-notification").state ==
-        SitePermissions.UNKNOWN
+      PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+        null &&
+      PermissionTestUtils.getPermissionObject(u, "desktop-notification") == null
   );
 });
 
 add_task(async function onPermissionChangeAndDelete() {
   await openPermissionsDialog();
 
   let doc = sitePermissionsDialog.document;
   let richlistbox = doc.getElementById("permissionsBox");
 
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // 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(0));
   doc.getElementById("removePermission").click();
 
   await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
 
   doc.getElementById("btnApplyChanges").click();
 
   await TestUtils.waitForCondition(
     () =>
-      SitePermissions.get(URI, "desktop-notification").state ==
-      SitePermissions.UNKNOWN
+      PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+      null
   );
 });
 
 add_task(async function onPermissionChangeCancel() {
   await openPermissionsDialog();
 
   let doc = sitePermissionsDialog.document;
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // 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,
+    PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION,
     "Permission state does not change on clicking cancel"
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.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);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Remove that permission by clicking the "Remove" button.
   richlistbox.selectItem(richlistbox.getItemAtIndex(0));
   doc.getElementById("removePermission").click();
 
   await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
 
   doc.getElementById("cancel").click();
 
   Assert.equal(
-    SitePermissions.get(URI, "desktop-notification").state,
-    SitePermissions.ALLOW,
+    PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+      .capability,
+    Services.perms.ALLOW_ACTION,
     "Permission state does not change on clicking cancel"
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
 });
 
 add_task(async function onSearch() {
   await openPermissionsDialog();
   let doc = sitePermissionsDialog.document;
   let richlistbox = doc.getElementById("permissionsBox");
   let searchBox = doc.getElementById("searchBox");
 
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
   searchBox.value = "www.example.com";
 
   let u = Services.io.newURI("http://www.test.com");
-  SitePermissions.set(u, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    u,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   Assert.equal(
     doc.getElementsByAttribute("origin", "http://www.test.com")[0],
     null
   );
   Assert.equal(
     doc.getElementsByAttribute("origin", "http://www.example.com")[0],
     richlistbox.getItemAtIndex(0)
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
-  SitePermissions.remove(u, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(u, "desktop-notification");
 
   doc.getElementById("cancel").click();
 });
 
 add_task(async function onPermissionsSort() {
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
   let u = Services.io.newURI("http://www.test.com");
-  SitePermissions.set(u, "desktop-notification", SitePermissions.BLOCK);
+  PermissionTestUtils.add(
+    u,
+    "desktop-notification",
+    Services.perms.DENY_ACTION
+  );
 
   await openPermissionsDialog();
   let doc = sitePermissionsDialog.document;
   let richlistbox = doc.getElementById("permissionsBox");
 
   // Test default arrangement(Allow followed by Block).
   Assert.equal(
     richlistbox.getItemAtIndex(0).getAttribute("origin"),
@@ -325,18 +391,18 @@ add_task(async function onPermissionsSor
     richlistbox.getItemAtIndex(0).getAttribute("origin"),
     "http://www.test.com"
   );
   Assert.equal(
     richlistbox.getItemAtIndex(1).getAttribute("origin"),
     "http://www.example.com"
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
-  SitePermissions.remove(u, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(u, "desktop-notification");
 
   doc.getElementById("cancel").click();
 });
 
 add_task(async function onPermissionDisable() {
   // Enable desktop-notification permission prompts.
   Services.prefs.setIntPref(
     "permissions.default.desktop-notification",
@@ -423,19 +489,27 @@ add_task(async function checkDefaultPerm
 
 add_task(async function testTabBehaviour() {
   // Test tab behaviour inside the permissions setting dialog when site permissions are selected.
   // Only selected items in the richlistbox should be tabable for accessibility reasons.
 
   // Force tabfocus for all elements on OSX.
   SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
 
-  SitePermissions.set(URI, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    URI,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
   let u = Services.io.newURI("http://www.test.com");
-  SitePermissions.set(u, "desktop-notification", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    u,
+    "desktop-notification",
+    Services.perms.ALLOW_ACTION
+  );
 
   await openPermissionsDialog();
   let doc = sitePermissionsDialog.document;
 
   EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog);
   let richlistbox = doc.getElementById("permissionsBox");
   is(
     richlistbox,
@@ -457,17 +531,17 @@ add_task(async function testTabBehaviour
   EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog);
   let removeButton = doc.getElementById("removePermission");
   is(
     removeButton,
     doc.activeElement,
     "The focus moves outside the richlistbox and onto the remove button"
   );
 
-  SitePermissions.remove(URI, "desktop-notification");
-  SitePermissions.remove(u, "desktop-notification");
+  PermissionTestUtils.remove(URI, "desktop-notification");
+  PermissionTestUtils.remove(u, "desktop-notification");
 
   doc.getElementById("cancel").click();
 });
 
 add_task(async function removeTab() {
   gBrowser.removeCurrentTab();
 });
--- a/browser/components/preferences/in-content/tests/head.js
+++ b/browser/components/preferences/in-content/tests/head.js
@@ -1,12 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.import("resource://gre/modules/Promise.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 const kDefaultWait = 2000;
 
 function is_element_visible(aElement, aMsg) {
   isnot(aElement, null, "Element should not be null, when checking visibility");
   ok(!BrowserTestUtils.is_hidden(aElement), aMsg);
 }
 
--- a/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+++ b/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
@@ -1,20 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var { SitePermissions } = ChromeUtils.import(
   "resource:///modules/SitePermissions.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 async function testClearData(clearSiteData, clearCache) {
-  let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
-  SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+  PermissionTestUtils.add(
+    TEST_QUOTA_USAGE_ORIGIN,
+    "persistent-storage",
+    Services.perms.ALLOW_ACTION
+  );
 
   // Open a test site which saves into appcache.
   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   // Fill indexedDB with test data.
   // Don't wait for the page to load, to register the content event handler as quickly as possible.
   // If this test goes intermittent, we might have to tell the page to wait longer before
@@ -173,23 +179,23 @@ async function testClearData(clearSiteDa
         await ContentTaskUtils.waitForCondition(
           () => sizeLabel.textContent != opts.initialSizeLabelValue,
           "Site data size label should have updated."
         );
       }
     );
   }
 
-  let desiredPermissionState = clearSiteData
-    ? SitePermissions.UNKNOWN
-    : SitePermissions.ALLOW;
-  let permission = SitePermissions.get(quotaURI, "persistent-storage");
+  let permission = PermissionTestUtils.getPermissionObject(
+    TEST_QUOTA_USAGE_ORIGIN,
+    "persistent-storage"
+  );
   is(
-    permission.state,
-    desiredPermissionState,
+    clearSiteData ? permission : permission.capability,
+    clearSiteData ? null : Services.perms.ALLOW_ACTION,
     "Should have the correct permission state."
   );
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await SiteDataManager.removeAll();
 }
 
 // Test opening the "Clear All Data" dialog and cancelling.
--- a/browser/components/protections/content/monitor-card.js
+++ b/browser/components/protections/content/monitor-card.js
@@ -1,30 +1,34 @@
 /* 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/. */
 
 /* eslint-env mozilla/frame-script */
 
+const MONITOR_URL = RPMGetStringPref(
+  "browser.contentblocking.report.monitor.url",
+  ""
+);
 const MONITOR_SIGN_IN_URL = RPMGetStringPref(
-  "browser.contentblocking.report.monitor.url",
+  "browser.contentblocking.report.monitor.sign_in_url",
   ""
 );
 const HOW_IT_WORKS_URL_PREF = RPMGetFormatURLPref(
   "browser.contentblocking.report.monitor.how_it_works.url"
 );
 
 export default class MonitorClass {
   constructor(document) {
     this.doc = document;
   }
 
   init() {
     const monitorLinkTag = this.doc.getElementById("monitor-inline-link");
-    monitorLinkTag.href = MONITOR_SIGN_IN_URL;
+    monitorLinkTag.href = MONITOR_URL;
 
     RPMAddMessageListener("SendUserLoginsData", ({ data }) => {
       // Wait for monitor data and display the card.
       this.getMonitorData(data);
       RPMSendAsyncMessage("FetchMonitorData");
     });
 
     let monitorReportLink = this.doc.getElementById("full-report-link");
@@ -94,26 +98,17 @@ export default class MonitorClass {
    *
    * @param {String|null} email
    *        Optional. The email used to direct the user to the Monitor website's OAuth
    *        sign-in flow. If null, then direct user to just the Monitor website.
    *
    * @return URL to Monitor website.
    */
   buildMonitorUrl(email = null) {
-    let url = MONITOR_SIGN_IN_URL;
-
-    if (email) {
-      url += `/oauth/init?email=${email}&entrypoint=protection_report_monitor&utm_source=about-protections`;
-    } else {
-      url +=
-        "/?entrypoint=protection_report_monitor&utm_source=about-protections";
-    }
-
-    return url;
+    return email ? `${MONITOR_SIGN_IN_URL}${email}` : MONITOR_URL;
   }
 
   renderContentForUserWithLogins(monitorData) {
     const monitorCardBody = this.doc.querySelector(
       ".card.monitor-card .card-body"
     );
     monitorCardBody.classList.remove("hidden");
 
--- a/browser/components/protections/test/browser/browser_protections_telemetry.js
+++ b/browser/components/protections/test/browser/browser_protections_telemetry.js
@@ -9,16 +9,17 @@ add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["browser.contentblocking.database.enabled", true],
       ["browser.contentblocking.report.monitor.enabled", true],
       ["browser.contentblocking.report.lockwise.enabled", true],
       ["browser.contentblocking.report.proxy.enabled", true],
       // Change the endpoints to prevent non-local network connections when landing on the page.
       ["browser.contentblocking.report.monitor.url", ""],
+      ["browser.contentblocking.report.monitor.sign_in_url", ""],
       ["browser.contentblocking.report.lockwise.url", ""],
     ],
   });
 
   let oldCanRecord = Services.telemetry.canRecordExtended;
   Services.telemetry.canRecordExtended = true;
   registerCleanupFunction(() => {
     Services.telemetry.canRecordExtended = oldCanRecord;
--- a/browser/components/translation/test/browser_translation_exceptions.js
+++ b/browser/components/translation/test/browser_translation_exceptions.js
@@ -1,16 +1,19 @@
 /* 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/. */
 
 // tests the translation infobar, using a fake 'Translation' implementation.
 
 var tmp = {};
 ChromeUtils.import("resource:///modules/translation/Translation.jsm", tmp);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 var { Translation } = tmp;
 
 const kLanguagesPref = "browser.translation.neverForLanguages";
 const kShowUIPref = "browser.translation.ui.show";
 
 function test() {
   waitForExplicitFinish();
 
@@ -235,17 +238,17 @@ var gTests = [
       // Open the "options" drop down.
       await openPopup(notif._getAnonElt("options"));
       ok(
         notif._getAnonElt("neverForSite").disabled,
         "The 'Never translate French' item is disabled"
       );
 
       // Cleanup.
-      Services.perms.remove(makeURI("http://example.com"), "translate");
+      PermissionTestUtils.remove("http://example.com", "translate");
       notif.close();
     },
   },
 
   {
     desc: "language exception list",
     run: async function checkLanguageExceptions() {
       // Put 2 languages in the pref before opening the window to check
@@ -302,19 +305,26 @@ var gTests = [
     },
   },
 
   {
     desc: "domains exception list",
     run: async function checkDomainExceptions() {
       // Put 2 exceptions before opening the window to check the list is
       // displayed on load.
-      let perms = Services.perms;
-      perms.add(makeURI("http://example.org"), "translate", perms.DENY_ACTION);
-      perms.add(makeURI("http://example.com"), "translate", perms.DENY_ACTION);
+      PermissionTestUtils.add(
+        "http://example.org",
+        "translate",
+        Services.perms.DENY_ACTION
+      );
+      PermissionTestUtils.add(
+        "http://example.com",
+        "translate",
+        Services.perms.DENY_ACTION
+      );
 
       // Open the translation exceptions dialog.
       let win = openDialog(
         "chrome://browser/content/preferences/translation.xul",
         "Browser:TranslationExceptions",
         "",
         null
       );
@@ -334,24 +344,28 @@ var gTests = [
       ok(!remove.disabled, "The 'Remove Site' button is enabled");
 
       // Click the 'Remove' button.
       remove.click();
       is(tree.view.rowCount, 1, "The site exceptions now contains 1 item");
       is(getDomainExceptions().length, 1, "One exception in the permissions");
 
       // Clear the permissions, and check the last item is removed from the display.
-      perms.remove(makeURI("http://example.org"), "translate");
-      perms.remove(makeURI("http://example.com"), "translate");
+      PermissionTestUtils.remove("http://example.org", "translate");
+      PermissionTestUtils.remove("http://example.com", "translate");
       is(tree.view.rowCount, 0, "The site exceptions list is empty");
       ok(remove.disabled, "The 'Remove Site' button is disabled");
       ok(removeAll.disabled, "The 'Remove All Site' button is disabled");
 
       // Add an item and check it appears.
-      perms.add(makeURI("http://example.com"), "translate", perms.DENY_ACTION);
+      PermissionTestUtils.add(
+        "http://example.com",
+        "translate",
+        Services.perms.DENY_ACTION
+      );
       is(tree.view.rowCount, 1, "The site exceptions list has 1 item");
       ok(remove.disabled, "The 'Remove Site' button is disabled");
       ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled");
 
       // Click the 'Remove All' button.
       removeAll.click();
       is(tree.view.rowCount, 0, "The site exceptions list is empty");
       ok(remove.disabled, "The 'Remove Site' button is disabled");
--- a/browser/components/uitour/test/browser_no_tabs.js
+++ b/browser/components/uitour/test/browser_no_tabs.js
@@ -55,18 +55,17 @@ function destroyHiddenBrowser(aFrame, aB
  * browsers).
  */
 add_task(async function test_windowless_UITour() {
   // Get the URL for the test page.
   let pageURL = getRootDirectory(gTestPath) + "uitour.html";
 
   // Allow the URL to use the UITour.
   info("Adding UITour permission to the test page.");
-  let pageURI = Services.io.newURI(pageURL);
-  Services.perms.add(pageURI, "uitour", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(gTestPath, "uitour", Services.perms.ALLOW_ACTION);
 
   // UITour's ping will resolve this promise.
   await new Promise(resolve => {
     // Create a windowless browser and test that UITour works in it.
     let browserPromise = createHiddenBrowser(pageURL);
     browserPromise.then(frameInfo => {
       isnot(frameInfo.browser, null, "The browser must exist and not be null.");
 
--- a/browser/components/uitour/test/head.js
+++ b/browser/components/uitour/test/head.js
@@ -4,16 +4,20 @@
 /* global gTestTab:true, gContentAPI:true, gContentWindow:true, tests:false */
 
 ChromeUtils.defineModuleGetter(
   this,
   "UITour",
   "resource:///modules/UITour.jsm"
 );
 
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
+
 const SINGLE_TRY_TIMEOUT = 100;
 const NUMBER_OF_TRIES = 30;
 
 function waitForConditionPromise(
   condition,
   timeoutMsg,
   tryCount = NUMBER_OF_TRIES
 ) {
@@ -441,37 +445,45 @@ function loadUITourTestPage(callback, ho
 // Wrapper for UITourTest to be used by add_task tests.
 function setup_UITourTest() {
   return UITourTest(true);
 }
 
 // Use `add_task(setup_UITourTest);` instead as we will fold this into `setup_UITourTest` once all tests are using `add_UITour_task`.
 function UITourTest(usingAddTask = false) {
   Services.prefs.setBoolPref("browser.uitour.enabled", true);
-  let testHttpsUri = Services.io.newURI("https://example.org");
-  let testHttpUri = Services.io.newURI("http://example.org");
-  Services.perms.add(testHttpsUri, "uitour", Services.perms.ALLOW_ACTION);
-  Services.perms.add(testHttpUri, "uitour", Services.perms.ALLOW_ACTION);
+  let testHttpsOrigin = "https://example.org";
+  let testHttpOrigin = "http://example.org";
+  PermissionTestUtils.add(
+    testHttpsOrigin,
+    "uitour",
+    Services.perms.ALLOW_ACTION
+  );
+  PermissionTestUtils.add(
+    testHttpOrigin,
+    "uitour",
+    Services.perms.ALLOW_ACTION
+  );
 
   // If a test file is using add_task, we don't need to have a test function or
   // call `waitForExplicitFinish`.
   if (!usingAddTask) {
     waitForExplicitFinish();
   }
 
   registerCleanupFunction(function() {
     delete window.gContentWindow;
     delete window.gContentAPI;
     if (gTestTab) {
       gBrowser.removeTab(gTestTab);
     }
     delete window.gTestTab;
     Services.prefs.clearUserPref("browser.uitour.enabled");
-    Services.perms.remove(testHttpsUri, "uitour");
-    Services.perms.remove(testHttpUri, "uitour");
+    PermissionTestUtils.remove(testHttpsOrigin, "uitour");
+    PermissionTestUtils.remove(testHttpOrigin, "uitour");
   });
 
   // When using tasks, the harness will call the next added task for us.
   if (!usingAddTask) {
     nextTest();
   }
 }
 
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -837,16 +837,26 @@ class UrlbarInput {
   set value(val) {
     return this._setValue(val, true);
   }
 
   get openViewOnFocus() {
     return this._openViewOnFocus;
   }
 
+  get openViewOnFocusForCurrentTab() {
+    return (
+      this.openViewOnFocus &&
+      !["about:newtab", "about:home"].includes(
+        this.window.gBrowser.currentURI.spec
+      ) &&
+      !this.isPrivate
+    );
+  }
+
   // Private methods below.
 
   _setOpenViewOnFocus() {
     // FIXME: Not using UrlbarPrefs because its pref observer may run after
     // this call, so we'd get the previous openViewOnFocus value here. This
     // can be cleaned up after bug 1560013.
     this._openViewOnFocus = Services.prefs.getBoolPref(
       "browser.urlbar.openViewOnFocus"
@@ -1519,17 +1529,17 @@ class UrlbarInput {
       // The rest of this handler only cares about left clicks.
       if (event.button != 0) {
         return;
       }
 
       if (event.detail == 2 && UrlbarPrefs.get("doubleClickSelectsAll")) {
         this.editor.selectAll();
         event.preventDefault();
-      } else if (this.openViewOnFocus && !this.view.isOpen) {
+      } else if (this.openViewOnFocusForCurrentTab && !this.view.isOpen) {
         this.controller.engagementEvent.start(event);
         this.startQuery({
           allowAutofill: false,
         });
       }
       return;
     }
 
--- a/browser/components/urlbar/tests/browser/browser_openViewOnFocus.js
+++ b/browser/components/urlbar/tests/browser/browser_openViewOnFocus.js
@@ -1,41 +1,129 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function test() {
+async function checkOpensOnFocus(win = window) {
+  Assert.ok(
+    win.gURLBar.openViewOnFocusForCurrentTab,
+    "openViewOnFocusForCurrentTab should be true"
+  );
+  // Even with openViewOnFocus = true, the view should not open when the input
+  // is focused programmatically.
+  win.gURLBar.blur();
+  win.gURLBar.focus();
+  Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+  Assert.ok(win.gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
+  win.gURLBar.blur();
+  Assert.ok(win.gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
+  // Check the keyboard shortcut.
+  await UrlbarTestUtils.promisePopupOpen(win, () => {
+    win.document.getElementById("Browser:OpenLocation").doCommand();
+  });
+  win.gURLBar.blur();
+  // Focus with the mouse.
+  await UrlbarTestUtils.promisePopupOpen(win, () => {
+    EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {});
+  });
+  win.gURLBar.blur();
+}
+
+function checkDoesNotOpenOnFocus(win = window) {
+  Assert.ok(
+    !win.gURLBar.openViewOnFocusForCurrentTab,
+    "openViewOnFocusForCurrentTab should be false"
+  );
+  // The view should not open when the input is focused programmatically.
+  win.gURLBar.blur();
+  win.gURLBar.focus();
+  Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+  Assert.ok(win.gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
+  win.gURLBar.blur();
+  Assert.ok(win.gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
+  // Check the keyboard shortcut.
+  win.document.getElementById("Browser:OpenLocation").doCommand();
+  Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+  win.gURLBar.blur();
+  // Focus with the mouse.
+  EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {});
+  Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+  win.gURLBar.blur();
+}
+
+add_task(async function setUp() {
   await SpecialPowers.pushPrefEnv({
     set: [["browser.urlbar.openViewOnFocus", true]],
   });
-
   // Add some history for the empty panel.
   await PlacesTestUtils.addVisits([
     {
       uri: "http://mochi.test:8888/",
       transition: PlacesUtils.history.TRANSITIONS.TYPED,
     },
   ]);
   registerCleanupFunction(() => PlacesUtils.history.clear());
+});
 
+add_task(async function test() {
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:blank" },
     async browser => {
-      gURLBar.blur();
-      gURLBar.focus();
-      Assert.ok(!gURLBar.view.isOpen, "check urlbar panel is not open");
-      Assert.ok(gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
-      gURLBar.blur();
-      Assert.ok(gURLBar.dropmarker.hidden, "The dropmarker should be hidden");
-      // Check the keyboard shortcut.
-      await UrlbarTestUtils.promisePopupOpen(window, () => {
-        window.document.getElementById("Browser:OpenLocation").doCommand();
-      });
-      gURLBar.blur();
-      // Focus with the mouse.
-      await UrlbarTestUtils.promisePopupOpen(window, () => {
-        EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
-      });
-      gURLBar.blur();
+      await checkOpensOnFocus();
     }
   );
 });
+
+add_task(async function newtabAndHome() {
+  for (let url of ["about:newtab", "about:home"]) {
+    // withNewTab randomly hangs on these pages when waitForLoad = true (the
+    // default), so pass false.
+    await BrowserTestUtils.withNewTab(
+      { gBrowser, url, waitForLoad: false },
+      async browser => {
+        // openViewOnFocus should be disabled for these pages even though the
+        // pref is true.
+        checkDoesNotOpenOnFocus();
+        // Open a new tab where openViewOnFocus should be enabled.
+        await BrowserTestUtils.withNewTab(
+          { gBrowser, url: "http://example.com/" },
+          async otherBrowser => {
+            // openViewOnFocus should be enabled.
+            await checkOpensOnFocus();
+            // Switch back to about:newtab/home.  openViewOnFocus should be
+            // disabled.
+            await BrowserTestUtils.switchTab(
+              gBrowser,
+              gBrowser.getTabForBrowser(browser)
+            );
+            checkDoesNotOpenOnFocus();
+            // Switch back to example.com.  openViewOnFocus should be enabled.
+            await BrowserTestUtils.switchTab(
+              gBrowser,
+              gBrowser.getTabForBrowser(otherBrowser)
+            );
+            await checkOpensOnFocus();
+          }
+        );
+        // After example.com closes, about:newtab/home should be selected again,
+        // and openViewOnFocus should be disabled.
+        checkDoesNotOpenOnFocus();
+        // Load example.com in the same tab.  openViewOnFocus should become
+        // enabled.
+        await BrowserTestUtils.loadURI(
+          gBrowser.selectedBrowser,
+          "http://example.com/"
+        );
+        await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        await checkOpensOnFocus();
+      }
+    );
+  }
+});
+
+add_task(async function privateWindow() {
+  let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+    private: true,
+  });
+  checkDoesNotOpenOnFocus(privateWin);
+  await BrowserTestUtils.closeWindow(privateWin);
+});
--- a/browser/locales/en-US/browser/aboutLogins.ftl
+++ b/browser/locales/en-US/browser/aboutLogins.ftl
@@ -125,8 +125,10 @@ confirm-delete-dialog-confirm-button = D
 confirm-discard-changes-dialog-title = Discard unsaved changes?
 confirm-discard-changes-dialog-message = All unsaved changes will be lost.
 confirm-discard-changes-dialog-confirm-button = Discard
 
 ## Breach Alert notification
 
 breach-alert-text = Passwords were leaked or stolen from this website since you last updated your login details. Change your password to protect your account.
 breach-alert-link = Learn more about this breach.
+breach-alert-dismiss = 
+    .title = Close this alert
--- a/browser/modules/test/browser/browser_PermissionUI.js
+++ b/browser/modules/test/browser/browser_PermissionUI.js
@@ -4,16 +4,19 @@
  * add-ons can introduce their own permission prompts.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Integration.jsm", this);
 ChromeUtils.import("resource:///modules/PermissionUI.jsm", this);
 ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 /**
  * Tests the PermissionPromptForRequest prototype to ensure that a prompt
  * can be displayed. Does not test permission handling.
  */
 add_task(async function test_permission_prompt_for_request() {
   await BrowserTestUtils.withNewTab(
     {
@@ -192,17 +195,17 @@ add_task(async function test_with_permis
         callback() {
           denied = true;
         },
       };
 
       let mockRequest = makeMockPermissionRequest(browser);
       let principal = mockRequest.principal;
       registerCleanupFunction(function() {
-        SitePermissions.remove(principal.URI, kTestPermissionKey);
+        PermissionTestUtils.remove(principal.URI, kTestPermissionKey);
       });
 
       let TestPrompt = {
         __proto__: PermissionUI.PermissionPromptForRequestPrototype,
         request: mockRequest,
         notificationID: kTestNotificationID,
         permissionKey: kTestPermissionKey,
         message: kTestMessage,
@@ -223,18 +226,18 @@ add_task(async function test_with_permis
       TestPrompt.prompt();
       await shownPromise;
       let notification = PopupNotifications.getNotification(
         kTestNotificationID,
         browser
       );
       Assert.ok(notification, "Should have gotten the notification");
 
-      let curPerm = SitePermissions.get(
-        principal.URI,
+      let curPerm = SitePermissions.getForPrincipal(
+        principal,
         kTestPermissionKey,
         browser
       );
       Assert.equal(
         curPerm.state,
         SitePermissions.UNKNOWN,
         "Should be no permission set to begin with."
       );
@@ -244,48 +247,56 @@ add_task(async function test_with_permis
       popupNotification.checkbox.checked = false;
 
       Assert.equal(
         notification.secondaryActions.length,
         1,
         "There should only be 1 secondary action"
       );
       await clickSecondaryAction();
-      curPerm = SitePermissions.get(principal.URI, kTestPermissionKey, browser);
+      curPerm = SitePermissions.getForPrincipal(
+        principal,
+        kTestPermissionKey,
+        browser
+      );
       Assert.deepEqual(
         curPerm,
         {
           state: SitePermissions.BLOCK,
           scope: SitePermissions.SCOPE_TEMPORARY,
         },
         "Should have denied the action temporarily"
       );
       // Try getting the permission without passing the browser object (should fail).
-      curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
-      Assert.deepEqual(
+      curPerm = PermissionTestUtils.getPermissionObject(
+        principal.URI,
+        kTestPermissionKey
+      );
+      Assert.equal(
         curPerm,
-        {
-          state: SitePermissions.UNKNOWN,
-          scope: SitePermissions.SCOPE_PERSISTENT,
-        },
+        null,
         "Should have made no permanent permission entry"
       );
       Assert.ok(denied, "The secondaryAction callback should have fired");
       Assert.ok(!allowed, "The mainAction callback should not have fired");
       Assert.ok(
         mockRequest._cancelled,
         "The request should have been cancelled"
       );
       Assert.ok(
         !mockRequest._allowed,
         "The request should not have been allowed"
       );
 
       // Clear the permission and pretend we never denied
-      SitePermissions.remove(principal.URI, kTestPermissionKey, browser);
+      SitePermissions.removeFromPrincipal(
+        principal,
+        kTestPermissionKey,
+        browser
+      );
       denied = false;
       mockRequest._cancelled = false;
 
       // Bring the PopupNotification back up now...
       shownPromise = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
@@ -294,60 +305,62 @@ add_task(async function test_with_permis
 
       // Test denying the permission request.
       Assert.equal(
         notification.secondaryActions.length,
         1,
         "There should only be 1 secondary action"
       );
       await clickSecondaryAction();
-      curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
-      Assert.deepEqual(
-        curPerm,
-        {
-          state: SitePermissions.BLOCK,
-          scope: SitePermissions.SCOPE_PERSISTENT,
-        },
+      curPerm = PermissionTestUtils.getPermissionObject(
+        principal.URI,
+        kTestPermissionKey
+      );
+      Assert.equal(
+        curPerm.capability,
+        Services.perms.DENY_ACTION,
         "Should have denied the action"
       );
+      Assert.equal(curPerm.expireTime, 0, "Deny should be permanent");
       Assert.ok(denied, "The secondaryAction callback should have fired");
       Assert.ok(!allowed, "The mainAction callback should not have fired");
       Assert.ok(
         mockRequest._cancelled,
         "The request should have been cancelled"
       );
       Assert.ok(
         !mockRequest._allowed,
         "The request should not have been allowed"
       );
 
       // Clear the permission and pretend we never denied
-      SitePermissions.remove(principal.URI, kTestPermissionKey);
+      PermissionTestUtils.remove(principal.URI, kTestPermissionKey);
       denied = false;
       mockRequest._cancelled = false;
 
       // Bring the PopupNotification back up now...
       shownPromise = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
       TestPrompt.prompt();
       await shownPromise;
 
       // Test allowing the permission request.
       await clickMainAction();
-      curPerm = SitePermissions.get(principal.URI, kTestPermissionKey);
-      Assert.deepEqual(
-        curPerm,
-        {
-          state: SitePermissions.ALLOW,
-          scope: SitePermissions.SCOPE_PERSISTENT,
-        },
+      curPerm = PermissionTestUtils.getPermissionObject(
+        principal.URI,
+        kTestPermissionKey
+      );
+      Assert.equal(
+        curPerm.capability,
+        Services.perms.ALLOW_ACTION,
         "Should have allowed the action"
       );
+      Assert.equal(curPerm.expireTime, 0, "Allow should be permanent");
       Assert.ok(!denied, "The secondaryAction callback should not have fired");
       Assert.ok(allowed, "The mainAction callback should have fired");
       Assert.ok(
         !mockRequest._cancelled,
         "The request should not have been cancelled"
       );
       Assert.ok(mockRequest._allowed, "The request should have been allowed");
     }
--- a/browser/modules/test/browser/browser_PermissionUI_prompts.js
+++ b/browser/modules/test/browser/browser_PermissionUI_prompts.js
@@ -4,16 +4,19 @@
  * add-ons can introduce their own permission prompts.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Integration.jsm", this);
 ChromeUtils.import("resource:///modules/PermissionUI.jsm", this);
 ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 
 // Tests that GeolocationPermissionPrompt works as expected
 add_task(async function test_geo_permission_prompt() {
   await testPrompt(PermissionUI.GeolocationPermissionPrompt);
 });
 
 // Tests that DesktopNotificationPermissionPrompt works as expected
 add_task(async function test_desktop_notification_permission_prompt() {
@@ -52,17 +55,17 @@ async function testPrompt(Prompt) {
       let mockRequest = makeMockPermissionRequest(browser);
       let principal = mockRequest.principal;
       let TestPrompt = new Prompt(mockRequest);
       let permissionKey =
         TestPrompt.usePermissionManager && TestPrompt.permissionKey;
 
       registerCleanupFunction(function() {
         if (permissionKey) {
-          SitePermissions.remove(principal.URI, permissionKey);
+          PermissionTestUtils.remove(principal.URI, permissionKey);
         }
       });
 
       let shownPromise = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
       TestPrompt.prompt();
@@ -70,17 +73,21 @@ async function testPrompt(Prompt) {
       let notification = PopupNotifications.getNotification(
         TestPrompt.notificationID,
         browser
       );
       Assert.ok(notification, "Should have gotten the notification");
 
       let curPerm;
       if (permissionKey) {
-        curPerm = SitePermissions.get(principal.URI, permissionKey, browser);
+        curPerm = SitePermissions.getForPrincipal(
+          principal,
+          permissionKey,
+          browser
+        );
         Assert.equal(
           curPerm.state,
           SitePermissions.UNKNOWN,
           "Should be no permission set to begin with."
         );
       }
 
       // First test denying the permission request without the checkbox checked.
@@ -104,17 +111,21 @@ async function testPrompt(Prompt) {
         notification.secondaryActions.length,
         expectedSecondaryActionsCount,
         "There should only be " +
           expectedSecondaryActionsCount +
           " secondary action(s)"
       );
       await clickSecondaryAction();
       if (permissionKey) {
-        curPerm = SitePermissions.get(principal.URI, permissionKey, browser);
+        curPerm = SitePermissions.getForPrincipal(
+          principal,
+          permissionKey,
+          browser
+        );
         Assert.deepEqual(
           curPerm,
           {
             state: SitePermissions.BLOCK,
             scope: SitePermissions.SCOPE_TEMPORARY,
           },
           "Should have denied the action temporarily"
         );
@@ -123,17 +134,17 @@ async function testPrompt(Prompt) {
           mockRequest._cancelled,
           "The request should have been cancelled"
         );
         Assert.ok(
           !mockRequest._allowed,
           "The request should not have been allowed"
         );
 
-        SitePermissions.remove(principal.URI, permissionKey, browser);
+        SitePermissions.removeFromPrincipal(principal, permissionKey, browser);
         mockRequest._cancelled = false;
       }
 
       // Bring the PopupNotification back up now...
       shownPromise = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
@@ -154,37 +165,38 @@ async function testPrompt(Prompt) {
         notification.secondaryActions.length,
         expectedSecondaryActionsCount,
         "There should only be " +
           expectedSecondaryActionsCount +
           " secondary action(s)"
       );
       await clickSecondaryAction(secondaryActionToClickIndex);
       if (permissionKey) {
-        curPerm = SitePermissions.get(principal.URI, permissionKey);
-        Assert.deepEqual(
-          curPerm,
-          {
-            state: SitePermissions.BLOCK,
-            scope: SitePermissions.SCOPE_PERSISTENT,
-          },
-          "Should have denied the action permanently"
+        curPerm = PermissionTestUtils.getPermissionObject(
+          principal.URI,
+          permissionKey
         );
+        Assert.equal(
+          curPerm.capability,
+          Services.perms.DENY_ACTION,
+          "Should have denied the action"
+        );
+        Assert.equal(curPerm.expireTime, 0, "Deny should be permanent");
         Assert.ok(
           mockRequest._cancelled,
           "The request should have been cancelled"
         );
         Assert.ok(
           !mockRequest._allowed,
           "The request should not have been allowed"
         );
       }
 
       if (permissionKey) {
-        SitePermissions.remove(principal.URI, permissionKey);
+        PermissionTestUtils.remove(principal.URI, permissionKey);
         mockRequest._cancelled = false;
       }
 
       // Bring the PopupNotification back up now...
       shownPromise = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popupshown"
       );
@@ -192,25 +204,26 @@ async function testPrompt(Prompt) {
       await shownPromise;
 
       // Test allowing the permission request with the checkbox checked.
       popupNotification = getPopupNotificationNode();
       popupNotification.checkbox.checked = true;
 
       await clickMainAction();
       if (permissionKey) {
-        curPerm = SitePermissions.get(principal.URI, permissionKey);
-        Assert.deepEqual(
-          curPerm,
-          {
-            state: SitePermissions.ALLOW,
-            scope: SitePermissions.SCOPE_PERSISTENT,
-          },
-          "Should have allowed the action permanently"
+        curPerm = PermissionTestUtils.getPermissionObject(
+          principal.URI,
+          permissionKey
         );
+        Assert.equal(
+          curPerm.capability,
+          Services.perms.ALLOW_ACTION,
+          "Should have allowed the action"
+        );
+        Assert.equal(curPerm.expireTime, 0, "Allow should be permanent");
         Assert.ok(
           !mockRequest._cancelled,
           "The request should not have been cancelled"
         );
         Assert.ok(mockRequest._allowed, "The request should have been allowed");
       }
     }
   );
--- a/browser/modules/test/unit/test_SiteDataManager.js
+++ b/browser/modules/test/unit/test_SiteDataManager.js
@@ -8,16 +8,19 @@ const EXAMPLE_ORIGIN_2 = "https://exampl
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { SiteDataManager } = ChromeUtils.import(
   "resource:///modules/SiteDataManager.jsm"
 );
 const { SiteDataTestUtils } = ChromeUtils.import(
   "resource://testing-common/SiteDataTestUtils.jsm"
 );
+const { PermissionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PermissionTestUtils.jsm"
+);
 ChromeUtils.defineModuleGetter(
   this,
   "setTimeout",
   "resource://gre/modules/Timer.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "TestUtils",
@@ -114,17 +117,17 @@ add_task(async function testGetTotalUsag
 
   await SiteDataTestUtils.clear();
 });
 
 add_task(async function testRemove() {
   await SiteDataManager.updateSites();
 
   let uri = Services.io.newURI(EXAMPLE_ORIGIN);
-  Services.perms.add(uri, "camera", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
 
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN, "foo1", "bar1");
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN, "foo2", "bar2");
   await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN_2, "foo", "bar");
   await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
   await SiteDataTestUtils.persist(EXAMPLE_ORIGIN_2);
 
@@ -146,35 +149,35 @@ add_task(async function testRemove() {
   );
 
   let usage = await SiteDataTestUtils.getQuotaUsage(EXAMPLE_ORIGIN);
   Assert.equal(usage, 0, "Has cleared quota usage for example.com");
 
   let cookies = Services.cookies.countCookiesFromHost("example.com");
   Assert.equal(cookies, 0, "Has cleared cookies for example.com");
 
-  let perm = Services.perms.testPermission(uri, "persistent-storage");
+  let perm = PermissionTestUtils.testPermission(uri, "persistent-storage");
   Assert.equal(
     perm,
     Services.perms.UNKNOWN_ACTION,
     "Cleared the persistent-storage permission."
   );
-  perm = Services.perms.testPermission(uri, "camera");
+  perm = PermissionTestUtils.testPermission(uri, "camera");
   Assert.equal(
     perm,
     Services.perms.ALLOW_ACTION,
     "Did not clear other permissions."
   );
 
-  Services.perms.remove(uri, "camera");
+  PermissionTestUtils.remove(uri, "camera");
 });
 
 add_task(async function testRemoveSiteData() {
   let uri = Services.io.newURI(EXAMPLE_ORIGIN);
-  Services.perms.add(uri, "camera", Services.perms.ALLOW_ACTION);
+  PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
 
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN, "foo1", "bar1");
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN, "foo2", "bar2");
   await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
   SiteDataTestUtils.addToCookies(EXAMPLE_ORIGIN_2, "foo", "bar");
   await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
   await SiteDataTestUtils.persist(EXAMPLE_ORIGIN_2);
 
@@ -194,23 +197,23 @@ add_task(async function testRemoveSiteDa
   Assert.equal(usage, 0, "Has cleared quota usage for example.com");
 
   usage = await SiteDataTestUtils.getQuotaUsage(EXAMPLE_ORIGIN_2);
   Assert.equal(usage, 0, "Has cleared quota usage for example.org");
 
   let cookies = Services.cookies.countCookiesFromHost("example.org");
   Assert.equal(cookies, 0, "Has cleared cookies for example.org");
 
-  let perm = Services.perms.testPermission(uri, "persistent-storage");
+  let perm = PermissionTestUtils.testPermission(uri, "persistent-storage");
   Assert.equal(
     perm,
     Services.perms.UNKNOWN_ACTION,
     "Cleared the persistent-storage permission."
   );
-  perm = Services.perms.testPermission(uri, "camera");
+  perm = PermissionTestUtils.testPermission(uri, "camera");
   Assert.equal(
     perm,
     Services.perms.ALLOW_ACTION,
     "Did not clear other permissions."
   );
 
-  Services.perms.remove(uri, "camera");
+  PermissionTestUtils.remove(uri, "camera");
 });
--- a/devtools/client/accessibility/components/TextLabelCheck.js
+++ b/devtools/client/accessibility/components/TextLabelCheck.js
@@ -99,17 +99,17 @@ const ISSUE_TO_ANNOTATION_MAP = {
     args: {
       get code() {
         return ReactDOM.code({}, "fieldset");
       },
     },
   },
   [FORM_FIELDSET_NO_NAME_FROM_LEGEND]: {
     href: A11Y_TEXT_LABEL_LINKS.FORM_FIELDSET_NO_NAME_FROM_LEGEND,
-    l10nId: "accessibility-text-label-issue-fieldset-legend",
+    l10nId: "accessibility-text-label-issue-fieldset-legend2",
     args: {
       get code() {
         return ReactDOM.code({}, "legend");
       },
       // Note: there is no way right now to use custom elements in privileged
       // content. We have to use something like <span> since we can't provide
       // two args with the same name.
       get span() {
@@ -122,17 +122,17 @@ const ISSUE_TO_ANNOTATION_MAP = {
     l10nId: "accessibility-text-label-issue-form",
   },
   [FORM_NO_VISIBLE_NAME]: {
     href: A11Y_TEXT_LABEL_LINKS.FORM_NO_VISIBLE_NAME,
     l10nId: "accessibility-text-label-issue-form-visible",
   },
   [FORM_OPTGROUP_NO_NAME_FROM_LABEL]: {
     href: A11Y_TEXT_LABEL_LINKS.FORM_OPTGROUP_NO_NAME_FROM_LABEL,
-    l10nId: "accessibility-text-label-issue-optgroup-label",
+    l10nId: "accessibility-text-label-issue-optgroup-label2",
     args: {
       get code() {
         return ReactDOM.code({}, "label");
       },
       // Note: there is no way right now to use custom elements in privileged
       // content. We have to use something like <span> since we can't provide
       // two args with the same name.
       get span() {
--- a/devtools/client/debugger/src/client/firefox/commands.js
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -1,17 +1,16 @@
 /* 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/>. */
 
 // @flow
 
-import { prepareSourcePayload, createWorker } from "./create";
-import { supportsWorkers, updateWorkerTargets } from "./workers";
-import { features } from "../../utils/prefs";
+import { prepareSourcePayload, createTarget } from "./create";
+import { updateTargets } from "./targets";
 
 import Reps from "devtools-reps";
 import type { Node } from "devtools-reps";
 
 import type {
   ActorId,
   BreakpointLocation,
   BreakpointOptions,
@@ -22,51 +21,51 @@ import type {
   Script,
   SourceId,
   SourceActor,
   Worker,
   Range,
 } from "../../types";
 
 import type {
-  TabTarget,
+  Target,
   DebuggerClient,
   Grip,
   ThreadFront,
   ObjectClient,
   SourcesPacket,
 } from "./types";
 
 import type {
   EventListenerCategoryList,
   EventListenerActiveList,
 } from "../../actions/types";
 
-let workerTargets: Object;
-let threadFront: ThreadFront;
-let tabTarget: TabTarget;
+let targets: { [string]: Target };
+let currentThreadFront: ThreadFront;
+let currentTarget: Target;
 let debuggerClient: DebuggerClient;
 let sourceActors: { [ActorId]: SourceId };
 let breakpoints: { [string]: Object };
 let eventBreakpoints: ?EventListenerActiveList;
 let supportsWasm: boolean;
 
 type Dependencies = {
   threadFront: ThreadFront,
-  tabTarget: TabTarget,
+  tabTarget: Target,
   debuggerClient: DebuggerClient,
   supportsWasm: boolean,
 };
 
 function setupCommands(dependencies: Dependencies) {
-  threadFront = dependencies.threadFront;
-  tabTarget = dependencies.tabTarget;
+  currentThreadFront = dependencies.threadFront;
+  currentTarget = dependencies.tabTarget;
   debuggerClient = dependencies.debuggerClient;
   supportsWasm = dependencies.supportsWasm;
-  workerTargets = {};
+  targets = {};
   sourceActors = {};
   breakpoints = {};
 }
 
 function hasWasmSupport() {
   return supportsWasm;
 }
 
@@ -94,42 +93,44 @@ function releaseActor(actor: String) {
   return debuggerClient.release(actor);
 }
 
 function sendPacket(packet: Object) {
   return debuggerClient.request(packet);
 }
 
 function lookupTarget(thread: string) {
-  if (thread == threadFront.actor) {
-    return tabTarget;
+  if (thread == currentThreadFront.actor) {
+    return currentTarget;
   }
-  if (!workerTargets[thread]) {
+
+  if (!targets[thread]) {
     throw new Error(`Unknown thread front: ${thread}`);
   }
-  return workerTargets[thread];
+
+  return targets[thread];
 }
 
 function lookupThreadFront(thread: string) {
   const target = lookupTarget(thread);
   return target.threadFront;
 }
 
-function listWorkerThreadFronts() {
-  return (Object.values(workerTargets): any).map(target => target.threadFront);
+function listThreadFronts() {
+  return (Object.values(targets): any).map(target => target.threadFront);
 }
 
 function forEachThread(iteratee) {
   // We have to be careful here to atomically initiate the operation on every
   // thread, with no intervening await. Otherwise, other code could run and
   // trigger additional thread operations. Requests on server threads will
   // resolve in FIFO order, and this could result in client and server state
   // going out of sync.
 
-  const promises = [threadFront, ...listWorkerThreadFronts()].map(
+  const promises = [currentThreadFront, ...listThreadFronts()].map(
     // If a thread shuts down while sending the message then it will
     // throw. Ignore these exceptions.
     t => iteratee(t).catch(e => console.log(e))
   );
 
   return Promise.all(promises);
 }
 
@@ -167,49 +168,53 @@ async function sourceContents({
 }: SourceActor): Promise<{| source: any, contentType: ?string |}> {
   const sourceThreadFront = lookupThreadFront(thread);
   const sourceFront = sourceThreadFront.source({ actor });
   const { source, contentType } = await sourceFront.source();
   return { source, contentType };
 }
 
 function setXHRBreakpoint(path: string, method: string) {
-  return threadFront.setXHRBreakpoint(path, method);
+  return currentThreadFront.setXHRBreakpoint(path, method);
 }
 
 function removeXHRBreakpoint(path: string, method: string) {
-  return threadFront.removeXHRBreakpoint(path, method);
+  return currentThreadFront.removeXHRBreakpoint(path, method);
 }
 
 // Get the string key to use for a breakpoint location.
 // See also duplicate code in breakpoint-actor-map.js :(
 function locationKey(location: BreakpointLocation) {
   const { sourceUrl, line, column } = location;
   const sourceId = location.sourceId || "";
   // $FlowIgnore
   return `${sourceUrl}:${sourceId}:${line}:${column}`;
 }
 
 function detachWorkers() {
-  for (const thread of listWorkerThreadFronts()) {
+  for (const thread of listThreadFronts()) {
     thread.detach();
   }
 }
 
 function maybeGenerateLogGroupId(options) {
-  if (options.logValue && tabTarget.traits && tabTarget.traits.canRewind) {
+  if (
+    options.logValue &&
+    currentTarget.traits &&
+    currentTarget.traits.canRewind
+  ) {
     return { ...options, logGroupId: `logGroup-${Math.random()}` };
   }
   return options;
 }
 
 function maybeClearLogpoint(location: BreakpointLocation) {
   const bp = breakpoints[locationKey(location)];
-  if (bp && bp.options.logGroupId && tabTarget.activeConsole) {
-    tabTarget.activeConsole.emit(
+  if (bp && bp.options.logGroupId && currentTarget.activeConsole) {
+    currentTarget.activeConsole.emit(
       "clearLogpointMessages",
       bp.options.logGroupId
     );
   }
 }
 
 function hasBreakpoint(location: BreakpointLocation) {
   return !!breakpoints[locationKey(location)];
@@ -243,53 +248,53 @@ async function evaluateExpressions(scrip
 
 type EvaluateParam = { thread: string, frameId: ?FrameId };
 
 function evaluate(
   script: ?Script,
   { thread, frameId }: EvaluateParam = {}
 ): Promise<{ result: Grip | null }> {
   const params = { thread, frameActor: frameId };
-  if (!tabTarget || !script) {
+  if (!currentTarget || !script) {
     return Promise.resolve({ result: null });
   }
 
-  const target = thread ? lookupTarget(thread) : tabTarget;
+  const target = thread ? lookupTarget(thread) : currentTarget;
   const console = target.activeConsole;
   if (!console) {
     return Promise.resolve({ result: null });
   }
 
   return console.evaluateJSAsync(script, params);
 }
 
 function autocomplete(
   input: string,
   cursor: number,
   frameId: ?string
 ): Promise<mixed> {
-  if (!tabTarget || !tabTarget.activeConsole || !input) {
+  if (!currentTarget || !currentTarget.activeConsole || !input) {
     return Promise.resolve({});
   }
   return new Promise(resolve => {
-    tabTarget.activeConsole.autocomplete(
+    currentTarget.activeConsole.autocomplete(
       input,
       cursor,
       result => resolve(result),
       frameId
     );
   });
 }
 
 function navigate(url: string): Promise<*> {
-  return tabTarget.navigateTo({ url });
+  return currentTarget.navigateTo({ url });
 }
 
 function reload(): Promise<*> {
-  return tabTarget.reload();
+  return currentTarget.reload();
 }
 
 function getProperties(thread: string, grip: Grip): Promise<*> {
   const objClient = lookupThreadFront(thread).pauseGrip(grip);
 
   return objClient.getPrototypeAndProperties().then(resp => {
     const { ownProperties, safeGetterValues } = resp;
     for (const name in safeGetterValues) {
@@ -323,17 +328,17 @@ function pauseOnExceptions(
   );
 }
 
 async function blackBox(
   sourceActor: SourceActor,
   isBlackBoxed: boolean,
   range?: Range
 ): Promise<*> {
-  const sourceFront = threadFront.source({ actor: sourceActor.actor });
+  const sourceFront = currentThreadFront.source({ actor: sourceActor.actor });
   if (isBlackBoxed) {
     await sourceFront.unblackBox(range);
   } else {
     await sourceFront.blackBox(range);
   }
 }
 
 function setSkipPausing(shouldSkip: boolean) {
@@ -351,17 +356,17 @@ function setEventListenerBreakpoints(ids
 }
 
 // eslint-disable-next-line
 async function getEventListenerBreakpointTypes(): Promise<
   EventListenerCategoryList
 > {
   let categories;
   try {
-    categories = await threadFront.getAvailableEventBreakpoints();
+    categories = await currentThreadFront.getAvailableEventBreakpoints();
 
     if (!Array.isArray(categories)) {
       // When connecting to older browser that had our placeholder
       // implementation of the 'getAvailableEventBreakpoints' endpoint, we
       // actually get back an object with a 'value' property containing
       // the categories. Since that endpoint wasn't actually backed with a
       // functional implementation, we just bail here instead of storing the
       // 'value' property into the categories.
@@ -385,73 +390,57 @@ async function getSources(
   client: ThreadFront
 ): Promise<Array<GeneratedSourceData>> {
   const { sources }: SourcesPacket = await client.getSources();
 
   return sources.map(source => prepareSourcePayload(client, source));
 }
 
 async function fetchSources(): Promise<Array<GeneratedSourceData>> {
-  return getSources(threadFront);
+  return getSources(currentThreadFront);
 }
 
 function getSourceForActor(actor: ActorId) {
   if (!sourceActors[actor]) {
     throw new Error(`Unknown source actor: ${actor}`);
   }
   return sourceActors[actor];
 }
 
 async function fetchWorkers(): Promise<Worker[]> {
-  if (features.windowlessWorkers) {
-    const options = {
-      breakpoints,
-      eventBreakpoints,
-      observeAsmJS: true,
-    };
-
-    const newWorkerTargets = await updateWorkerTargets({
-      tabTarget,
-      debuggerClient,
-      threadFront,
-      workerTargets,
-      options,
-    });
+  const options = {
+    breakpoints,
+    eventBreakpoints,
+    observeAsmJS: true,
+  };
 
-    // Fetch the sources and install breakpoints on any new workers.
-    const workerNames = Object.getOwnPropertyNames(newWorkerTargets);
-    for (const actor of workerNames) {
-      if (!workerTargets[actor]) {
-        const front = newWorkerTargets[actor].threadFront;
+  const newTargets = await updateTargets({
+    currentTarget,
+    debuggerClient,
+    targets,
+    options,
+  });
 
-        // This runs in the background and populates some data, but we also
-        // want to allow it to fail quietly. For instance, it is pretty easy
-        // for source clients to throw during the fetch if their thread
-        // shuts down, and this would otherwise cause test failures.
-        getSources(front).catch(e => console.error(e));
-      }
+  // Fetch the sources and install breakpoints on any new workers.
+  // NOTE: This runs in the background and fails quitely because it is
+  // pretty easy for sources to throw during the fetch if their thread
+  // shuts down, which would cause test failures.
+  for (const actor in newTargets) {
+    if (!targets[actor]) {
+      const { threadFront } = newTargets[actor];
+      getSources(threadFront).catch(e => console.error(e));
     }
-
-    workerTargets = newWorkerTargets;
-
-    return workerNames.map(actor =>
-      createWorker(actor, workerTargets[actor].url)
-    );
   }
 
-  if (!supportsWorkers(tabTarget)) {
-    return Promise.resolve([]);
-  }
-
-  const { workers } = await tabTarget.listWorkers();
-  return workers;
+  targets = newTargets;
+  return Object.keys(targets).map(id => createTarget(id, targets[id]));
 }
 
 function getMainThread() {
-  return threadFront.actor;
+  return currentThreadFront.actor;
 }
 
 async function getBreakpointPositions(
   actors: Array<SourceActor>,
   range: ?Range
 ): Promise<{ [string]: number[] }> {
   const sourcePositions = {};
 
--- a/devtools/client/debugger/src/client/firefox/create.js
+++ b/devtools/client/debugger/src/client/firefox/create.js
@@ -1,22 +1,23 @@
 /* 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/>. */
 
 // @flow
 // This module converts Firefox specific types to the generic types
 
-import type { Frame, ThreadId, GeneratedSourceData } from "../../types";
+import type { Frame, ThreadId, GeneratedSourceData, Worker } from "../../types";
 import type {
   PausedPacket,
   FramesResponse,
   FramePacket,
   SourcePayload,
   ThreadFront,
+  Target,
 } from "./types";
 
 import { clientCommands } from "./commands";
 
 export function prepareSourcePayload(
   client: ThreadFront,
   source: SourcePayload
 ): GeneratedSourceData {
@@ -67,17 +68,17 @@ export function createPause(
   return {
     ...packet,
     thread,
     frame: createFrame(thread, frame),
     frames: response.frames.map(createFrame.bind(null, thread)),
   };
 }
 
-export function createWorker(actor: string, url: string) {
+export function createTarget(actor: string, target: Target): Worker {
   return {
     actor,
-    url,
+    url: target.url || "",
     // Ci.nsIWorkerDebugger.TYPE_DEDICATED
-    type: 0,
+    type: actor.includes("process") ? 1 : 0,
     name: "",
   };
 }
--- a/devtools/client/debugger/src/client/firefox/events.js
+++ b/devtools/client/debugger/src/client/firefox/events.js
@@ -4,27 +4,27 @@
 
 // @flow
 
 import type {
   SourcePacket,
   PausedPacket,
   ThreadFront,
   Actions,
-  TabTarget,
+  Target,
 } from "./types";
 
 import { createPause, prepareSourcePayload } from "./create";
 import sourceQueue from "../../utils/source-queue";
 
 const CALL_STACK_PAGE_SIZE = 1000;
 
 type Dependencies = {
   threadFront: ThreadFront,
-  tabTarget: TabTarget,
+  tabTarget: Target,
   actions: Actions,
 };
 
 let actions: Actions;
 let isInterrupted: boolean;
 
 function addThreadEventListeners(thread: ThreadFront) {
   Object.keys(clientEvents).forEach(eventName => {
--- a/devtools/client/debugger/src/client/firefox/moz.build
+++ b/devtools/client/debugger/src/client/firefox/moz.build
@@ -6,10 +6,10 @@
 DIRS += [
 
 ]
 
 CompiledModules(
     'commands.js',
     'create.js',
     'events.js',
-    'workers.js',
+    'targets.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/src/client/firefox/targets.js
@@ -0,0 +1,77 @@
+/* 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/>. */
+
+// @flow
+
+import { addThreadEventListeners } from "./events";
+import { prefs } from "../../utils/prefs";
+import type { DebuggerClient, Target } from "./types";
+
+type Args = {
+  currentTarget: Target,
+  debuggerClient: DebuggerClient,
+  targets: { [string]: Target },
+  options: Object,
+};
+
+async function attachTargets(targetLists, { options, targets }: Args) {
+  const newTargets = {};
+
+  for (const targetFront of targetLists) {
+    try {
+      await targetFront.attach();
+      const threadActorID = targetFront.targetForm.threadActor;
+      if (targets[threadActorID]) {
+        newTargets[threadActorID] = targets[threadActorID];
+      } else {
+        const [, threadFront] = await targetFront.attachThread(options);
+        // NOTE: resume is not necessary for ProcessDescriptors and can be removed
+        // once we switch to WorkerDescriptors
+        threadFront.resume();
+
+        addThreadEventListeners(threadFront);
+
+        await targetFront.attachConsole();
+        newTargets[threadFront.actor] = targetFront;
+      }
+    } catch (e) {
+      // If any of the workers have terminated since the list command initiated
+      // then we will get errors. Ignore these.
+    }
+  }
+
+  return newTargets;
+}
+
+export async function updateWorkerTargets(args: Args) {
+  const { currentTarget } = args;
+  if (!currentTarget.isBrowsingContext || currentTarget.isContentProcess) {
+    return {};
+  }
+
+  const { workers } = await currentTarget.listWorkers();
+  return attachTargets(workers, args);
+}
+
+export async function updateProcessTargets(args: Args): Promise<*> {
+  const { currentTarget, debuggerClient } = args;
+  if (!prefs.fission || !currentTarget.chrome || currentTarget.isAddon) {
+    return Promise.resolve({});
+  }
+
+  const { processes } = await debuggerClient.mainRoot.listProcesses();
+  const targets = await Promise.all(
+    processes
+      .filter(descriptor => !descriptor.isParent)
+      .map(descriptor => descriptor.getTarget())
+  );
+
+  return attachTargets(targets, args);
+}
+
+export async function updateTargets(args: Args) {
+  const workers = await updateWorkerTargets(args);
+  const processes = await updateProcessTargets(args);
+  return { ...workers, ...processes };
+}
--- a/devtools/client/debugger/src/client/firefox/types.js
+++ b/devtools/client/debugger/src/client/firefox/types.js
@@ -184,53 +184,59 @@ export type TabPayload = {
 export type Actions = {
   paused: Pause => void,
   resumed: ActorId => void,
   newQueuedSources: (QueuedSourceData[]) => void,
   fetchEventListeners: () => void,
   updateWorkers: () => void,
 };
 
+type ConsoleClient = {
+  evaluateJS: (
+    script: Script,
+    func: Function,
+    params?: { frameActor: ?FrameId }
+  ) => void,
+  evaluateJSAsync: (
+    script: Script,
+    func: Function,
+    params?: { frameActor: ?FrameId }
+  ) => Promise<{ result: Grip | null }>,
+  autocomplete: (
+    input: string,
+    cursor: number,
+    func: Function,
+    frameId: ?string
+  ) => void,
+  emit: (string, any) => void,
+};
+
 /**
  * Tab Target gives access to the browser tabs
  * @memberof firefox
  * @static
  */
-export type TabTarget = {
+export type Target = {
   on: (string, Function) => void,
   emit: (string, any) => void,
-  threadFront: ThreadFront,
-  activeConsole: {
-    evaluateJS: (
-      script: Script,
-      func: Function,
-      params?: { frameActor: ?FrameId }
-    ) => void,
-    evaluateJSAsync: (
-      script: Script,
-      func: Function,
-      params?: { frameActor: ?FrameId }
-    ) => Promise<{ result: Grip | null }>,
-    autocomplete: (
-      input: string,
-      cursor: number,
-      func: Function,
-      frameId: ?string
-    ) => void,
-    emit: (string, any) => void,
-  },
   form: { consoleActor: any },
   root: any,
   navigateTo: ({ url: string }) => Promise<*>,
   listWorkers: () => Promise<*>,
   reload: () => Promise<*>,
   destroy: () => void,
+  threadFront: ThreadFront,
+  activeConsole: ConsoleClient,
+
   isBrowsingContext: boolean,
   isContentProcess: boolean,
   traits: Object,
+  chrome: Boolean,
+  url: string,
+  isAddon: Boolean,
 };
 
 /**
  * Clients for accessing the Firefox debug server and browser
  * @memberof firefox/clients
  * @static
  */
 
@@ -242,29 +248,26 @@ export type TabTarget = {
 export type DebuggerClient = {
   _activeRequests: {
     get: any => any,
     delete: any => void,
   },
   mainRoot: {
     traits: any,
     getFront: string => Promise<*>,
+    listProcesses: () => Promise<{ processes: ProcessDescriptor }>,
   },
   connect: () => Promise<*>,
   request: (packet: Object) => Promise<*>,
   attachConsole: (actor: String, listeners: Array<*>) => Promise<*>,
   createObjectClient: (grip: Grip) => {},
   release: (actor: String) => {},
 };
 
-export type TabClient = {
-  listWorkers: () => Promise<*>,
-  addListener: (string, Function) => void,
-  on: (string, Function) => void,
-};
+type ProcessDescriptor = Object;
 
 /**
  * A grip is a JSON value that refers to a specific JavaScript value in the
  * debuggee. Grips appear anywhere an arbitrary value from the debuggee needs
  * to be conveyed to the client: stack frames, object property lists, lexical
  * environments, paused packets, and so on.
  *
  * For mutable values like objects and arrays, grips do not merely convey the
@@ -359,24 +362,25 @@ export type ThreadFront = {
   interrupt: () => Promise<*>,
   eventListeners: () => Promise<*>,
   getFrames: (number, number) => FramesResponse,
   getEnvironment: (frame: Frame) => Promise<*>,
   on: (string, Function) => void,
   getSources: () => Promise<SourcesPacket>,
   reconfigure: ({ observeAsmJS: boolean }) => Promise<*>,
   getLastPausePacket: () => ?PausedPacket,
-  _parent: TabClient,
+  _parent: Target,
   actor: ActorId,
   actorID: ActorId,
   request: (payload: Object) => Promise<*>,
   url: string,
-  setActiveEventBreakpoints: (string[]) => void,
+  setActiveEventBreakpoints: (string[]) => Promise<void>,
   getAvailableEventBreakpoints: () => Promise<EventListenerCategoryList>,
   skipBreakpoints: boolean => Promise<{| skip: boolean |}>,
+  detach: () => Promise<void>,
 };
 
 export type Panel = {|
   emit: (eventName: string) => void,
   openLink: (url: string) => void,
   openWorkerToolbox: (worker: Worker) => void,
   openElementInInspector: (grip: Object) => void,
   openConsoleAndEvaluate: (input: string) => void,
deleted file mode 100644
--- a/devtools/client/debugger/src/client/firefox/workers.js
+++ /dev/null
@@ -1,52 +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/>. */
-
-// @flow
-
-import { addThreadEventListeners } from "./events";
-import type { TabTarget } from "./types";
-
-export function supportsWorkers(tabTarget: TabTarget) {
-  return tabTarget.isBrowsingContext || tabTarget.isContentProcess;
-}
-
-export async function updateWorkerTargets({
-  tabTarget,
-  debuggerClient,
-  threadFront,
-  workerTargets,
-  options,
-}: Object) {
-  if (!supportsWorkers(tabTarget)) {
-    return {};
-  }
-
-  const newWorkerTargets = {};
-
-  const { workers } = await tabTarget.listWorkers();
-  for (const workerTargetFront of workers) {
-    try {
-      await workerTargetFront.attach();
-      const threadActorID = workerTargetFront._threadActor;
-      if (workerTargets[threadActorID]) {
-        newWorkerTargets[threadActorID] = workerTargets[threadActorID];
-      } else {
-        const [, workerThread] = await workerTargetFront.attachThread(options);
-        workerThread.resume();
-
-        addThreadEventListeners(workerThread);
-
-        const consoleFront = await workerTargetFront.getFront("console");
-        await consoleFront.startListeners([]);
-
-        newWorkerTargets[workerThread.actor] = workerTargetFront;
-      }
-    } catch (e) {
-      // If any of the workers have terminated since the list command initiated
-      // then we will get errors. Ignore these.
-    }
-  }
-
-  return newWorkerTargets;
-}
--- a/devtools/client/debugger/src/utils/prefs.js
+++ b/devtools/client/debugger/src/utils/prefs.js
@@ -9,16 +9,17 @@ import { isDevelopment } from "devtools-
 import Services from "devtools-services";
 
 // Schema version to bump when the async store format has changed incompatibly
 // and old stores should be cleared.
 const prefsSchemaVersion = 11;
 const pref = Services.pref;
 
 if (isDevelopment()) {
+  pref("devtools.browsertoolbox.fission", false);
   pref("devtools.debugger.logging", false);
   pref("devtools.debugger.alphabetize-outline", false);
   pref("devtools.debugger.auto-pretty-print", false);
   pref("devtools.source-map.client-service.enabled", true);
   pref("devtools.chrome.enabled", false);
   pref("devtools.debugger.pause-on-exceptions", false);
   pref("devtools.debugger.pause-on-caught-exceptions", false);
   pref("devtools.debugger.ignore-caught-exceptions", true);
@@ -69,16 +70,17 @@ if (isDevelopment()) {
   pref("devtools.debugger.features.dom-mutation-breakpoints", true);
   pref("devtools.debugger.features.log-points", true);
   pref("devtools.debugger.features.inline-preview", true);
   pref("devtools.debugger.log-actions", true);
   pref("devtools.debugger.features.overlay-step-buttons", false);
 }
 
 export const prefs = new PrefsHelper("devtools", {
+  fission: ["Bool", "browsertoolbox.fission"],
   logging: ["Bool", "debugger.logging"],
   editorWrapping: ["Bool", "debugger.ui.editor-wrapping"],
   alphabetizeOutline: ["Bool", "debugger.alphabetize-outline"],
   autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
   clientSourceMapsEnabled: ["Bool", "source-map.client-service.enabled"],
   chromeAndExtenstionsEnabled: ["Bool", "chrome.enabled"],
   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
   pauseOnCaughtExceptions: ["Bool", "debugger.pause-on-caught-exceptions"],
--- a/devtools/client/locales/en-US/accessibility.ftl
+++ b/devtools/client/locales/en-US/accessibility.ftl
@@ -30,17 +30,17 @@ accessibility-text-label-issue-dialog = 
 accessibility-text-label-issue-document-title = Documents must have a <code>title</code>. <a>Learn more</a>
 
 accessibility-text-label-issue-embed = Embedded content must be labeled. <a>Learn more</a>
 
 accessibility-text-label-issue-figure = Figures with optional captions should be labeled. <a>Learn more</a>
 
 accessibility-text-label-issue-fieldset = <code>fieldset</code> elements must be labeled. <a>Learn more</a>
 
-accessibility-text-label-issue-fieldset-legend = Use <code>legend</code> element to label <span>fieldset</span> elements. <a>Learn more</a>
+accessibility-text-label-issue-fieldset-legend2 = Use a <code>legend</code> element to label a <span>fieldset</span>. <a>Learn more</a>
 
 accessibility-text-label-issue-form = Form elements must be labeled. <a>Learn more</a>
 
 accessibility-text-label-issue-form-visible = Form elements should have a visible text label. <a>Learn more</a>
 
 accessibility-text-label-issue-frame = <code>frame</code> elements must be labeled. <a>Learn more</a>
 
 accessibility-text-label-issue-glyph = Use <code>alt</code> attribute to label <span>mglyph</span> elements. <a>Learn more</a>
@@ -50,11 +50,11 @@ accessibility-text-label-issue-heading =
 accessibility-text-label-issue-heading-content = Headings should have visible text content. <a>Learn more</a>
 
 accessibility-text-label-issue-iframe = Use <code>title</code> attribute to describe <span>iframe</span> content. <a>Learn more</a>
 
 accessibility-text-label-issue-image = Content with images must be labeled. <a>Learn more</a>
 
 accessibility-text-label-issue-interactive = Interactive elements must be labeled. <a>Learn more</a>
 
-accessibility-text-label-issue-optgroup-label = Use <code>label</code> attribute to label <span>optgroup</span> elements. <a>Learn more</a>
+accessibility-text-label-issue-optgroup-label2 = Use a <code>label</code> attribute to label an <span>optgroup</span>. <a>Learn more</a>
 
 accessibility-text-label-issue-toolbar = Toolbars must be labeled when there is more than one toolbar. <a>Learn more</a>
--- a/devtools/server/actors/highlighters/utils/accessibility.js
+++ b/devtools/server/actors/highlighters/utils/accessibility.js
@@ -671,21 +671,21 @@ class TextLabel extends AuditReport {
     return {
       [AREA_NO_NAME_FROM_ALT]: "accessibility.text.label.issue.area",
       [DIALOG_NO_NAME]: "accessibility.text.label.issue.dialog",
       [DOCUMENT_NO_TITLE]: "accessibility.text.label.issue.document.title",
       [EMBED_NO_NAME]: "accessibility.text.label.issue.embed",
       [FIGURE_NO_NAME]: "accessibility.text.label.issue.figure",
       [FORM_FIELDSET_NO_NAME]: "accessibility.text.label.issue.fieldset",
       [FORM_FIELDSET_NO_NAME_FROM_LEGEND]:
-        "accessibility.text.label.issue.fieldset.legend",
+        "accessibility.text.label.issue.fieldset.legend2",
       [FORM_NO_NAME]: "accessibility.text.label.issue.form",
       [FORM_NO_VISIBLE_NAME]: "accessibility.text.label.issue.form.visible",
       [FORM_OPTGROUP_NO_NAME_FROM_LABEL]:
-        "accessibility.text.label.issue.optgroup.label",
+        "accessibility.text.label.issue.optgroup.label2",
       [FRAME_NO_NAME]: "accessibility.text.label.issue.frame",
       [HEADING_NO_CONTENT]: "accessibility.text.label.issue.heading.content",
       [HEADING_NO_NAME]: "accessibility.text.label.issue.heading",
       [IFRAME_NO_NAME_FROM_TITLE]: "accessibility.text.label.issue.iframe",
       [IMAGE_NO_NAME]: "accessibility.text.label.issue.image",
       [INTERACTIVE_NO_NAME]: "accessibility.text.label.issue.interactive",
       [MATHML_GLYPH_NO_NAME]: "accessibility.text.label.issue.glyph",
       [TOOLBAR_NO_NAME]: "accessibility.text.label.issue.toolbar",
--- a/devtools/shared/locales/en-US/accessibility.properties
+++ b/devtools/shared/locales/en-US/accessibility.properties
@@ -45,20 +45,20 @@ accessibility.text.label.issue.embed = E
 # provided.
 accessibility.text.label.issue.figure = Figures with optional captions should be labeled.
 
 # LOCALIZATION NOTE (accessibility.text.label.issue.fieldset): A title text that
 # describes that currently selected accessible object for a <fieldset> must have a name
 # provided.
 accessibility.text.label.issue.fieldset = “fieldset” elements must be labeled.
 
-# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset.legend): A title text that
+# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset.legend2): A title text that
 # describes that currently selected accessible object for a <fieldset> must have a name
 # provided via <legend> element.
-accessibility.text.label.issue.fieldset.legend = Use “legend” element to label “fieldset” elements.
+accessibility.text.label.issue.fieldset.legend2 = Use a “legend” element to label a “fieldset”.
 
 # LOCALIZATION NOTE (accessibility.text.label.issue.form): A title text that
 # describes that currently selected accessible object for a form element must have a name
 # provided.
 accessibility.text.label.issue.form = Form elements must be labeled.
 
 # LOCALIZATION NOTE (accessibility.text.label.issue.form.visible): A title text that
 # describes that currently selected accessible object for a form element should have a name
@@ -95,17 +95,17 @@ accessibility.text.label.issue.iframe = Use “title” attribute to describe “iframe” content.
 # name provided.
 accessibility.text.label.issue.image = Content with images must be labeled.
 
 # LOCALIZATION NOTE (accessibility.text.label.issue.interactive): A title text that
 # describes that currently selected accessible object for interactive element must have a
 # name provided.
 accessibility.text.label.issue.interactive = Interactive elements must be labeled.
 
-# LOCALIZATION NOTE (accessibility.text.label.issue.optgroup.label): A title text that
+# LOCALIZATION NOTE (accessibility.text.label.issue.optgroup.label2): A title text that
 # describes that currently selected accessible object for an <optgroup> must have a
 # name provided via label attribute.
-accessibility.text.label.issue.optgroup.label = Use “label” attribute to label “optgroup” elements.
+accessibility.text.label.issue.optgroup.label2 = Use a “label” attribute to label an “optgroup”.
 
 # LOCALIZATION NOTE (accessibility.text.label.issue.toolbar): A title text that
 # describes that currently selected accessible object for a toolbar must have a
 # name provided when there is more than one toolbar in the document.
 accessibility.text.label.issue.toolbar = Toolbars must be labeled when there is more than one toolbar.
--- a/docshell/test/navigation/mochitest.ini
+++ b/docshell/test/navigation/mochitest.ini
@@ -70,17 +70,16 @@ skip-if = toolkit == "android" || toolki
 [test_bug430624.html]
 [test_bug430723.html]
 skip-if = (toolkit == 'android') || (!debug && (os == 'mac' || os == 'win')) # Bug 874423
 [test_bug1364364.html]
 [test_bug1375833.html]
 [test_child.html]
 skip-if = fission # Times out.
 [test_grandchild.html]
-skip-if = fission && (debug || asan) # Causes shutdown leaks under Fission.
 fail-if = fission
 [test_not-opener.html]
 skip-if = fission # Times out.
 [test_opener.html]
 skip-if = fission # Times out.
 [test_popup-navigates-children.html]
 skip-if = fission # Times out.
 [test_reserved.html]
--- a/dom/abort/moz.build
+++ b/dom/abort/moz.build
@@ -14,9 +14,11 @@ EXPORTS.mozilla.dom += [
     'AbortSignal.h',
 ]
 
 UNIFIED_SOURCES += [
     'AbortController.cpp',
     'AbortSignal.cpp',
 ]
 
+include('/ipc/chromium/chromium-config.mozbuild')
+
 FINAL_LIBRARY = 'xul'
--- a/dom/animation/test/mozilla/file_restyles.html
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -79,17 +79,17 @@ function observeStyling(frameCount, onFr
   return observeStylingInTargetWindow(window, frameCount, onFrame);
 }
 
 // As with observeStyling but applied to target window |aWindow|.
 function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) {
   const docShell = getDocShellForObservingRestylesForWindow(aWindow);
 
   return new Promise(resolve => {
-    return waitForAnimationFrames(aFrameCount, aOnFrame).then(() => {
+    return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => {
       const markers = docShell.popProfileTimelineMarkers();
       docShell.recordProfileTimelineMarkers = false;
       const stylingMarkers = markers.filter((marker, index) => {
         return marker.name == 'Styles' && marker.isAnimationOnly;
       });
       resolve(stylingMarkers);
     });
   });
--- a/dom/animation/test/testcommon.js
+++ b/dom/animation/test/testcommon.js
@@ -252,50 +252,54 @@ function waitForFrame() {
 }
 
 /**
  * Waits for a requestAnimationFrame callback in the next refresh driver tick.
  * Note that the 'dom.animations-api.core.enabled' and
  * 'dom.animations-api.timelines.enabled' prefs should be true to use this
  * function.
  */
-function waitForNextFrame() {
-  const timeAtStart = document.timeline.currentTime;
+function waitForNextFrame(aWindow = window) {
+  const timeAtStart = aWindow.document.timeline.currentTime;
   return new Promise(resolve => {
-    window.requestAnimationFrame(() => {
-      if (timeAtStart === document.timeline.currentTime) {
-        window.requestAnimationFrame(resolve);
+    aWindow.requestAnimationFrame(() => {
+      if (timeAtStart === aWindow.document.timeline.currentTime) {
+        aWindow.requestAnimationFrame(resolve);
       } else {
         resolve();
       }
     });
   });
 }
 
 /**
  * Returns a Promise that is resolved after the given number of consecutive
  * animation frames have occured (using requestAnimationFrame callbacks).
  *
- * @param frameCount  The number of animation frames.
- * @param onFrame  An optional function to be processed in each animation frame.
+ * @param aFrameCount  The number of animation frames.
+ * @param aOnFrame  An optional function to be processed in each animation frame.
+ * @param aWindow  An optional window object to be used for requestAnimationFrame.
  */
-function waitForAnimationFrames(frameCount, onFrame) {
-  const timeAtStart = document.timeline.currentTime;
+function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) {
+  const timeAtStart = aWindow.document.timeline.currentTime;
   return new Promise(function(resolve, reject) {
     function handleFrame() {
-      if (onFrame && typeof onFrame === "function") {
-        onFrame();
+      if (aOnFrame && typeof aOnFrame === "function") {
+        aOnFrame();
       }
-      if (timeAtStart != document.timeline.currentTime && --frameCount <= 0) {
+      if (
+        timeAtStart != aWindow.document.timeline.currentTime &&
+        --aFrameCount <= 0
+      ) {
         resolve();
       } else {
-        window.requestAnimationFrame(handleFrame); // wait another frame
+        aWindow.requestAnimationFrame(handleFrame); // wait another frame
       }
     }
-    window.requestAnimationFrame(handleFrame);
+    aWindow.requestAnimationFrame(handleFrame);
   });
 }
 
 /**
  * Promise wrapper for requestIdleCallback.
  */
 function waitForIdle() {
   return new Promise(resolve => {
@@ -480,11 +484,11 @@ function animationStartsRightNow(aAnimat
 async function waitForAnimationReadyToRestyle(aAnimation) {
   await aAnimation.ready;
   // If |aAnimation| begins at the current timeline time, we will not process
   // restyling in the initial frame because of aligning with the refresh driver,
   // the animation frame in which the ready promise is resolved happens to
   // coincide perfectly with the start time of the animation.  In this case no
   // restyling is needed in the frame so we have to wait one more frame.
   if (animationStartsRightNow(aAnimation)) {
-    await waitForNextFrame();
+    await waitForNextFrame(aAnimation.ownerGlobal);
   }
 }
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -14046,18 +14046,21 @@ void Document::DocAddSizeOfExcludingThis
   DocumentOrShadowRoot::AddSizeOfExcludingThis(aWindowSizes);
 
   for (auto& sheetArray : mAdditionalSheets) {
     AddSizeOfOwnedSheetArrayExcludingThis(aWindowSizes, sheetArray);
   }
   // Lumping in the loader with the style-sheets size is not ideal,
   // but most of the things in there are in fact stylesheets, so it
   // doesn't seem worthwhile to separate it out.
-  aWindowSizes.mLayoutStyleSheetsSize +=
-      CSSLoader()->SizeOfIncludingThis(aWindowSizes.mState.mMallocSizeOf);
+  // This can be null if we've already been unlinked.
+  if (mCSSLoader) {
+    aWindowSizes.mLayoutStyleSheetsSize +=
+        mCSSLoader->SizeOfIncludingThis(aWindowSizes.mState.mMallocSizeOf);
+  }
 
   aWindowSizes.mDOMOtherSize += mAttrStyleSheet
                                     ? mAttrStyleSheet->DOMSizeOfIncludingThis(
                                           aWindowSizes.mState.mMallocSizeOf)
                                     : 0;
 
   aWindowSizes.mDOMOtherSize += mStyledLinks.ShallowSizeOfExcludingThis(
       aWindowSizes.mState.mMallocSizeOf);
--- a/dom/base/RangeUtils.cpp
+++ b/dom/base/RangeUtils.cpp
@@ -137,30 +137,39 @@ nsresult RangeUtils::CompareNodeToRange(
     nodeEnd = nodeStart + 1;
     MOZ_ASSERT(nodeStart < nodeEnd, "nodeStart shouldn't be INT32_MAX");
   }
 
   // XXX nsContentUtils::ComparePoints() may be expensive.  If some callers
   //     just want one of aNodeIsBeforeRange or aNodeIsAfterRange, we can
   //     skip the other comparison.
 
+  // In the ComparePoints calls below we use a container & offset instead of
+  // a range boundary because the range boundary constructor warns if you pass
+  // in a -1 offset and the ComputeIndexOf call above can return -1 if aNode
+  // is native anonymous content. ComparePoints has comments about offsets
+  // being -1 and it seems to deal with it, or at least we aren't aware of any
+  // problems arising because of it. We don't have a better idea how to get
+  // rid of the warning without much larger changes so we do this just to
+  // silence the warning. (Bug 1438996)
+
   // is RANGE(start) <= NODE(start) ?
   bool disconnected = false;
   *aNodeIsBeforeRange =
-      nsContentUtils::ComparePoints(aAbstractRange->StartRef(),
-                                    RawRangeBoundary(parent, nodeStart),
-                                    &disconnected) > 0;
+      nsContentUtils::ComparePoints(aAbstractRange->StartRef().Container(),
+                                    aAbstractRange->StartRef().Offset(), parent,
+                                    nodeStart, &disconnected) > 0;
   if (NS_WARN_IF(disconnected)) {
     return NS_ERROR_DOM_WRONG_DOCUMENT_ERR;
   }
 
   // is RANGE(end) >= NODE(end) ?
   *aNodeIsAfterRange =
-      nsContentUtils::ComparePoints(aAbstractRange->EndRef(),
-                                    RawRangeBoundary(parent, nodeEnd),
-                                    &disconnected) < 0;
+      nsContentUtils::ComparePoints(aAbstractRange->EndRef().Container(),
+                                    aAbstractRange->EndRef().Offset(), parent,
+                                    nodeEnd, &disconnected) < 0;
   if (NS_WARN_IF(disconnected)) {
     return NS_ERROR_DOM_WRONG_DOCUMENT_ERR;
   }
   return NS_OK;
 }
 
 }  // namespace mozilla
--- a/dom/base/nsContentPermissionHelper.cpp
+++ b/dom/base/nsContentPermissionHelper.cpp
@@ -510,22 +510,25 @@ nsContentPermissionRequester::GetOnVisib
       mListener->GetCallback();
   callback.forget(aCallback);
   return NS_OK;
 }
 
 static nsIPrincipal* GetTopLevelPrincipal(nsPIDOMWindowInner* aWindow) {
   MOZ_ASSERT(aWindow);
 
-  nsPIDOMWindowOuter* top = aWindow->GetInProcessScriptableTop();
-  if (!top) {
+  BrowsingContext* top = aWindow->GetBrowsingContext()->Top();
+  MOZ_ASSERT(top);
+
+  nsPIDOMWindowOuter* outer = top->GetDOMWindow();
+  if (!outer) {
     return nullptr;
   }
 
-  nsPIDOMWindowInner* inner = top->GetCurrentInnerWindow();
+  nsPIDOMWindowInner* inner = outer->GetCurrentInnerWindow();
   if (!inner) {
     return nullptr;
   }
 
   return nsGlobalWindowInner::Cast(inner)->GetPrincipal();
 }
 
 NS_IMPL_CYCLE_COLLECTION(ContentPermissionRequestBase, mPrincipal,
--- a/dom/bindings/Configuration.py
+++ b/dom/bindings/Configuration.py
@@ -77,17 +77,17 @@ class Configuration(DescriptorProvider):
                 continue
             iface = thing
             # Our build system doesn't support dep builds involving
             # addition/removal of partial interfaces that appear in a different
             # .webidl file than the interface they are extending.  Make sure we
             # don't have any of those.  See similar block above for "implements"
             # statements!
             if not iface.isExternal():
-                for partialIface in iface.getPartialInterfaces():
+                for partialIface in iface.getPartials():
                     if (partialIface.filename() != iface.filename() and
                         # Unfortunately, NavigatorProperty does exactly the
                         # thing we're trying to prevent here.  I'm not sure how
                         # to deal with that, short of effectively requiring a
                         # clobber when NavigatorProperty is added/removed and
                         # whitelisting the things it outputs here as
                         # restrictively as I can.
                         (partialIface.identifier.name != "Navigator" or
--- a/dom/bindings/parser/WebIDL.py
+++ b/dom/bindings/parser/WebIDL.py
@@ -309,17 +309,17 @@ class IDLScope(IDLObject):
 
         # Default to throwing, derived classes can override.
         conflictdesc = "\n\t%s at %s\n\t%s at %s" % (originalObject,
                                                      originalObject.location,
                                                      newObject,
                                                      newObject.location)
 
         raise WebIDLError(
-            "Multiple unresolvable definitions of identifier '%s' in scope '%s%s"
+            "Multiple unresolvable definitions of identifier '%s' in scope '%s'%s"
             % (identifier.name, str(self), conflictdesc), [])
 
     def _lookupIdentifier(self, identifier):
         return self._dict[identifier.name]
 
     def lookupIdentifier(self, identifier):
         assert isinstance(identifier, IDLIdentifier)
         assert identifier.scope == self
@@ -600,17 +600,17 @@ class IDLPartialInterfaceOrNamespace(IDL
         self.identifier = name
         self.members = members
         # propagatedExtendedAttrs are the ones that should get
         # propagated to our non-partial interface.
         self.propagatedExtendedAttrs = []
         self._haveSecureContextExtendedAttribute = False
         self._nonPartialInterfaceOrNamespace = nonPartialInterfaceOrNamespace
         self._finished = False
-        nonPartialInterfaceOrNamespace.addPartialInterface(self)
+        nonPartialInterfaceOrNamespace.addPartial(self)
 
     def addExtendedAttributes(self, attrs):
         for attr in attrs:
             identifier = attr.identifier()
 
             if identifier in ["Constructor", "NamedConstructor"]:
                 self.propagatedExtendedAttrs.append(attr)
             elif identifier == "SecureContext":
@@ -671,40 +671,222 @@ def convertExposedAttrToGlobalNameSet(ex
         assert exposedAttr.hasArgs()
         targetSet.update(exposedAttr.args())
 
 
 def globalNameSetToExposureSet(globalScope, nameSet, exposureSet):
     for name in nameSet:
         exposureSet.update(globalScope.globalNameMapping[name])
 
-
-class IDLInterfaceOrNamespace(IDLObjectWithScope, IDLExposureMixins):
+class IDLInterfaceOrInterfaceMixinOrNamespace(IDLObjectWithScope, IDLExposureMixins):
+    def __init__(self, location, parentScope, name):
+        assert isinstance(parentScope, IDLScope)
+        assert isinstance(name, IDLUnresolvedIdentifier)
+
+        self._finished = False
+        self.members = []
+        self._partials = []
+        self._extendedAttrDict = {}
+        self._isKnownNonPartial = False
+
+        IDLObjectWithScope.__init__(self, location, parentScope, name)
+        IDLExposureMixins.__init__(self, location)
+
+    def finish(self, scope):
+        if not self._isKnownNonPartial:
+            raise WebIDLError("%s does not have a non-partial declaration" %
+                              str(self), [self.location])
+
+        IDLExposureMixins.finish(self, scope)
+
+        # Now go ahead and merge in our partials.
+        for partial in self._partials:
+            partial.finish(scope)
+            self.addExtendedAttributes(partial.propagatedExtendedAttrs)
+            self.members.extend(partial.members)
+
+    def resolveIdentifierConflict(self, scope, identifier, originalObject, newObject):
+        assert isinstance(scope, IDLScope)
+        assert isinstance(originalObject, IDLInterfaceMember)
+        assert isinstance(newObject, IDLInterfaceMember)
+
+        retval = IDLScope.resolveIdentifierConflict(self, scope, identifier,
+                                                    originalObject, newObject)
+
+        # Might be a ctor, which isn't in self.members
+        if newObject in self.members:
+            self.members.remove(newObject)
+        return retval
+
+    def typeName(self):
+        if self.isInterface():
+            return "interface"
+        if self.isNamespace():
+            return "namespace"
+        return "interface mixin"
+
+    def getExtendedAttribute(self, name):
+        return self._extendedAttrDict.get(name, None)
+
+    def setNonPartial(self, location, members):
+        if self._isKnownNonPartial:
+            raise WebIDLError("Two non-partial definitions for the "
+                              "same %s" % self.typeName(),
+                              [location, self.location])
+        self._isKnownNonPartial = True
+        # Now make it look like we were parsed at this new location, since
+        # that's the place where the interface is "really" defined
+        self.location = location
+        # Put the new members at the beginning
+        self.members = members + self.members
+    
+    def addPartial(self, partial):
+        assert self.identifier.name == partial.identifier.name
+        self._partials.append(partial)
+
+    def getPartials(self):
+        # Don't let people mutate our guts.
+        return list(self._partials)
+
+    def finishMembers(self, scope):
+        # Assuming we've merged in our partials, set the _exposureGlobalNames on
+        # any members that don't have it set yet.  Note that any partial
+        # interfaces that had [Exposed] set have already set up
+        # _exposureGlobalNames on all the members coming from them, so this is
+        # just implementing the "members default to interface or interface mixin
+        # that defined them" and "partial interfaces or interface mixins default
+        # to interface or interface mixin they're a partial for" rules from the
+        # spec.
+        for m in self.members:
+            # If m, or the partial m came from, had [Exposed]
+            # specified, it already has a nonempty exposure global names set.
+            if len(m._exposureGlobalNames) == 0:
+                m._exposureGlobalNames.update(self._exposureGlobalNames)
+
+        # resolve() will modify self.members, so we need to iterate
+        # over a copy of the member list here.
+        for member in list(self.members):
+            member.resolve(self)
+
+        for member in self.members:
+            member.finish(scope)
+
+        # Now that we've finished our members, which has updated their exposure
+        # sets, make sure they aren't exposed in places where we are not.
+        for member in self.members:
+            if not member.exposureSet.issubset(self.exposureSet):
+                raise WebIDLError("Interface or interface mixin member has"
+                                  "larger exposure set than its container",
+                                  [member.location, self.location])
+
+
+class IDLInterfaceMixin(IDLInterfaceOrInterfaceMixinOrNamespace):
+    def __init__(self, location, parentScope, name, members, isKnownNonPartial):
+        self.actualExposureGlobalNames = set()
+
+        assert isKnownNonPartial or len(members) == 0
+        IDLInterfaceOrInterfaceMixinOrNamespace.__init__(self, location, parentScope, name)
+
+        if isKnownNonPartial:
+            self.setNonPartial(location, members)
+
+    def __str__(self):
+        return "Interface mixin '%s'" % self.identifier.name
+    
+    def finish(self, scope):
+        if self._finished:
+            return
+        self._finished = True
+
+        # Expose to the globals of interfaces that includes this mixin if this
+        # mixin has no explicit [Exposed] so that its members can be exposed
+        # based on the base interface exposure set.
+        # Make sure this is done before IDLExposureMixins.finish call to
+        # prevent exposing to PrimaryGlobal by default.
+        hasImplicitExposure = len(self._exposureGlobalNames) == 0
+        if hasImplicitExposure:
+            self._exposureGlobalNames.update(self.actualExposureGlobalNames)
+
+        IDLInterfaceOrInterfaceMixinOrNamespace.finish(self, scope)
+
+        self.finishMembers(scope)
+
+    def validate(self):
+        for member in self.members:
+
+            if member.isAttr():
+                if member.inherit:
+                    raise WebIDLError("Interface mixin member cannot include "
+                                      "an inherited attribute",
+                                      [member.location, self.location])
+                if member.isStatic():
+                    raise WebIDLError("Interface mixin member cannot include "
+                                      "a static member",
+                                      [member.location, self.location])
+
+            if member.isMethod():
+                if member.isStatic():
+                    raise WebIDLError("Interface mixin member cannot include "
+                                      "a static operation",
+                                      [member.location, self.location])
+                if (member.isGetter() or
+                    member.isSetter() or
+                    member.isDeleter() or
+                    member.isLegacycaller()):
+                    raise WebIDLError("Interface mixin member cannot include a "
+                                      "special operation",
+                                      [member.location, self.location])
+
+    def addExtendedAttributes(self, attrs):
+        for attr in attrs:
+            identifier = attr.identifier()
+
+            if identifier == "SecureContext":
+                if not attr.noArguments():
+                    raise WebIDLError("[%s] must take no arguments" % identifier,
+                                      [attr.location])
+                # This gets propagated to all our members.
+                for member in self.members:
+                    if member.getExtendedAttribute("SecureContext"):
+                        raise WebIDLError("[SecureContext] specified on both "
+                                          "an interface mixin member and on"
+                                          "the interface mixin itself",
+                                          [member.location, attr.location])
+                    member.addExtendedAttributes([attr])
+            elif identifier == "Exposed":
+                convertExposedAttrToGlobalNameSet(attr,
+                                                  self._exposureGlobalNames)
+            else:
+                raise WebIDLError("Unknown extended attribute %s on interface" % identifier,
+                                  [attr.location])
+
+            attrlist = attr.listValue()
+            self._extendedAttrDict[identifier] = attrlist if len(attrlist) else True
+
+    def _getDependentObjects(self):
+        return set(self.members)
+
+
+class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace):
     def __init__(self, location, parentScope, name, parent, members,
                  isKnownNonPartial, toStringTag):
-        assert isinstance(parentScope, IDLScope)
-        assert isinstance(name, IDLUnresolvedIdentifier)
         assert isKnownNonPartial or not parent
         assert isKnownNonPartial or len(members) == 0
 
         self.parent = None
         self._callback = False
-        self._finished = False
-        self.members = []
         self.maplikeOrSetlikeOrIterable = None
-        self._partialInterfaces = []
-        self._extendedAttrDict = {}
         # namedConstructors needs deterministic ordering because bindings code
         # outputs the constructs in the order that namedConstructors enumerates
         # them.
         self.namedConstructors = list()
         self.legacyWindowAliases = []
         self.implementedInterfaces = set()
+        self.includedMixins = set()
         self._consequential = False
-        self._isKnownNonPartial = False
         # self.interfacesBasedOnSelf is the set of interfaces that inherit from
         # self or have self as a consequential interface, including self itself.
         # Used for distinguishability checking.
         self.interfacesBasedOnSelf = set([self])
         # self.interfacesImplementingSelf is the set of interfaces that directly
         # have self as a consequential interface
         self.interfacesImplementingSelf = set()
         self._hasChildInterfaces = False
@@ -715,18 +897,17 @@ class IDLInterfaceOrNamespace(IDLObjectW
         # Tracking of the number of own own members we have in slots
         self._ownMembersInSlots = 0
         # If this is an iterator interface, we need to know what iterable
         # interface we're iterating for in order to get its nativeType.
         self.iterableInterface = None
 
         self.toStringTag = toStringTag
 
-        IDLObjectWithScope.__init__(self, location, parentScope, name)
-        IDLExposureMixins.__init__(self, location)
+        IDLInterfaceOrInterfaceMixinOrNamespace.__init__(self, location, parentScope, name)
 
         if isKnownNonPartial:
             self.setNonPartial(location, parent, members)
 
     def ctor(self):
         identifier = IDLUnresolvedIdentifier(self.location, "constructor",
                                              allowForbidden=True)
         try:
@@ -736,58 +917,34 @@ class IDLInterfaceOrNamespace(IDLObjectW
 
     def isIterable(self):
         return (self.maplikeOrSetlikeOrIterable and
                 self.maplikeOrSetlikeOrIterable.isIterable())
 
     def isIteratorInterface(self):
         return self.iterableInterface is not None
 
-    def resolveIdentifierConflict(self, scope, identifier, originalObject, newObject):
-        assert isinstance(scope, IDLScope)
-        assert isinstance(originalObject, IDLInterfaceMember)
-        assert isinstance(newObject, IDLInterfaceMember)
-
-        retval = IDLScope.resolveIdentifierConflict(self, scope, identifier,
-                                                    originalObject, newObject)
-
-        # Might be a ctor, which isn't in self.members
-        if newObject in self.members:
-            self.members.remove(newObject)
-        return retval
-
     def finish(self, scope):
         if self._finished:
             return
 
         self._finished = True
 
-        if not self._isKnownNonPartial:
-            raise WebIDLError("Interface %s does not have a non-partial "
-                              "declaration" % self.identifier.name,
-                              [self.location])
-
-        IDLExposureMixins.finish(self, scope)
+        IDLInterfaceOrInterfaceMixinOrNamespace.finish(self, scope)
 
         if len(self.legacyWindowAliases) > 0:
             if not self.hasInterfaceObject():
                 raise WebIDLError("Interface %s unexpectedly has [LegacyWindowAlias] "
                                   "and [NoInterfaceObject] together" % self.identifier.name,
                                   [self.location])
             if not self.isExposedInWindow():
                 raise WebIDLError("Interface %s has [LegacyWindowAlias] "
                                   "but not exposed in Window" % self.identifier.name,
                                   [self.location])
 
-        # Now go ahead and merge in our partial interfaces.
-        for partial in self._partialInterfaces:
-            partial.finish(scope)
-            self.addExtendedAttributes(partial.propagatedExtendedAttrs)
-            self.members.extend(partial.members)
-
         # Generate maplike/setlike interface members. Since generated members
         # need to be treated like regular interface members, do this before
         # things like exposure setting.
         for member in self.members:
             if member.isMaplikeOrSetlikeOrIterable():
                 # Check that we only have one interface declaration (currently
                 # there can only be one maplike/setlike declaration per
                 # interface)
@@ -799,29 +956,16 @@ class IDLInterfaceOrNamespace(IDLObjectW
                                        self.maplikeOrSetlikeOrIterable.maplikeOrSetlikeOrIterableType),
                                       [self.maplikeOrSetlikeOrIterable.location,
                                        member.location])
                 self.maplikeOrSetlikeOrIterable = member
                 # If we've got a maplike or setlike declaration, we'll be building all of
                 # our required methods in Codegen. Generate members now.
                 self.maplikeOrSetlikeOrIterable.expand(self.members, self.isJSImplemented())
 
-        # Now that we've merged in our partial interfaces, set the
-        # _exposureGlobalNames on any members that don't have it set yet.  Note
-        # that any partial interfaces that had [Exposed] set have already set up
-        # _exposureGlobalNames on all the members coming from them, so this is
-        # just implementing the "members default to interface that defined them"
-        # and "partial interfaces default to interface they're a partial for"
-        # rules from the spec.
-        for m in self.members:
-            # If m, or the partial interface m came from, had [Exposed]
-            # specified, it already has a nonempty exposure global names set.
-            if len(m._exposureGlobalNames) == 0:
-                m._exposureGlobalNames.update(self._exposureGlobalNames)
-
         assert not self.parent or isinstance(self.parent, IDLIdentifierPlaceholder)
         parent = self.parent.finish(scope) if self.parent else None
         if parent and isinstance(parent, IDLExternalInterface):
             raise WebIDLError("%s inherits from %s which does not have "
                               "a definition" %
                               (self.identifier.name,
                                self.parent.identifier.name),
                               [self.location])
@@ -907,48 +1051,33 @@ class IDLInterfaceOrNamespace(IDLObjectW
                                   "[SecureContext] but inherits from "
                                   "interface %s which does" %
                                   (self.identifier.name,
                                    self.parent.identifier.name),
                                   [self.location, self.parent.location])
 
         for iface in self.implementedInterfaces:
             iface.finish(scope)
+        for mixin in self.includedMixins:
+            mixin.finish(scope)
 
         cycleInGraph = self.findInterfaceLoopPoint(self)
         if cycleInGraph:
             raise WebIDLError("Interface %s has itself as ancestor or "
                               "implemented interface" % self.identifier.name,
                               [self.location, cycleInGraph.location])
 
         if self.isCallback():
             # "implements" should have made sure we have no
             # consequential interfaces.
             assert len(self.getConsequentialInterfaces()) == 0
             # And that we're not consequential.
             assert not self.isConsequential()
 
-        # Now resolve() and finish() our members before importing the
-        # ones from our implemented interfaces.
-
-        # resolve() will modify self.members, so we need to iterate
-        # over a copy of the member list here.
-        for member in list(self.members):
-            member.resolve(self)
-
-        for member in self.members:
-            member.finish(scope)
-
-        # Now that we've finished our members, which has updated their exposure
-        # sets, make sure they aren't exposed in places where we are not.
-        for member in self.members:
-            if not member.exposureSet.issubset(self.exposureSet):
-                raise WebIDLError("Interface member has larger exposure set "
-                                  "than the interface itself",
-                                  [member.location, self.location])
+        self.finishMembers(scope)
 
         ctor = self.ctor()
         if ctor is not None:
             assert len(ctor._exposureGlobalNames) == 0
             ctor._exposureGlobalNames.update(self._exposureGlobalNames)
             ctor.finish(scope)
 
         for ctor in self.namedConstructors:
@@ -991,16 +1120,20 @@ class IDLInterfaceOrNamespace(IDLObjectW
                     if additionalMember.identifier.name == member.identifier.name:
                         raise WebIDLError(
                             "Multiple definitions of %s on %s coming from 'implements' statements" %
                             (member.identifier.name, self),
                             [additionalMember.location, member.location])
             self.members.extend(additionalMembers)
             iface.interfacesImplementingSelf.add(self)
 
+        for mixin in sorted(self.includedMixins,
+                            key=lambda x: x.identifier.name):
+            self.members.extend(mixin.members)
+
         for ancestor in self.getInheritedInterfaces():
             ancestor.interfacesBasedOnSelf.add(self)
             if (ancestor.maplikeOrSetlikeOrIterable is not None and
                 self.maplikeOrSetlikeOrIterable is not None):
                 raise WebIDLError("Cannot have maplike/setlike on %s that "
                                   "inherits %s, which is already "
                                   "maplike/setlike" %
                                   (self.identifier.name,
@@ -1425,16 +1558,20 @@ class IDLInterfaceOrNamespace(IDLObjectW
     def hasInterfacePrototypeObject(self):
         return (not self.isCallback() and not self.isNamespace()
                 and self.getUserData('hasConcreteDescendant', False))
 
     def addImplementedInterface(self, implementedInterface):
         assert(isinstance(implementedInterface, IDLInterface))
         self.implementedInterfaces.add(implementedInterface)
 
+    def addIncludedMixin(self, includedMixin):
+        assert(isinstance(includedMixin, IDLInterfaceMixin))
+        self.includedMixins.add(includedMixin)
+
     def getInheritedInterfaces(self):
         """
         Returns a list of the interfaces this interface inherits from
         (not including this interface itself).  The list is in order
         from most derived to least derived.
         """
         assert(self._finished)
         if not self.parent:
@@ -1473,44 +1610,21 @@ class IDLInterfaceOrNamespace(IDLObjectW
                 return loopPoint
         if otherInterface in self.implementedInterfaces:
             return self
         for iface in self.implementedInterfaces:
             loopPoint = iface.findInterfaceLoopPoint(otherInterface)
             if loopPoint:
                 return loopPoint
         return None
-
-    def getExtendedAttribute(self, name):
-        return self._extendedAttrDict.get(name, None)
-
     def setNonPartial(self, location, parent, members):
         assert not parent or isinstance(parent, IDLIdentifierPlaceholder)
-        if self._isKnownNonPartial:
-            raise WebIDLError("Two non-partial definitions for the "
-                              "same %s" %
-                              ("interface" if self.isInterface()
-                               else "namespace"),
-                              [location, self.location])
-        self._isKnownNonPartial = True
-        # Now make it look like we were parsed at this new location, since
-        # that's the place where the interface is "really" defined
-        self.location = location
+        IDLInterfaceOrInterfaceMixinOrNamespace.setNonPartial(self, location, members)
         assert not self.parent
         self.parent = parent
-        # Put the new members at the beginning
-        self.members = members + self.members
-
-    def addPartialInterface(self, partial):
-        assert self.identifier.name == partial.identifier.name
-        self._partialInterfaces.append(partial)
-
-    def getPartialInterfaces(self):
-        # Don't let people mutate our guts.
-        return list(self._partialInterfaces)
 
     def getJSImplementation(self):
         classId = self.getExtendedAttribute("JSImplementation")
         if not classId:
             return classId
         assert isinstance(classId, list)
         assert len(classId) == 1
         return classId[0]
@@ -1563,16 +1677,17 @@ class IDLInterfaceOrNamespace(IDLObjectW
         return self._hasChildInterfaces
 
     def isOnGlobalProtoChain(self):
         return self._isOnGlobalProtoChain
 
     def _getDependentObjects(self):
         deps = set(self.members)
         deps.update(self.implementedInterfaces)
+        deps.update(self.includedMixins)
         if self.parent:
             deps.add(self.parent)
         return deps
 
     def hasMembersInSlots(self):
         return self._ownMembersInSlots != 0
 
     conditionExtendedAttributes = [ "Pref", "ChromeOnly", "Func",
@@ -5413,16 +5528,59 @@ class IDLImplementsStatement(IDLObject):
         pass
 
     def addExtendedAttributes(self, attrs):
         if len(attrs) != 0:
             raise WebIDLError("There are no extended attributes that are "
                               "allowed on implements statements",
                               [attrs[0].location, self.location])
 
+class IDLIncludesStatement(IDLObject):
+    def __init__(self, location, interface, mixin):
+        IDLObject.__init__(self, location)
+        self.interface = interface
+        self.mixin = mixin
+        self._finished = False
+
+    def finish(self, scope):
+        if self._finished:
+            return
+        self._finished = True
+        assert(isinstance(self.interface, IDLIdentifierPlaceholder))
+        assert(isinstance(self.mixin, IDLIdentifierPlaceholder))
+        interface = self.interface.finish(scope)
+        mixin = self.mixin.finish(scope)
+        # NOTE: we depend on not setting self.interface and
+        # self.mixin here to keep track of the original
+        # locations.
+        if not isinstance(interface, IDLInterface):
+            raise WebIDLError("Left-hand side of 'includes' is not an "
+                              "interface",
+                              [self.interface.location])
+        if interface.isCallback():
+            raise WebIDLError("Left-hand side of 'includes' is a callback "
+                              "interface",
+                              [self.interface.location])
+        if not isinstance(mixin, IDLInterfaceMixin):
+            raise WebIDLError("Right-hand side of 'includes' is not an "
+                              "interface mixin",
+                              [self.mixin.location])
+        mixin.actualExposureGlobalNames.update(interface._exposureGlobalNames)
+        interface.addIncludedMixin(mixin)
+        self.interface = interface
+        self.mixin = mixin
+
+    def validate(self):
+        pass
+
+    def addExtendedAttributes(self, attrs):
+        if len(attrs) != 0:
+            raise WebIDLError("There are no extended attributes that are "
+                              "allowed on includes statements",
+                              [attrs[0].location, self.location])
 
 class IDLExtendedAttribute(IDLObject):
     """
     A class to represent IDL extended attributes so we can give them locations
     """
     def __init__(self, location, tuple):
         IDLObject.__init__(self, location)
         self._tuple = tuple
@@ -5509,22 +5667,24 @@ class Tokenizer(object):
         r'[^\t\n\r 0-9A-Z_a-z]'
         t.type = self.keywords.get(t.value, 'OTHER')
         return t
 
     keywords = {
         "module": "MODULE",
         "interface": "INTERFACE",
         "partial": "PARTIAL",
+        "mixin": "MIXIN",
         "dictionary": "DICTIONARY",
         "exception": "EXCEPTION",
         "enum": "ENUM",
         "callback": "CALLBACK",
         "typedef": "TYPEDEF",
         "implements": "IMPLEMENTS",
+        "includes": "INCLUDES",
         "const": "CONST",
         "null": "NULL",
         "true": "TRUE",
         "false": "FALSE",
         "serializer": "SERIALIZER",
         "stringifier": "STRINGIFIER",
         "unrestricted": "UNRESTRICTED",
         "attribute": "ATTRIBUTE",
@@ -5669,49 +5829,50 @@ class Parser(Tokenizer):
     def p_DefinitionsEmpty(self, p):
         """
             Definitions :
         """
         p[0] = []
 
     def p_Definition(self, p):
         """
-            Definition : CallbackOrInterface
+            Definition : CallbackOrInterfaceOrMixin
                        | Namespace
                        | Partial
                        | Dictionary
                        | Exception
                        | Enum
                        | Typedef
                        | ImplementsStatement
+                       | IncludesStatement
         """
         p[0] = p[1]
         assert p[1]  # We might not have implemented something ...
 
-    def p_CallbackOrInterfaceCallback(self, p):
-        """
-            CallbackOrInterface : CALLBACK CallbackRestOrInterface
+    def p_CallbackOrInterfaceOrMixinCallback(self, p):
+        """
+            CallbackOrInterfaceOrMixin : CALLBACK CallbackRestOrInterface
         """
         if p[2].isInterface():
             assert isinstance(p[2], IDLInterface)
             p[2].setCallback(True)
 
         p[0] = p[2]
 
-    def p_CallbackOrInterfaceInterface(self, p):
-        """
-            CallbackOrInterface : Interface
-        """
-        p[0] = p[1]
+    def p_CallbackOrInterfaceOrMixinInterfaceOrMixin(self, p):
+        """
+            CallbackOrInterfaceOrMixin : INTERFACE InterfaceOrMixin
+        """
+        p[0] = p[2]
 
     def p_CallbackRestOrInterface(self, p):
         """
             CallbackRestOrInterface : CallbackRest
                                     | CallbackConstructorRest
-                                    | Interface
+                                    | CallbackInterface
         """
         assert p[1]
         p[0] = p[1]
 
     def handleNonPartialObject(self, location, identifier, constructor,
                                constructorArgs, nonPartialArgs):
         """
         This handles non-partial objects (interfaces, namespaces and
@@ -5742,36 +5903,49 @@ class Parser(Tokenizer):
         except Exception as ex:
             if isinstance(ex, WebIDLError):
                 raise ex
             pass
 
         # True for isKnownNonPartial
         return constructor(*(constructorArgs + [True]))
 
-    def p_Interface(self, p):
-        """
-            Interface : INTERFACE IDENTIFIER Inheritance LBRACE InterfaceMembers RBRACE SEMICOLON
+    def p_InterfaceOrMixin(self, p):
+        """
+            InterfaceOrMixin : InterfaceRest
+                             | MixinRest
+        """
+        p[0] = p[1]
+
+    def p_CallbackInterface(self, p):
+        """
+            CallbackInterface : INTERFACE InterfaceRest
+        """
+        p[0] = p[2]
+
+    def p_InterfaceRest(self, p):
+        """
+            InterfaceRest : IDENTIFIER Inheritance LBRACE InterfaceMembers RBRACE SEMICOLON
         """
         location = self.getLocation(p, 1)
-        identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
-        members = p[5]
-        parent = p[3]
+        identifier = IDLUnresolvedIdentifier(location, p[1])
+        members = p[4]
+        parent = p[2]
 
         p[0] = self.handleNonPartialObject(
             location, identifier, IDLInterface,
             [location, self.globalScope(), identifier, parent, members],
             [location, parent, members])
 
     def p_InterfaceForwardDecl(self, p):
         """
-            Interface : INTERFACE IDENTIFIER SEMICOLON
+            InterfaceRest : IDENTIFIER SEMICOLON
         """
         location = self.getLocation(p, 1)
-        identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
+        identifier = IDLUnresolvedIdentifier(location, p[1])
 
         try:
             if self.globalScope()._lookupIdentifier(identifier):
                 p[0] = self.globalScope()._lookupIdentifier(identifier)
                 if not isinstance(p[0], IDLExternalInterface):
                     raise WebIDLError("Name collision between external "
                                       "interface declaration for identifier "
                                       "%s and %s" % (identifier.name, p[0]),
@@ -5779,16 +5953,29 @@ class Parser(Tokenizer):
                 return
         except Exception as ex:
             if isinstance(ex, WebIDLError):
                 raise ex
             pass
 
         p[0] = IDLExternalInterface(location, self.globalScope(), identifier)
 
+    def p_MixinRest(self, p):
+        """
+            MixinRest : MIXIN IDENTIFIER LBRACE MixinMembers RBRACE SEMICOLON
+        """
+        location = self.getLocation(p, 1)
+        identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
+        members = p[4]
+
+        p[0] = self.handleNonPartialObject(
+            location, identifier, IDLInterfaceMixin,
+            [location, self.globalScope(), identifier, members],
+            [location, members])
+
     def p_Namespace(self, p):
         """
             Namespace : NAMESPACE IDENTIFIER LBRACE InterfaceMembers RBRACE SEMICOLON
         """
         location = self.getLocation(p, 1)
         identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
         members = p[4]
 
@@ -5798,20 +5985,25 @@ class Parser(Tokenizer):
             [location, None, members])
 
     def p_Partial(self, p):
         """
             Partial : PARTIAL PartialDefinition
         """
         p[0] = p[2]
 
+    def p_PartialDefinitionInterface(self, p):
+        """
+            PartialDefinition : INTERFACE PartialInterfaceOrPartialMixin
+        """
+        p[0] = p[2]
+
     def p_PartialDefinition(self, p):
         """
-            PartialDefinition : PartialInterface
-                              | PartialNamespace
+            PartialDefinition : PartialNamespace
                               | PartialDictionary
         """
         p[0] = p[1]
 
     def handlePartialObject(self, location, identifier, nonPartialConstructor,
                             nonPartialConstructorArgs,
                             partialConstructorArgs):
         """
@@ -5844,42 +6036,63 @@ class Parser(Tokenizer):
         except Exception as ex:
             if isinstance(ex, WebIDLError):
                 raise ex
             pass
 
         if not nonPartialObject:
             nonPartialObject = nonPartialConstructor(
                 # No members, False for isKnownNonPartial
-                *(nonPartialConstructorArgs + [[], False]))
+                *(nonPartialConstructorArgs), members=[], isKnownNonPartial=False)
 
         partialObject = None
         if isinstance(nonPartialObject, IDLDictionary):
             partialObject = IDLPartialDictionary(
                 *(partialConstructorArgs + [nonPartialObject]))
-        elif isinstance(nonPartialObject, (IDLInterface, IDLNamespace)):
+        elif isinstance(nonPartialObject, (IDLInterface, IDLInterfaceMixin, IDLNamespace)):
             partialObject = IDLPartialInterfaceOrNamespace(
                 *(partialConstructorArgs + [nonPartialObject]))
         else:
             raise WebIDLError("Unknown partial object type %s" %
-                    type(partialObject))
+                    type(partialObject),
+                    [location])
 
         return partialObject
 
-    def p_PartialInterface(self, p):
-        """
-            PartialInterface : INTERFACE IDENTIFIER LBRACE InterfaceMembers RBRACE SEMICOLON
+    def p_PartialInterfaceOrPartialMixin(self, p):
+        """
+            PartialInterfaceOrPartialMixin : PartialInterfaceRest
+                                           | PartialMixinRest
+        """
+        p[0] = p[1]
+
+    def p_PartialInterfaceRest(self, p):
+        """
+            PartialInterfaceRest : IDENTIFIER LBRACE InterfaceMembers RBRACE SEMICOLON
+        """
+        location = self.getLocation(p, 1)
+        identifier = IDLUnresolvedIdentifier(location, p[1])
+        members = p[3]
+
+        p[0] = self.handlePartialObject(
+            location, identifier, IDLInterface,
+            [location, self.globalScope(), identifier, None],
+            [location, identifier, members])
+
+    def p_PartialMixinRest(self, p):
+        """
+            PartialMixinRest : MIXIN IDENTIFIER LBRACE MixinMembers RBRACE SEMICOLON
         """
         location = self.getLocation(p, 1)
         identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
         members = p[4]
 
         p[0] = self.handlePartialObject(
-            location, identifier, IDLInterface,
-            [location, self.globalScope(), identifier, None],
+            location, identifier, IDLInterfaceMixin,
+            [location, self.globalScope(), identifier],
             [location, identifier, members])
 
     def p_PartialNamespace(self, p):
         """
             PartialNamespace : NAMESPACE IDENTIFIER LBRACE InterfaceMembers RBRACE SEMICOLON
         """
         location = self.getLocation(p, 1)
         identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
@@ -5914,17 +6127,17 @@ class Parser(Tokenizer):
             Inheritance :
         """
         pass
 
     def p_InterfaceMembers(self, p):
         """
             InterfaceMembers : ExtendedAttributeList InterfaceMember InterfaceMembers
         """
-        p[0] = [p[2]] if p[2] else []
+        p[0] = [p[2]]
 
         assert not p[1] or p[2]
         p[2].addExtendedAttributes(p[1])
 
         p[0].extend(p[3])
 
     def p_InterfaceMembersEmpty(self, p):
         """
@@ -5934,16 +6147,42 @@ class Parser(Tokenizer):
 
     def p_InterfaceMember(self, p):
         """
             InterfaceMember : Const
                             | AttributeOrOperationOrMaplikeOrSetlikeOrIterable
         """
         p[0] = p[1]
 
+        
+    def p_MixinMembersEmpty(self, p):
+        """
+            MixinMembers :
+        """
+        p[0] = []
+
+    def p_MixinMembers(self, p):
+        """
+            MixinMembers : ExtendedAttributeList MixinMember MixinMembers
+        """
+        p[0] = [p[2]]
+
+        assert not p[1] or p[2]
+        p[2].addExtendedAttributes(p[1])
+
+        p[0].extend(p[3])
+
+    def p_MixinMember(self, p):
+        """
+            MixinMember : Const
+                        | Attribute
+                        | Operation
+        """
+        p[0] = p[1]
+
     def p_Dictionary(self, p):
         """
             Dictionary : DICTIONARY IDENTIFIER Inheritance LBRACE DictionaryMembers RBRACE SEMICOLON
         """
         location = self.getLocation(p, 1)
         identifier = IDLUnresolvedIdentifier(self.getLocation(p, 2), p[2])
         members = p[5]
         p[0] = IDLDictionary(location, self.globalScope(), identifier, p[3], members)
@@ -6110,16 +6349,25 @@ class Parser(Tokenizer):
             ImplementsStatement : ScopedName IMPLEMENTS ScopedName SEMICOLON
         """
         assert(p[2] == "implements")
         implementor = IDLIdentifierPlaceholder(self.getLocation(p, 1), p[1])
         implementee = IDLIdentifierPlaceholder(self.getLocation(p, 3), p[3])
         p[0] = IDLImplementsStatement(self.getLocation(p, 1), implementor,
                                       implementee)
 
+    def p_IncludesStatement(self, p):
+        """
+            IncludesStatement : ScopedName INCLUDES ScopedName SEMICOLON
+        """
+        assert(p[2] == "includes")
+        interface = IDLIdentifierPlaceholder(self.getLocation(p, 1), p[1])
+        mixin = IDLIdentifierPlaceholder(self.getLocation(p, 3), p[3])
+        p[0] = IDLIncludesStatement(self.getLocation(p, 1), interface, mixin)
+
     def p_Const(self, p):
         """
             Const : CONST ConstType IDENTIFIER EQUALS ConstValue SEMICOLON
         """
         location = self.getLocation(p, 1)
         type = p[2]
         identifier = IDLUnresolvedIdentifier(self.getLocation(p, 3), p[3])
         value = p[5]
@@ -7239,20 +7487,27 @@ class Parser(Tokenizer):
                 self._productions.append(itr_iface)
                 iterable.iteratorType = IDLWrapperType(iface.location, itr_iface)
 
         # Then, finish all the IDLImplementsStatements.  In particular, we
         # have to make sure we do those before we do the IDLInterfaces.
         # XXX khuey hates this bit and wants to nuke it from orbit.
         implementsStatements = [p for p in self._productions if
                                 isinstance(p, IDLImplementsStatement)]
+        # Make sure we finish IDLIncludesStatements before we finish the
+        # IDLInterfaces.
+        includesStatements = [p for p in self._productions if
+                                isinstance(p, IDLIncludesStatement)]
         otherStatements = [p for p in self._productions if
-                           not isinstance(p, IDLImplementsStatement)]
+                           not isinstance(p, (IDLImplementsStatement,
+                                              IDLIncludesStatement))]
         for production in implementsStatements:
             production.finish(self.globalScope())
+        for production in includesStatements:
+            production.finish(self.globalScope())
         for production in otherStatements:
             production.finish(self.globalScope())
 
         # Do any post-finish validation we need to do
         for production in self._productions:
             production.validate()
 
         # De-duplicate self._productions, without modifying its order.
new file mode 100644
--- /dev/null
+++ b/dom/bindings/parser/tests/test_interfacemixin.py
@@ -0,0 +1,373 @@
+import WebIDL
+
+def WebIDLTest(parser, harness):
+    parser.parse("interface mixin Foo { };")
+    results = parser.finish()
+    harness.ok(True, "Empty interface mixin parsed without error.")
+    harness.check(len(results), 1, "Should be one production")
+    harness.ok(isinstance(results[0], WebIDL.IDLInterfaceMixin),
+               "Should be an IDLInterfaceMixin")
+    mixin = results[0]
+    harness.check(mixin.identifier.QName(), "::Foo", "Interface mixin has the right QName")
+    harness.check(mixin.identifier.name, "Foo", "Interface mixin has the right name")
+
+    parser = parser.reset()
+    parser.parse("""
+        interface mixin QNameBase {
+            const long foo = 3;
+        };
+    """)
+    results = parser.finish()
+    harness.check(len(results), 1, "Should be one productions")
+    harness.ok(isinstance(results[0], WebIDL.IDLInterfaceMixin),
+               "Should be an IDLInterfaceMixin")
+    harness.check(len(results[0].members), 1, "Expect 1 productions")
+    mixin = results[0]
+    harness.check(mixin.members[0].identifier.QName(), "::QNameBase::foo",
+                  "Member has the right QName")
+
+    parser = parser.reset()
+    parser.parse("""
+        interface mixin A {
+            readonly attribute boolean x;
+            void foo();
+        };
+        partial interface mixin A {
+            readonly attribute boolean y;
+            void foo(long arg);
+        };
+    """)
+    results = parser.finish()
+    harness.check(len(results), 2,
+                  "Should have two results with partial interface mixin")
+    mixin = results[0]
+    harness.check(len(mixin.members), 3,
+                  "Should have three members with partial interface mixin")
+    harness.check(mixin.members[0].identifier.name, "x",
+                  "First member should be x with partial interface mixin")
+    harness.check(mixin.members[1].identifier.name, "foo",
+                  "Second member should be foo with partial interface mixin")
+    harness.check(len(mixin.members[1].signatures()), 2,
+                  "Should have two foo signatures with partial interface mixin")
+    harness.check(mixin.members[2].identifier.name, "y",
+                  "Third member should be y with partial interface mixin")
+
+    parser = parser.reset()
+    parser.parse("""
+        partial interface mixin A {
+            readonly attribute boolean y;
+            void foo(long arg);
+        };
+        interface mixin A {
+            readonly attribute boolean x;
+            void foo();
+        };
+    """)
+    results = parser.finish()
+    harness.check(len(results), 2,
+                  "Should have two results with reversed partial interface mixin")
+    mixin = results[1]
+    harness.check(len(mixin.members), 3,
+                  "Should have three members with reversed partial interface mixin")
+    harness.check(mixin.members[0].identifier.name, "x",
+                  "First member should be x with reversed partial interface mixin")
+    harness.check(mixin.members[1].identifier.name, "foo",
+                  "Second member should be foo with reversed partial interface mixin")
+    harness.check(len(mixin.members[1].signatures()), 2,
+                  "Should have two foo signatures with reversed partial interface mixin")
+    harness.check(mixin.members[2].identifier.name, "y",
+                  "Third member should be y with reversed partial interface mixin")
+
+    parser = parser.reset()
+    parser.parse("""
+        interface Interface {};
+        interface mixin Mixin {
+            attribute short x;
+        };
+        Interface includes Mixin;
+    """)
+    results = parser.finish()
+    iface = results[0]
+    harness.check(len(iface.members), 1, "Should merge members from mixins")
+    harness.check(iface.members[0].identifier.name, "x",
+                  "Should merge members from mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                readonly attribute boolean x;
+            };
+            interface mixin A {
+                readonly attribute boolean y;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow two non-partial interface mixins with the same name")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            partial interface mixin A {
+                readonly attribute boolean x;
+            };
+            partial interface mixin A {
+                readonly attribute boolean y;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Must have a non-partial interface mixin for a given name")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            dictionary A {
+                boolean x;
+            };
+            partial interface mixin A {
+                readonly attribute boolean y;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow a name collision between partial interface "
+               "mixin and other object")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            dictionary A {
+                boolean x;
+            };
+            interface mixin A {
+                readonly attribute boolean y;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow a name collision between interface mixin "
+               "and other object")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                readonly attribute boolean x;
+            };
+            interface A;
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow a name collision between external interface "
+               "and interface mixin")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            [SomeRandomAnnotation]
+            interface mixin A {
+                readonly attribute boolean y;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow unknown extended attributes on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                getter double (DOMString propertyName);
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow getters on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                setter void (DOMString propertyName, double propertyValue);
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow setters on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                deleter void (DOMString propertyName);
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow deleters on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                legacycaller double compute(double x);
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow legacycallers on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin A {
+                inherit attribute x;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should not allow inherited attribute on interface mixins")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface Interface {};
+            interface NotMixin {
+                attribute short x;
+            };
+            Interface includes NotMixin;
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should fail if the right side does not point an interface mixin")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin NotInterface {};
+            interface mixin Mixin {
+                attribute short x;
+            };
+            NotInterface includes Mixin;
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should fail if the left side does not point an interface")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin Mixin {
+                iterable<DOMString>;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should fail if an interface mixin includes iterable")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin Mixin {
+                setlike<DOMString>;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should fail if an interface mixin includes setlike")
+
+    parser = parser.reset()
+    threw = False
+    try:
+        parser.parse("""
+            interface mixin Mixin {
+                maplike<DOMString, DOMString>;
+            };
+        """)
+        results = parser.finish()
+    except:
+        threw = True
+    harness.ok(threw,
+               "Should fail if an interface mixin includes maplike")
+
+    parser = parser.reset()
+    parser.parse("""
+        [Global] interface Window {};
+        [Global] interface Worker {};
+        [Exposed=Window]
+        interface Base {};
+        interface mixin Mixin {
+            Base returnSelf();
+        };
+        Base includes Mixin;
+    """)
+    results = parser.finish()
+    base = results[2]
+    attr = base.members[0]
+    harness.check(attr.exposureSet, set(["Window"]),
+                  "Should expose on globals where the base interfaces are exposed")
+
+    parser = parser.reset()
+    parser.parse("""
+        [Global] interface Window {};
+        [Global] interface Worker {};
+        [Exposed=Window]
+        interface Base {};
+        [Exposed=Window]
+        interface mixin Mixin {
+            attribute short a;
+        };
+        Base includes Mixin;
+    """)
+    results = parser.finish()
+    base = results[2]
+    attr = base.members[0]
+    harness.check(attr.exposureSet, set(["Window"]),
+                 "Should follow [Exposed] on interface mixin")
--- a/dom/canvas/CanvasImageCache.cpp
+++ b/dom/canvas/CanvasImageCache.cpp
@@ -8,16 +8,17 @@
 #include "nsIImageLoadingContent.h"
 #include "nsExpirationTracker.h"
 #include "imgIRequest.h"
 #include "mozilla/dom/Element.h"
 #include "nsTHashtable.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
 #include "nsContentUtils.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_canvas.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/gfx/2D.h"
 #include "gfx2DGlue.h"
 
 namespace mozilla {
 
 using namespace dom;
 using namespace gfx;
@@ -113,19 +114,16 @@ class AllCanvasImageCacheEntry : public 
     return HashGeneric(key->mImage.get());
   }
   enum { ALLOW_MEMMOVE = true };
 
   nsCOMPtr<imgIContainer> mImage;
   RefPtr<SourceSurface> mSourceSurface;
 };
 
-static bool sPrefsInitialized = false;
-static int32_t sCanvasImageCacheLimit = 0;
-
 class ImageCacheObserver;
 
 class ImageCache final : public nsExpirationTracker<ImageCacheEntryData, 4> {
  public:
   // We use 3 generations of 1 second each to get a 2-3 seconds timeout.
   enum { GENERATION_MS = 1000 };
   ImageCache();
   ~ImageCache();
@@ -214,21 +212,16 @@ class CanvasImageCacheShutdownObserver f
   NS_DECL_NSIOBSERVER
 };
 
 ImageCache::ImageCache()
     : nsExpirationTracker<ImageCacheEntryData, 4>(
           GENERATION_MS, "ImageCache",
           SystemGroup::EventTargetFor(TaskCategory::Other)),
       mTotal(0) {
-  if (!sPrefsInitialized) {
-    sPrefsInitialized = true;
-    Preferences::AddIntVarCache(&sCanvasImageCacheLimit,
-                                "canvas.image.cache.limit", 0);
-  }
   mImageCacheObserver = new ImageCacheObserver(this);
   MOZ_RELEASE_ASSERT(mImageCacheObserver,
                      "GFX: Can't alloc ImageCacheObserver");
 }
 
 ImageCache::~ImageCache() {
   AgeAllGenerations();
   mImageCacheObserver->Destroy();
@@ -292,21 +285,25 @@ void CanvasImageCache::NotifyDrawImage(E
 
     AllCanvasImageCacheEntry* allEntry =
         gImageCache->mAllCanvasCache.PutEntry(allCanvasCacheKey);
     if (allEntry) {
       allEntry->mSourceSurface = aSource;
     }
   }
 
-  if (!sCanvasImageCacheLimit) return;
+  if (!StaticPrefs::canvas_image_cache_limit()) {
+    return;
+  }
 
   // Expire the image cache early if its larger than we want it to be.
-  while (gImageCache->mTotal > size_t(sCanvasImageCacheLimit))
+  while (gImageCache->mTotal >
+         size_t(StaticPrefs::canvas_image_cache_limit())) {
     gImageCache->AgeOneGeneration();
+  }
 }
 
 SourceSurface* CanvasImageCache::LookupAllCanvas(Element* aImage) {
   if (!gImageCache) {
     return nullptr;
   }
 
   nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage);
--- a/dom/console/moz.build
+++ b/dom/console/moz.build
@@ -47,9 +47,11 @@ LOCAL_INCLUDES += [
     '/dom/base',
     '/js/xpconnect/src',
 ]
 
 MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ]
 MOCHITEST_CHROME_MANIFESTS += [ 'tests/chrome.ini' ]
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 
+include('/ipc/chromium/chromium-config.mozbuild')
+
 FINAL_LIBRARY = 'xul'
--- a/dom/encoding/FallbackEncoding.cpp
+++ b/dom/encoding/FallbackEncoding.cpp
@@ -50,17 +50,16 @@ static const EncodingProp domainsFallbac
 
 static constexpr nsUConvProp nonParticipatingDomains[] = {
 #include "nonparticipatingdomains.properties.h"
 };
 
 NS_IMPL_ISUPPORTS(FallbackEncoding, nsIObserver)
 
 StaticRefPtr<FallbackEncoding> FallbackEncoding::sInstance;
-bool FallbackEncoding::sGuessFallbackFromTopLevelDomain = true;
 
 FallbackEncoding::FallbackEncoding() : mFallback(nullptr) {
   MOZ_ASSERT(!FallbackEncoding::sInstance, "Singleton already exists.");
 }
 
 NotNull<const Encoding*> FallbackEncoding::Get() {
   if (mFallback) {
     return WrapNotNull(mFallback);
@@ -134,18 +133,16 @@ FallbackEncoding::Observe(nsISupports* a
 }
 
 void FallbackEncoding::Initialize() {
   MOZ_ASSERT(!FallbackEncoding::sInstance,
              "Initializing pre-existing fallback cache.");
   FallbackEncoding::sInstance = new FallbackEncoding;
   Preferences::RegisterCallback(FallbackEncoding::PrefChanged,
                                 "intl.charset.fallback.override");
-  Preferences::AddBoolVarCache(&sGuessFallbackFromTopLevelDomain,
-                               "intl.charset.fallback.tld");
 
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->AddObserver(sInstance, "intl:requested-locales-changed", true);
   }
 }
 
 void FallbackEncoding::Shutdown() {
--- a/dom/encoding/FallbackEncoding.h
+++ b/dom/encoding/FallbackEncoding.h
@@ -18,21 +18,16 @@ class Encoding;
 namespace dom {
 
 class FallbackEncoding : public nsIObserver, nsSupportsWeakReference {
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   /**
-   * Whether FromTopLevelDomain() should be used.
-   */
-  static bool sGuessFallbackFromTopLevelDomain;
-
-  /**
    * Gets the locale-dependent fallback encoding for legacy HTML and plain
    * text content.
    *
    * @param aFallback the outparam for the fallback encoding
    */
   static NotNull<const Encoding*> FromLocale();
 
   /**
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -805,17 +805,22 @@ FetchDriver::OnStartRequest(nsIRequest* 
   aRequest->GetStatus(&rv);
   if (NS_FAILED(rv)) {
     FailWithNetworkError(rv);
     return rv;
   }
 
   // We should only get to the following code once.
   MOZ_ASSERT(!mPipeOutputStream);
-  MOZ_ASSERT(mObserver);
+
+  if (!mObserver) {