Merge mozilla-central to mozilla-beta a=same-version-merge l10n=same
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 15 May 2019 18:05:33 +0300
changeset 532717 571bc76da583cfbb4ffeb42c99905cd02e297cc3
parent 532538 18c20e6dcc0eae461388bae521966c6fedfeec1f (current diff)
parent 532716 76bbedc1ec1ae367906390c01a8ca008d7944cac (diff)
child 532718 cf273dce0debb09b0556f9023d16242c8f7e9b7c
push id11270
push userrgurzau@mozilla.com
push dateWed, 15 May 2019 15:07:19 +0000
treeherdermozilla-beta@571bc76da583 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssame-version-merge
milestone68.0
Merge mozilla-central to mozilla-beta a=same-version-merge l10n=same
browser/base/content/test/general/browser_contextmenu.js
browser/base/content/test/general/browser_contextmenu_childprocess.js
browser/base/content/test/general/browser_contextmenu_input.js
browser/base/content/test/general/contextmenu_common.js
browser/base/content/test/general/ctxmenu-image.png
browser/base/content/test/general/subtst_contextmenu.html
browser/base/content/test/general/subtst_contextmenu_input.html
browser/base/content/test/general/subtst_contextmenu_xul.xul
browser/components/controlcenter/content/panel.inc.xul
services/common/tests/unit/test_blocklist_signatures.js
services/common/tests/unit/test_blocklist_signatures/collection_signing_ee.pem.certspec
services/common/tests/unit/test_blocklist_signatures/collection_signing_int.pem.certspec
services/common/tests/unit/test_blocklist_signatures/collection_signing_root.pem.certspec
services/common/tests/unit/test_blocklist_signatures/moz.build
testing/web-platform/meta/service-workers/service-worker/update-bytecheck.https.html.ini
toolkit/modules/addons/.eslintrc.js
toolkit/modules/addons/MatchURLFilters.jsm
toolkit/modules/addons/SecurityInfo.jsm
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestCommon.jsm
toolkit/modules/addons/WebRequestContent.js
toolkit/modules/addons/WebRequestUpload.jsm
toolkit/modules/tests/browser/WebRequest_dynamic.sjs
toolkit/modules/tests/browser/WebRequest_redirection.sjs
toolkit/modules/tests/browser/browser_WebNavigation.js
toolkit/modules/tests/browser/browser_WebRequest.js
toolkit/modules/tests/browser/browser_WebRequest_ancestors.js
toolkit/modules/tests/browser/browser_WebRequest_cookies.js
toolkit/modules/tests/browser/browser_WebRequest_filtering.js
toolkit/modules/tests/browser/dummy_page.html
toolkit/modules/tests/browser/file_WebNavigation_page1.html
toolkit/modules/tests/browser/file_WebNavigation_page2.html
toolkit/modules/tests/browser/file_WebNavigation_page3.html
toolkit/modules/tests/browser/file_WebRequest_page1.html
toolkit/modules/tests/browser/file_WebRequest_page2.html
toolkit/modules/tests/browser/file_image_bad.png
toolkit/modules/tests/browser/file_image_good.png
toolkit/modules/tests/browser/file_image_redirect.png
toolkit/modules/tests/browser/file_script_bad.js
toolkit/modules/tests/browser/file_script_good.js
toolkit/modules/tests/browser/file_script_redirect.js
toolkit/modules/tests/browser/file_script_xhr.js
toolkit/modules/tests/browser/file_style_bad.css
toolkit/modules/tests/browser/file_style_good.css
toolkit/modules/tests/browser/file_style_redirect.css
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -357,17 +357,17 @@ dependencies = [
 ]
 
 [[package]]
 name = "bookmark_sync"
 version = "0.1.0"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "dogear 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dogear 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "moz_task 0.1.0",
  "nserror 0.1.0",
  "nsstring 0.1.0",
  "storage 0.1.0",
  "storage_variant 0.1.0",
  "thin-vec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -915,17 +915,17 @@ dependencies = [
  "regex 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 1.0.88 (git+https://github.com/servo/serde?branch=deserialize_from_enums10)",
  "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "dogear"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallbitvec 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "dtoa"
@@ -3686,17 +3686,17 @@ dependencies = [
 "checksum darling_macro 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "244e8987bd4e174385240cde20a3657f607fb0797563c28255c353b5819a07b1"
 "checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
 "checksum derive_more 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f57d78cf3bd45270dad4e70c21ec77a960b36c7a841ff9db76aaa775a8fb871"
 "checksum devd-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e7c9ac481c38baf400d3b732e4a06850dfaa491d1b6379a249d9d40d14c2434c"
 "checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a"
 "checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
 "checksum dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a"
 "checksum docopt 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "db2906c2579b5b7207fc1e328796a9a8835dc44e22dbe8e460b1d636f9a7b225"
-"checksum dogear 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "30ac4a8e8f834f02deb2266b1f279aa5494e990c625d8be8f2988a7c708ba1f8"
+"checksum dogear 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "26b7583e1427e296c852f3217eaab3890e698f742b8d7349beb1f40c4e946fc9"
 "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
 "checksum dtoa-short 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "068d4026697c1a18f0b0bb8cfcad1b0c151b90d8edb9bf4c235ad68128920d1d"
 "checksum dwrote 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c31c624339dab99c223a4b26c2e803b7c248adaca91549ce654c76f39a03f5c8"
 "checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a"
 "checksum ena 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25b4e5febb25f08c49f1b07dc33a182729a6b21edfb562b5aef95f78e0dbe5bb"
 "checksum encoding_c 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "769ecb8b33323998e482b218c0d13cd64c267609023b4b7ec3ee740714c318ee"
 "checksum encoding_rs 0.8.16 (registry+https://github.com/rust-lang/crates.io-index)" = "0535f350c60aac0b87ccf28319abc749391e912192255b0c00a2c12c6917bd73"
 "checksum env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0561146661ae44c579e993456bc76d11ce1e0c7d745e57b2fa7146b6e49fa2ad"
--- a/accessible/base/FocusManager.cpp
+++ b/accessible/base/FocusManager.cpp
@@ -94,16 +94,20 @@ FocusManager::FocusDisposition FocusMana
     if (child == focus) return eContainedByFocus;
 
     child = child->Parent();
   }
 
   return eNone;
 }
 
+bool FocusManager::WasLastFocused(const Accessible* aAccessible) const {
+  return mLastFocus == aAccessible;
+}
+
 void FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) {
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eFocus))
     logging::FocusNotificationTarget("DOM focus", "Target", aTarget);
 #endif
 
   mActiveItem = nullptr;
 
@@ -335,16 +339,17 @@ void FocusManager::ProcessFocusEvent(Acc
   // Reset cached caret value. The cache will be updated upon processing the
   // next caret move event. This ensures that we will return the correct caret
   // offset before the caret move event is handled.
   SelectionMgr()->ResetCaretOffset();
 
   RefPtr<AccEvent> focusEvent = new AccEvent(nsIAccessibleEvent::EVENT_FOCUS,
                                              target, aEvent->FromUserInput());
   nsEventShell::FireEvent(focusEvent);
+  mLastFocus = target;
 
   // Fire scrolling_start event when the document receives the focus if it has
   // an anchor jump. If an accessible within the document receive the focus
   // then null out the anchor jump because it no longer applies.
   DocAccessible* targetDocument = target->Document();
   Accessible* anchorJump = targetDocument->AnchorJump();
   if (anchorJump) {
     if (target == targetDocument) {
--- a/accessible/base/FocusManager.h
+++ b/accessible/base/FocusManager.h
@@ -65,16 +65,28 @@ class FocusManager {
 
   /**
    * Return whether the given accessible is focused or contains the focus or
    * contained by focused accessible.
    */
   enum FocusDisposition { eNone, eFocused, eContainsFocus, eContainedByFocus };
   FocusDisposition IsInOrContainsFocus(const Accessible* aAccessible) const;
 
+  /**
+   * Return true if the given accessible was the last accessible focused.
+   * This is useful to detect the case where the last focused accessible was
+   * removed before something else was focused. This can happen in one of two
+   * ways:
+   * 1. The DOM focus was removed. DOM doesn't fire a blur event when this
+   *    happens; see bug 559561.
+   * 2. The accessibility focus was an active item (e.g. aria-activedescendant)
+   *    and that item was removed.
+   */
+  bool WasLastFocused(const Accessible* aAccessible) const;
+
   //////////////////////////////////////////////////////////////////////////////
   // Focus notifications and processing (don't use until you know what you do).
 
   /**
    * Called when DOM focus event is fired.
    */
   void NotifyOfDOMFocus(nsISupports* aTarget);
 
@@ -119,15 +131,16 @@ class FocusManager {
 
   /**
    * Return DOM document having DOM focus.
    */
   dom::Document* FocusedDOMDocument() const;
 
  private:
   RefPtr<Accessible> mActiveItem;
+  RefPtr<Accessible> mLastFocus;
   RefPtr<Accessible> mActiveARIAMenubar;
 };
 
 }  // namespace a11y
 }  // namespace mozilla
 
 #endif
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -1211,19 +1211,19 @@ void DocAccessible::BindToDocument(Acces
     }
   }
 }
 
 void DocAccessible::UnbindFromDocument(Accessible* aAccessible) {
   NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
                "Unbinding the unbound accessible!");
 
-  // Fire focus event on accessible having DOM focus if active item was removed
+  // Fire focus event on accessible having DOM focus if last focus was removed
   // from the tree.
-  if (FocusMgr()->IsActiveItem(aAccessible)) {
+  if (FocusMgr()->WasLastFocused(aAccessible)) {
     FocusMgr()->ActiveItemChanged(nullptr);
 #ifdef A11Y_LOG
     if (logging::IsEnabled(logging::eFocus))
       logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
 #endif
   }
 
   // Remove an accessible from node-to-accessible map if it exists there.
--- a/accessible/tests/mochitest/events/a11y.ini
+++ b/accessible/tests/mochitest/events/a11y.ini
@@ -29,16 +29,17 @@ skip-if = os == 'win' || os == 'linux'
 [test_focus_contextmenu.xul]
 [test_focus_controls.html]
 [test_focus_doc.html]
 [test_focus_general.html]
 [test_focus_general.xul]
 [test_focus_listcontrols.xul]
 [test_focus_menu.xul]
 [test_focus_name.html]
+[test_focus_removal.html]
 [test_focus_selects.html]
 [test_focus_tabbox.xul]
 skip-if = webrender
 [test_focus_tree.xul]
 [test_fromUserInput.html]
 [test_label.xul]
 [test_menu.xul]
 [test_mutation.html]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_removal.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+  <title>Test removal of focused accessible</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="../events.js"></script>
+
+  <script type="application/javascript">
+    async function setFocus(aNodeToFocus, aExpectedFocus) {
+      let expected = aExpectedFocus || aNodeToFocus;
+      let focused = waitForEventPromise(EVENT_FOCUS, expected);
+      info("Focusing " + aNodeToFocus.id);
+      aNodeToFocus.focus();
+      await focused;
+      ok(true, expected.id + " focused after " +
+        aNodeToFocus.id + ".focus()");
+    }
+
+    async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus) {
+      let focused = waitForEventPromise(EVENT_FOCUS, aExpectedFocus);
+      info("Removing " + aNodeToRemove.id);
+      aNodeToRemove.remove();
+      await focused;
+      let friendlyExpected = aExpectedFocus == document ?
+        "document" : aExpectedFocus.id;
+      ok(true, friendlyExpected + " focused after " +
+        aNodeToRemove.id + " removed");
+    }
+
+    async function doTests() {
+      info("Testing removal of focused node itself");
+      let button = getNode("button");
+      await setFocus(button);
+      await expectFocusAfterRemove(button, document);
+
+      info("Testing removal of focused node's parent");
+      let dialog = getNode("dialog");
+      let dialogButton = getNode("dialogButton");
+      await setFocus(dialogButton);
+      await expectFocusAfterRemove(dialog, document);
+
+      info("Testing removal of aria-activedescendant target");
+      let listbox = getNode("listbox");
+      let option = getNode("option");
+      await setFocus(listbox, option);
+      await expectFocusAfterRemove(option, listbox);
+
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTests);
+  </script>
+</head>
+
+<body>
+
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+
+  <button id="button"></button>
+
+  <div role="dialog" id="dialog">
+    <button id="dialogButton"></button>
+  </div>
+
+  <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option">
+    <div role="option" id="option"></div>
+  </div>
+
+</body>
+</html>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1634,16 +1634,21 @@ pref("browser.contentblocking.rejecttrac
 pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
 
 pref("browser.contentblocking.introCount", 0);
 
 pref("browser.contentblocking.maxIntroCount", 5);
 // 1800 = 30 min in seconds
 pref("browser.contentblocking.introDelaySeconds", 1800);
 
+// Enables the new Protections Panel.
+#ifdef NIGHTLY_BUILD
+pref("browser.protections_panel.enabled", true);
+#endif
+
 pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/content-blocking/start/");
 
 // 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);
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -347,23 +347,20 @@ var gXPInstallObserver = {
     Services.telemetry
             .getHistogramById("SECURITY_UI")
             .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
   },
 
   // IDs of addon install related notifications
   NOTIFICATION_IDS: [
     "addon-install-blocked",
-    "addon-install-blocked-silent",
     "addon-install-complete",
     "addon-install-confirmation",
-    "addon-install-disabled",
     "addon-install-failed",
     "addon-install-origin-blocked",
-    "addon-install-started",
     "addon-progress",
     "addon-webext-permissions",
     "xpinstall-disabled",
   ],
 
   // Remove all opened addon installation notifications
   removeAllNotifications(browser) {
     this.NOTIFICATION_IDS.forEach((id) => {
@@ -578,16 +575,26 @@ var gXPInstallObserver = {
         } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
           error += "Blocklisted";
           args = [install.name];
         } else {
           error += "Incompatible";
           args = [brandShortName, Services.appinfo.version, install.name];
         }
 
+        if (install.addon && !Services.policies.mayInstallAddon(install.addon)) {
+          error = "addonInstallBlockedByPolicy";
+          let extensionSettings = Services.policies.getExtensionSettings(install.addon.id);
+          let message = "";
+          if (extensionSettings && "blocked_install_message" in extensionSettings) {
+            message = " " + extensionSettings.blocked_install_message;
+          }
+          args = [install.name, install.addon.id, message];
+        }
+
         // Add Learn More link when refusing to install an unsigned add-on
         if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
           options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
         }
 
         messageString = gNavigatorBundle.getFormattedString(error, args);
 
         PopupNotifications.show(browser, notificationID, messageString, anchorID,
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -227,16 +227,23 @@ var gIdentityHandler = {
     return this._insecureConnectionTextEnabled;
   },
   get _insecureConnectionTextPBModeEnabled() {
     delete this._insecureConnectionTextPBModeEnabled;
     XPCOMUtils.defineLazyPreferenceGetter(this, "_insecureConnectionTextPBModeEnabled",
                                           "security.insecure_connection_text.pbmode.enabled");
     return this._insecureConnectionTextPBModeEnabled;
   },
+  get _protectionsPanelEnabled() {
+    delete this._protectionsPanelEnabled;
+    XPCOMUtils.defineLazyPreferenceGetter(this, "_protectionsPanelEnabled",
+                                          "browser.protections_panel.enabled",
+                                          false);
+    return this._protectionsPanelEnabled;
+  },
 
   /**
    * Handles clicks on the "Clear Cookies and Site Data" button.
    */
   async clearSiteData(event) {
     if (!this._uriHasHost) {
       return;
     }
@@ -827,16 +834,24 @@ var gIdentityHandler = {
       // NetUtil's methods will throw for malformed URIs and the like
     }
   },
 
   /**
    * Click handler for the identity-box element in primary chrome.
    */
   handleIdentityButtonEvent(event) {
+    // For Nightly users, show the WIP protections panel if the tracking
+    // protection icon was clicked.
+    if (this._protectionsPanelEnabled &&
+        event.originalTarget.id == "tracking-protection-icon-animatable-image") {
+      gProtectionsHandler.handleProtectionsButtonEvent(event);
+      return;
+    }
+
     event.stopPropagation();
 
     if ((event.type == "click" && event.button != 0) ||
         (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
          event.keyCode != KeyEvent.DOM_VK_RETURN)) {
       return; // Left click, space or enter only
     }
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-siteProtections.js
@@ -0,0 +1,39 @@
+/* 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/browser-window */
+
+/**
+ * Utility object to handle manipulations of the protections indicators in the UI
+ */
+var gProtectionsHandler = {
+  // smart getters
+  get _protectionsPopup() {
+    delete this._protectionsPopup;
+    return this._protectionsPopup = document.getElementById("protections-popup");
+  },
+  get _protectionsIconBox() {
+    delete this._protectionsIconBox;
+    return this._protectionsIconBox = document.getElementById("tracking-protection-icon-animatable-box");
+  },
+
+  handleProtectionsButtonEvent(event) {
+    event.stopPropagation();
+    if ((event.type == "click" && event.button != 0) ||
+        (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
+         event.keyCode != KeyEvent.DOM_VK_RETURN)) {
+      return; // Left click, space or enter only
+    }
+
+    // Make sure that the display:none style we set in xul is removed now that
+    // the popup is actually needed
+    this._protectionsPopup.hidden = false;
+
+    // Now open the popup, anchored off the primary chrome element
+    PanelMultiView.openPopup(this._protectionsPopup, this._protectionsIconBox, {
+      position: "bottomcenter topleft",
+      triggerEvent: event,
+    }).catch(Cu.reportError);
+  },
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -108,16 +108,18 @@ XPCOMUtils.defineLazyScriptGetter(this, 
 XPCOMUtils.defineLazyScriptGetter(this, "ctrlTab",
                                   "chrome://browser/content/browser-ctrlTab.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["CustomizationHandler", "AutoHideMenubar"],
                                   "chrome://browser/content/browser-customization.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PointerLock", "FullScreen"],
                                   "chrome://browser/content/browser-fullScreenAndPointerLock.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gIdentityHandler",
                                   "chrome://browser/content/browser-siteIdentity.js");
+XPCOMUtils.defineLazyScriptGetter(this, "gProtectionsHandler",
+                                  "chrome://browser/content/browser-siteProtections.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["gGestureSupport", "gHistorySwipeAnimation"],
                                   "chrome://browser/content/browser-gestureSupport.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gSafeBrowsing",
                                   "chrome://browser/content/browser-safebrowsing.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gSync",
                                   "chrome://browser/content/browser-sync.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gBrowserThumbnails",
                                   "chrome://browser/content/browser-thumbnails.js");
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -603,17 +603,18 @@
 #else
       <description class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/>
 #endif
     </tooltip>
 
 #include popup-notifications.inc
 
 #include ../../components/customizableui/content/panelUI.inc.xul
-#include ../../components/controlcenter/content/panel.inc.xul
+#include ../../components/controlcenter/content/identityPanel.inc.xul
+#include ../../components/controlcenter/content/protectionsPanel.inc.xul
 #include ../../components/downloads/content/downloadsPanel.inc.xul
 #include browser-allTabsMenu.inc.xul
 
     <hbox id="downloads-animation-container" mousethrough="always">
       <vbox id="downloads-notification-anchor" hidden="true">
         <vbox id="downloads-indicator-notification"/>
       </vbox>
     </hbox>
@@ -846,17 +847,16 @@
                      defaultPlaceholder="&urlbar.placeholder2;"
                      focused="true"
                      type="autocomplete"
                      autocompletesearch="unifiedcomplete"
                      autocompletesearchparam="enable-actions"
                      autocompletepopup="PopupAutoCompleteRichResult"
                      completeselectedindex="true"
                      tabscrolling="true"
-                     newlines="stripsurroundingwhitespace"
                      ontextentered="this.handleCommand(param);"
                      ontextreverted="return this.handleRevert();"
                      pageproxystate="invalid">
               <!-- Use onclick instead of normal popup= syntax since the popup
                    code fires onmousedown, and hence eats our favicon drag events. -->
               <box id="identity-box" role="button"
                    align="center"
                    aria-label="&urlbar.viewSiteInfo.label;"
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1601,16 +1601,17 @@ window._gBrowser = {
   },
 
   updateBrowserRemoteness(aBrowser, {
     newFrameloader,
     opener,
     remoteType,
     sameProcessAsFrameLoader,
     recordExecution,
+    replaceBrowsingContext,
   } = {}) {
     let isRemote = aBrowser.getAttribute("remote") == "true";
 
     // We have to be careful with this here, as the "no remote type" is null,
     // not a string. Make sure to check only for undefined, since null is
     // allowed.
     if (remoteType === undefined) {
       throw new Error("Remote type must be set!");
@@ -1721,17 +1722,17 @@ window._gBrowser = {
     }
 
     if (!Services.prefs.getBoolPref("fission.rebuild_frameloaders_on_remoteness_change", false)) {
       parent.appendChild(aBrowser);
     } else {
       // This call actually switches out our frameloaders. Do this as late as
       // possible before rebuilding the browser, as we'll need the new browser
       // state set up completely first.
-      aBrowser.changeRemoteness({ remoteType });
+      aBrowser.changeRemoteness({ remoteType, replaceBrowsingContext });
       // Once we have new frameloaders, this call sets the browser back up.
       //
       // FIXME(emilio): Shouldn't we call destroy() first? What hides the
       // select pop-ups and such otherwise?
       aBrowser.construct();
     }
 
     aBrowser.userTypedValue = oldUserTypedValue;
--- a/browser/base/content/test/contextMenu/browser.ini
+++ b/browser/base/content/test/contextMenu/browser.ini
@@ -1,15 +1,28 @@
 [DEFAULT]
 support-files =
-  !/browser/base/content/test/general/contextmenu_common.js
   subtst_contextmenu_webext.html
   test_contextmenu_links.html
+  subtst_contextmenu.html
+  subtst_contextmenu_input.html
+  subtst_contextmenu_xul.xul
+  ctxmenu-image.png
+  ../general/head.js
+  ../general/video.ogg
+  ../general/audio.ogg
+  contextmenu_common.js
 
 [browser_contextmenu_touch.js]
 skip-if = !(os == 'win' && os_version == '10.0')
 [browser_contextmenu_linkopen.js]
 [browser_contextmenu_iframe.js]
 support-files =
   test_contextmenu_iframe.html
 [browser_utilityOverlay.js]
 skip-if = os == "linux" || os == "mac" #Bug 1444631
 [browser_utilityOverlayPrincipal.js]
+[browser_contextmenu_childprocess.js]
+[browser_contextmenu.js]
+tags = fullscreen clipboard
+skip-if = toolkit == "gtk3" || verify || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1531590
+[browser_contextmenu_input.js]
+skip-if = toolkit == "gtk3" || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1533161
rename from browser/base/content/test/general/browser_contextmenu.js
rename to browser/base/content/test/contextMenu/browser_contextmenu.js
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu.js
@@ -8,22 +8,26 @@ let LOGIN_FILL_ITEMS = [
       "fill-login-no-logins", false,
       "---", null,
       "fill-login-saved-passwords", true,
     ], null,
 ];
 let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
 let hasContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
 
-const example_base = "http://example.com/browser/browser/base/content/test/general/";
-const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+const example_base = "http://example.com/browser/browser/base/content/test/contextMenu/";
+const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+const head_base = "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
 
 /* import-globals-from contextmenu_common.js */
 Services.scriptloader.loadSubScript(chrome_base + "contextmenu_common.js", this);
 
+/* import-globals-from ../general/head.js */
+Services.scriptloader.loadSubScript(head_base + "head.js", this);
+
 add_task(async function init() {
   // Ensure screenshots is really disabled (bug 1498738)
   const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
   await addon.disable({allowSystemAddons: true});
 });
 
 // Below are test cases for XUL element
 add_task(async function test_xul_text_link_label() {
rename from browser/base/content/test/general/browser_contextmenu_childprocess.js
rename to browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js
--- a/browser/base/content/test/general/browser_contextmenu_childprocess.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const gBaseURL = "https://example.com/browser/browser/base/content/test/general/";
+const gBaseURL = "https://example.com/browser/browser/base/content/test/contextMenu/";
 
 add_task(async function() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, gBaseURL + "subtst_contextmenu.html");
 
   let contextMenu = document.getElementById("contentAreaContextMenu");
 
   // Get the point of the element with the page menu (test-pagemenu) and
   // synthesize a right mouse click there.
rename from browser/base/content/test/general/browser_contextmenu_input.js
rename to browser/base/content/test/contextMenu/browser_contextmenu_input.js
--- a/browser/base/content/test/general/browser_contextmenu_input.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_input.js
@@ -1,19 +1,19 @@
 "use strict";
 
 let contextMenu;
 let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
 
 add_task(async function test_setup() {
-  const example_base = "http://example.com/browser/browser/base/content/test/general/";
+  const example_base = "http://example.com/browser/browser/base/content/test/contextMenu/";
   const url = example_base + "subtst_contextmenu_input.html";
   await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
 
-  const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+  const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
   const contextmenu_common = chrome_base + "contextmenu_common.js";
   /* import-globals-from contextmenu_common.js */
   Services.scriptloader.loadSubScript(contextmenu_common, this);
 
   // Ensure screenshots is really disabled (bug 1498738)
   const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
   await addon.disable({allowSystemAddons: true});
 });
rename from browser/base/content/test/general/contextmenu_common.js
rename to browser/base/content/test/contextMenu/contextmenu_common.js
rename from browser/base/content/test/general/ctxmenu-image.png
rename to browser/base/content/test/contextMenu/ctxmenu-image.png
rename from browser/base/content/test/general/subtst_contextmenu.html
rename to browser/base/content/test/contextMenu/subtst_contextmenu.html
rename from browser/base/content/test/general/subtst_contextmenu_input.html
rename to browser/base/content/test/contextMenu/subtst_contextmenu_input.html
rename from browser/base/content/test/general/subtst_contextmenu_xul.xul
rename to browser/base/content/test/contextMenu/subtst_contextmenu_xul.xul
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -20,18 +20,16 @@ support-files =
   browser_web_channel.html
   browser_web_channel_iframe.html
   bug592338.html
   bug792517-2.html
   bug792517.html
   bug792517.sjs
   bug839103.css
   clipboard_pastefile.html
-  contextmenu_common.js
-  ctxmenu-image.png
   discovery.html
   download_page.html
   download_page_1.txt
   download_page_2.txt
   dummy_page.html
   file_documentnavigation_frameset.html
   file_double_close_tab.html
   file_fullscreen-window-open.html
@@ -41,19 +39,16 @@ support-files =
   navigating_window_with_download.html
   page_style_sample.html
   pinning_headers.sjs
   ssl_error_reports.sjs
   print_postdata.sjs
   searchSuggestionEngine.sjs
   searchSuggestionEngine.xml
   searchSuggestionEngine2.xml
-  subtst_contextmenu.html
-  subtst_contextmenu_input.html
-  subtst_contextmenu_xul.xul
   test_bug462673.html
   test_bug628179.html
   test_bug839103.html
   test_process_flags_chrome.html
   title_test.svg
   unknownContentType_file.pif
   unknownContentType_file.pif^headers^
   video.ogg
@@ -242,23 +237,16 @@ tags = clipboard
 [browser_clipboard_pastefile.js]
 skip-if = true # Disabled due to the clipboard not supporting real file types yet (bug 1288773)
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_contentAreaClick.js]
 skip-if = e10s # Clicks in content don't go through contentAreaClick with e10s.
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_contentAltClick.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_contextmenu.js]
-tags = fullscreen clipboard
-skip-if = toolkit == "gtk3" || verify || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1531590
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_contextmenu_input.js]
-skip-if = toolkit == "gtk3" || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1533161
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_ctrlTab.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_datachoices_notification.js]
 skip-if = !datareporting || (verify && !debug && (os == 'win'))
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_decoderDoctor.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_search_discovery.js]
@@ -447,18 +435,16 @@ uses-unsafe-cpows = true
 tags = psm
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_windowactivation.js]
 skip-if = verify
 support-files =
   file_window_activation.html
   file_window_activation2.html
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_contextmenu_childprocess.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug963945.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_domFullscreen_fullscreenMode.js]
 tags = fullscreen
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_newTabDrop.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -34,14 +34,14 @@ skip-if = (os == 'win') || (os == 'mac')
 [browser_tabopen.js]
 skip-if = (verify && (os == 'mac'))
 [browser_tabopen_squeeze.js]
 [browser_tabstrip_overflow_underflow.js]
 skip-if = (verify && !debug && (os == 'win')) || (!debug && (os == 'win') && (bits == 32)) # Bug 1502255
 [browser_tabswitch.js]
 [browser_toolbariconcolor_restyles.js]
 [browser_urlbar_keyed_search.js]
-skip-if = (os == 'linux') || (os == 'win' && debug) || (os == 'win' && bits == 32) # Disabled on Linux and Windows debug due to perma failures. Bug 1392320. Disabled on Win32 because of intermittent OOM failures (bug 1448241).
+skip-if = (os == 'win' && bits == 32) # Disabled on Win32 because of intermittent OOM failures (bug 1448241).
 [browser_urlbar_search.js]
 skip-if = (debug || ccov) && (os == 'linux' || os == 'win') || (os == 'win' && bits == 32) # Disabled on Linux and Windows debug and ccov due to intermittent timeouts. Bug 1414126, bug 1426611. Disabled on Win32 because of intermittent OOM failures (bug 1448241)
 [browser_window_resize.js]
 [browser_windowclose.js]
 [browser_windowopen.js]
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -52,17 +52,17 @@ file, You can obtain one at http://mozil
                       tooltiptext="&urlbar.openHistoryPopup.tooltip;"
                       allowevents="true"
                       xbl:inherits="open,parentfocused=focused,usertyping"/>
       <children includes="hbox"/>
     </content>
   </binding>
 
   <binding id="legacy-urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
-    <content>
+    <content newlines="stripsurroundingwhitespace">
       <children includes="box"/>
       <xul:moz-input-box anonid="moz-input-box"
                          tooltip="aHTMLTooltip"
                          class="urlbar-input-box"
                          flex="1">
         <children/>
         <html:input anonid="scheme"
                     class="urlbar-scheme textbox-input"
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -47,16 +47,17 @@ browser.jar:
         content/browser/browser-gestureSupport.js     (content/browser-gestureSupport.js)
         content/browser/browser-media.js              (content/browser-media.js)
         content/browser/browser-pageActions.js        (content/browser-pageActions.js)
         content/browser/browser-places.js             (content/browser-places.js)
         content/browser/browser-plugins.js            (content/browser-plugins.js)
         content/browser/browser-safebrowsing.js       (content/browser-safebrowsing.js)
         content/browser/browser-sidebar.js            (content/browser-sidebar.js)
         content/browser/browser-siteIdentity.js       (content/browser-siteIdentity.js)
+        content/browser/browser-siteProtections.js    (content/browser-siteProtections.js)
         content/browser/browser-sync.js               (content/browser-sync.js)
         content/browser/browser-tabsintitlebar.js     (content/browser-tabsintitlebar.js)
         content/browser/browser-toolbarKeyNav.js      (content/browser-toolbarKeyNav.js)
         content/browser/browser-thumbnails.js         (content/browser-thumbnails.js)
         content/browser/browser-graphics-utils.js     (content/browser-graphics-utils.js)
         content/browser/tab-content.js                (content/tab-content.js)
         content/browser/content.js                    (content/content.js)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -31,16 +31,17 @@ let ACTORS = {
 
 let LEGACY_ACTORS = {
   AboutLogins: {
     child: {
       matches: ["about:logins"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
         "AboutLoginsDeleteLogin": {wantUntrusted: true},
+        "AboutLoginsOpenSite": {wantUntrusted: true},
         "AboutLoginsUpdateLogin": {wantUntrusted: true},
         "AboutLoginsInit": {wantUntrusted: true},
       },
       messages: [
         "AboutLogins:AllLogins",
         "AboutLogins:LoginAdded",
         "AboutLogins:LoginModified",
         "AboutLogins:LoginRemoved",
@@ -541,18 +542,19 @@ const listeners = {
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init
 
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
+    "AboutLogins:OpenSite": ["AboutLoginsParent"],
+    "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
-    "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
     "FormValidation:HidePopup": ["FormValidationHandler"],
     "PictureInPicture:Request": ["PictureInPicture"],
     "PictureInPicture:Close": ["PictureInPicture"],
     "PictureInPicture:Playing": ["PictureInPicture"],
     "PictureInPicture:Paused": ["PictureInPicture"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -25,16 +25,20 @@ class AboutLoginsChild extends ActorChil
           cloneFunctions: true,
         });
         break;
       }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
         break;
       }
+      case "AboutLoginsOpenSite": {
+        this.mm.sendAsyncMessage("AboutLogins:OpenSite", {login: event.detail});
+        break;
+      }
       case "AboutLoginsUpdateLogin": {
         this.mm.sendAsyncMessage("AboutLogins:UpdateLogin", {login: event.detail});
         break;
       }
     }
   }
 
   receiveMessage(message) {
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -56,16 +56,27 @@ var AboutLoginsParent = {
     }
 
     switch (message.name) {
       case "AboutLogins:DeleteLogin": {
         let login = LoginHelper.vanillaObjectToLogin(message.data.login);
         Services.logins.removeLogin(login);
         break;
       }
+      case "AboutLogins:OpenSite": {
+        let guid = message.data.login.guid;
+        let logins = LoginHelper.searchLoginsWithObject({guid});
+        if (!logins || logins.length != 1) {
+          log.warn(`AboutLogins:OpenSite: expected to find a login for guid: ${guid} but found ${(logins || []).length}`);
+          return;
+        }
+
+        message.target.ownerGlobal.openWebLinkIn(logins[0].hostname, "tab", {relatedToCurrent: true});
+        break;
+      }
       case "AboutLogins:Subscribe": {
         if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
           Services.obs.addObserver(this, "passwordmgr-storage-changed");
         }
         this._subscribers.add(message.target);
 
         let messageManager = message.target.messageManager;
         messageManager.sendAsyncMessage("AboutLogins:AllLogins", this.getAllLogins());
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/aboutLogins.css
@@ -0,0 +1,33 @@
+/* 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/. */
+
+body {
+  display: grid;
+  grid-template-columns: 280px 1fr;
+  grid-template-rows: 75px 1fr;
+  grid-template-areas: "header header"
+                       "logins login";
+  height: 100vh;
+}
+
+header {
+  display: flex;
+  grid-area: header;
+  background-color: var(--in-content-box-background);
+  border-bottom: 1px solid var(--in-content-box-border-color);
+}
+
+login-filter {
+  flex: auto;
+  align-self: center;
+}
+
+login-list {
+  grid-area: logins;
+}
+
+login-item {
+  grid-area: login;
+  max-width: 800px;
+}
--- a/browser/components/aboutlogins/content/aboutLogins.ftl
+++ b/browser/components/aboutlogins/content/aboutLogins.ftl
@@ -7,21 +7,31 @@
 ### and strings are likely to change often.
 
 ### Fluent isn't translating elements in the shadow DOM so the translated strings
 ### need to be applied to the composed node where they can be moved to the proper
 ### descendant after translation.
 
 about-logins-page-title = Login Manager
 
+login-filter =
+  .placeholder = Search Logins
+
 login-list =
-  .login-list-header = Logins
+  .count =
+    { $count ->
+        [one] { $count } entry
+       *[other] { $count } entries
+    }
 
 login-item =
   .cancel-button = Cancel
   .delete-button = Delete
+  .edit-button = Edit
   .hostname-label = Website Address
+  .modal-input-reveal-button = Toggle password visibility
+  .open-site-button = Launch
   .password-label = Password
   .save-changes-button = Save Changes
   .time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
   .time-changed = Last changed: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
   .time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
   .username-label = Username
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -4,66 +4,107 @@
 
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"/>
     <title data-l10n-id="about-logins-page-title"></title>
     <link rel="localization" href="browser/aboutLogins.ftl">
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/reflected-fluent-element.js"></script>
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/modal-input.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
     <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+    <link rel="stylesheet" type="text/css" href="chrome://browser/content/aboutlogins/aboutLogins.css">
   </head>
   <body>
+    <header>
+      <login-filter data-l10n-id="login-filter"
+                    data-l10n-attrs="placeholder"></login-filter>
+    </header>
     <login-list data-l10n-id="login-list"
-                data-l10n-attrs="login-list-header"></login-list>
+                data-l10n-attrs="count"
+                data-l10n-args='{"count": 0}'></login-list>
     <login-item data-l10n-id="login-item"
                 data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
                 data-l10n-attrs="cancel-button,
                                  delete-button,
+                                 edit-button,
                                  hostname-label,
+                                 modal-input-reveal-button,
+                                 open-site-button,
                                  password-label,
                                  save-changes-button,
                                  time-created,
                                  time-changed,
                                  time-used,
                                  username-label"></login-item>
 
     <template id="login-list-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list.css">
-      <h2></h2>
+      <div class="meta">
+        <span class="count"></span>
+      </div>
       <ol>
       </ol>
     </template>
 
     <template id="login-list-item-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list-item.css">
       <span class="hostname"></span>
       <span class="username"></span>
     </template>
 
     <template id="login-item-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
-      <h2 class="header"></h2>
-      <button class="delete-button"></button>
-      <label>
-        <span class="hostname-label field-label"></span>
-        <span class="hostname"/>
-      </label>
-      <label>
-        <span class="username-label field-label"></span>
-        <input name="username"/>
-      </label>
-      <label>
-        <span class="password-label field-label"></span>
-        <input type="password" name="password"/>
-      </label>
+      <div class="header">
+        <h2 class="title"></h2>
+        <button class="edit-button"></button>
+        <button class="delete-button"></button>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="hostname-label field-label"></span>
+          <span class="hostname"/>
+        </label>
+        <button class="open-site-button"></button>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="username-label field-label"></span>
+          <modal-input name="username"/>
+        </label>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="password-label field-label"></span>
+          <modal-input type="password" name="password"/>
+        </label>
+      </div>
       <p class="time-created meta-info"></p>
       <p class="time-changed meta-info"></p>
       <p class="time-used meta-info"></p>
       <button class="save-changes-button"></button>
       <button class="cancel-button"></button>
     </template>
+
+    <template id="login-filter-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-filter.css">
+      <input type="text"/>
+    </template>
+
+    <template id="modal-input-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/modal-input.css">
+      <span class="locked-value"></span>
+      <input type="text" class="unlocked-value"/>
+      <button class="reveal-button"/>
+    </template>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/login-filter.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+:host {
+  display: flex;
+}
+
+input {
+  margin: 18px;
+  flex: auto;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/login-filter.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+/* globals ReflectedFluentElement */
+
+class LoginFilter extends ReflectedFluentElement {
+  connectedCallback() {
+    if (this.children.length) {
+      return;
+    }
+
+    let loginFilterTemplate = document.querySelector("#login-filter-template");
+    this.attachShadow({mode: "open"})
+        .appendChild(loginFilterTemplate.content.cloneNode(true));
+
+    this.reflectFluentStrings();
+
+    this.addEventListener("input", this);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "input": {
+        this.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+          bubbles: true,
+          composed: true,
+          detail: event.originalTarget.value,
+        }));
+        break;
+      }
+    }
+  }
+
+  static get reflectedFluentIDs() {
+    return ["placeholder"];
+  }
+
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
+  }
+
+  handleSpecialCaseFluentString(attrName) {
+    if (attrName != "placeholder") {
+      return false;
+    }
+
+    this.shadowRoot.querySelector("input").placeholder = this.getAttribute(attrName);
+    return true;
+  }
+}
+customElements.define("login-filter", LoginFilter);
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -1,27 +1,61 @@
 /* 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/. */
 
-h2 {
-  border-bottom: 1px solid var(--grey-30);
+:host {
+  padding-top: 36px;
+  padding-left: 40px;
+  padding-right: 40px;
+}
+
+:host(:not([editing])) .save-changes-button,
+:host(:not([editing])) .cancel-button {
+  display: none;
+}
+
+.header {
+  display: flex;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+}
+
+.title {
+  margin-top: 0;
+  margin-bottom: 0;
+  flex: auto;
+}
+
+.detail-row {
+  display: flex;
+  margin-bottom: 20px;
+}
+
+.detail-row > label {
+  flex: auto;
+}
+
+.detail-row > button {
+  align-self: end;
 }
 
 .field-label {
   display: block;
+  font-size: smaller;
+  opacity: .7;
+  margin-bottom: 5px;
 }
 
 .meta-info {
   font-size: smaller;
 }
 
 .meta-info:not(:first-of-type) {
   margin-top: 0;
 }
 
 .meta-info:not(:last-of-type) {
   margin-bottom: 0;
 }
 
 .meta-info:first-of-type {
-  border-top: 1px solid var(--grey-30);
+  border-top: 1px solid var(--in-content-box-border-color);
 }
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -1,117 +1,139 @@
 /* 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/. */
 
-class LoginItem extends HTMLElement {
+/* globals ReflectedFluentElement */
+
+class LoginItem extends ReflectedFluentElement {
   constructor() {
     super();
     this._login = {};
   }
 
   connectedCallback() {
     if (this.children.length) {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginItemTemplate.content.cloneNode(true));
 
+    this.reflectFluentStrings();
+
     for (let selector of [
       ".delete-button",
+      ".edit-button",
+      ".open-site-button",
       ".save-changes-button",
       ".cancel-button",
     ]) {
       let button = this.shadowRoot.querySelector(selector);
       button.addEventListener("click", this);
     }
 
     window.addEventListener("AboutLoginsLoginSelected", this);
 
     this.render();
   }
 
-  static get observedAttributes() {
+  static get reflectedFluentIDs() {
     return [
       "cancel-button",
       "delete-button",
+      "edit-button",
       "hostname-label",
+      "modal-input-reveal-button",
+      "open-site-button",
       "password-label",
       "save-changes-button",
       "time-created",
       "time-changed",
       "time-used",
       "username-label",
     ];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
+  }
+
+  handleSpecialCaseFluentString(attrName) {
+    if (attrName != "modal-input-reveal-button") {
+      return false;
     }
 
-    // Strings that are reflected to their shadowed element are assigned
-    // to an attribute name that matches a className on the element.
-    let shadowedElement = this.shadowRoot.querySelector("." + attr);
-    shadowedElement.textContent = newValue;
+    this.shadowRoot.querySelector("modal-input[name='password']")
+                   .setAttribute("reveal-button", this.getAttribute(attrName));
+    return true;
   }
 
   render() {
     let l10nArgs = {
       timeCreated: this._login.timeCreated || "",
       timeChanged: this._login.timePasswordChanged || "",
       timeUsed: this._login.timeLastUsed || "",
     };
     document.l10n.setAttributes(this, "login-item", l10nArgs);
     let hostnameNoScheme = this._login.hostname && new URL(this._login.hostname).hostname;
-    this.shadowRoot.querySelector(".header").textContent = hostnameNoScheme || "";
+    this.shadowRoot.querySelector(".title").textContent = hostnameNoScheme || "";
     this.shadowRoot.querySelector(".hostname").textContent = this._login.hostname || "";
-    this.shadowRoot.querySelector("input[name='username']").value = this._login.username || "";
-    this.shadowRoot.querySelector("input[name='password']").value = this._login.password || "";
+    this.shadowRoot.querySelector("modal-input[name='username']").setAttribute("value", this._login.username || "");
+    this.shadowRoot.querySelector("modal-input[name='password']").setAttribute("value", this._login.password || "");
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "AboutLoginsLoginSelected": {
         this.setLogin(event.detail);
         break;
       }
       case "click": {
+        if (event.target.classList.contains("cancel-button")) {
+          this.toggleEditing();
+          this.render();
+          return;
+        }
         if (event.target.classList.contains("delete-button")) {
           document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
             bubbles: true,
             detail: this._login,
           }));
           return;
         }
+        if (event.target.classList.contains("edit-button")) {
+          this.toggleEditing();
+          return;
+        }
+        if (event.target.classList.contains("open-site-button")) {
+          document.dispatchEvent(new CustomEvent("AboutLoginsOpenSite", {
+            bubbles: true,
+            detail: this._login,
+          }));
+          return;
+        }
         if (event.target.classList.contains("save-changes-button")) {
           let loginUpdates = {
             guid: this._login.guid,
           };
-          let formUsername = this.shadowRoot.querySelector("input[name='username']").value.trim();
+          let formUsername = this.shadowRoot.querySelector("modal-input[name='username']").value.trim();
           if (formUsername != this._login.username) {
             loginUpdates.username = formUsername;
           }
-          let formPassword = this.shadowRoot.querySelector("input[name='password']").value.trim();
+          let formPassword = this.shadowRoot.querySelector("modal-input[name='password']").value.trim();
           if (formPassword != this._login.password) {
             loginUpdates.password = formPassword;
           }
           document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
             bubbles: true,
             detail: loginUpdates,
           }));
-          return;
-        }
-        if (event.target.classList.contains("cancel-button")) {
-          this.render();
         }
         break;
       }
     }
   }
 
   setLogin(login) {
     this._login = login;
@@ -119,20 +141,29 @@ class LoginItem extends HTMLElement {
   }
 
   loginModified(login) {
     if (login.guid != this._login.guid) {
       return;
     }
 
     this._login = login;
+    this.toggleEditing(false);
     this.render();
   }
 
   loginRemoved(login) {
     if (login.guid != this._login.guid) {
       return;
     }
     this._login = {};
     this.render();
   }
+
+  toggleEditing(force) {
+    let shouldEdit = force !== undefined ? force : !this.hasAttribute("editing");
+    this.shadowRoot.querySelector(".edit-button").disabled = shouldEdit;
+    this.shadowRoot.querySelectorAll("modal-input")
+                   .forEach(el => el.toggleAttribute("editing", shouldEdit));
+    this.toggleAttribute("editing", shouldEdit);
+  }
 }
 customElements.define("login-item", LoginItem);
--- a/browser/components/aboutlogins/content/components/login-list-item.css
+++ b/browser/components/aboutlogins/content/components/login-list-item.css
@@ -1,37 +1,35 @@
 /* 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/. */
 
 :host {
-  border-inline-start: 4px solid transparent;
-  border-bottom: 1px solid var(--grey-30);
   display: block;
   padding: 10px;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+  border-inline-start: 4px solid transparent;
+}
+
+/* [hidden] isn't applying to elements in Shadow DOM. */
+:host([hidden]) {
+  display: none;
 }
 
 :host(:hover) {
-  background-color: var(--grey-90-a10);
+  background-color: var(--in-content-box-background-hover);
 }
 
 :host(:hover:active) {
-  background-color: var(--grey-90-a20);
+  background-color: var(--in-content-box-background-active);
 }
 
 :host(.selected) {
-  border-inline-start-color: var(--blue-40);
-  background-color: var(--grey-20);
-}
-
-:host(.selected:hover) {
-}
-
-:host(.selected:hover:active) {
-  background-color: var(--grey-30);
+  border-inline-start-color: var(--in-content-border-highlight);
+  background-color: var(--in-content-box-background-hover);
 }
 
 .hostname {
   font-weight: bold;
 }
 
 .hostname,
 .username {
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -1,7 +1,27 @@
 /* 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/. */
 
+:host {
+  border-inline-end: 1px solid var(--in-content-box-border-color);
+  background-color: var(--in-content-box-background);
+}
+
+.meta {
+  display: flex;
+  padding: 10px;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+  background-color: var(--in-content-box-info-background);
+}
+
+.count {
+  flex: auto;
+  text-align: end;
+  font-size: smaller;
+}
+
 ol {
+  margin-top: 0;
+  margin-bottom: 0;
   padding-inline-start: 0;
 }
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -1,72 +1,90 @@
 /* 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/. */
 
-/* globals LoginListItem */
+/* globals ReflectedFluentElement, LoginListItem */
 
-class LoginList extends HTMLElement {
+class LoginList extends ReflectedFluentElement {
   constructor() {
     super();
     this._logins = [];
     this._selectedItem = null;
   }
 
   connectedCallback() {
     if (this.children.length) {
       return;
     }
     let loginListTemplate = document.querySelector("#login-list-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginListTemplate.content.cloneNode(true));
+
+    this.reflectFluentStrings();
+
     this.render();
 
     window.addEventListener("AboutLoginsLoginSelected", this);
+    window.addEventListener("AboutLoginsFilterLogins", this);
   }
 
   render() {
     let list = this.shadowRoot.querySelector("ol");
     for (let login of this._logins) {
       list.append(new LoginListItem(login));
     }
+    document.l10n.setAttributes(this, "login-list", {count: this._logins.length});
   }
 
   handleEvent(event) {
     switch (event.type) {
+      case "AboutLoginsFilterLogins": {
+        let query = event.detail.toLocaleLowerCase();
+        let matchingLoginGuids;
+        if (query) {
+          matchingLoginGuids = this._logins.filter(login => {
+            return login.hostname.toLocaleLowerCase().includes(query) ||
+                   login.username.toLocaleLowerCase().includes(query);
+          }).map(login => login.guid);
+        } else {
+          matchingLoginGuids = this._logins.map(login => login.guid);
+        }
+        for (let listItem of this.shadowRoot.querySelectorAll("login-list-item")) {
+          if (matchingLoginGuids.includes(listItem.getAttribute("guid"))) {
+            if (listItem.hidden) {
+              listItem.hidden = false;
+            }
+          } else if (!listItem.hidden) {
+            listItem.hidden = true;
+          }
+        }
+        document.l10n.setAttributes(this, "login-list", {count: matchingLoginGuids.length});
+        break;
+      }
       case "AboutLoginsLoginSelected": {
         if (this._selectedItem) {
           if (this._selectedItem.getAttribute("guid") == event.detail.guid) {
             return;
           }
           this._selectedItem.classList.toggle("selected", false);
         }
         this._selectedItem = this.shadowRoot.querySelector(`login-list-item[guid="${event.detail.guid}"]`);
         this._selectedItem.classList.toggle("selected", true);
         break;
       }
     }
   }
 
-  static get observedAttributes() {
-    return ["login-list-header"];
+  static get reflectedFluentIDs() {
+    return ["count"];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
-    }
-
-    switch (attr) {
-      case "login-list-header":
-        this.shadowRoot.querySelector("h2").textContent = newValue;
-        break;
-    }
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
   }
 
   setLogins(logins) {
     let list = this.shadowRoot.querySelector("ol");
     list.textContent = "";
     this._logins = logins;
     this.render();
   }
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+:host([editing]) .locked-value,
+:host(:not([editing])) .unlocked-value {
+  display: none;
+}
+
+:host(:not([type="password"])) .reveal-button {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+/* globals ReflectedFluentElement */
+
+class ModalInput extends ReflectedFluentElement {
+  static get LOCKED_PASSWORD_DISPLAY() {
+    return "••••••••";
+  }
+
+  connectedCallback() {
+    if (this.children.length) {
+      return;
+    }
+
+    let modalInputTemplate = document.querySelector("#modal-input-template");
+    this.attachShadow({mode: "open"})
+        .appendChild(modalInputTemplate.content.cloneNode(true));
+
+    if (this.hasAttribute("value")) {
+      this.value = this.getAttribute("value");
+    }
+
+    if (this.getAttribute("type") == "password") {
+      let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+      unlockedValue.setAttribute("type", "password");
+    }
+
+    this.shadowRoot.querySelector(".reveal-button").addEventListener("click", this);
+  }
+
+  static get reflectedFluentIDs() {
+    return ["reveal-button"];
+  }
+
+  static get observedAttributes() {
+    return ["editing", "type", "value"].concat(ModalInput.reflectedFluentIDs);
+  }
+
+  attributeChangedCallback(attr, oldValue, newValue) {
+    super.attributeChangedCallback(attr, oldValue, newValue);
+
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+
+    switch (attr) {
+      case "editing": {
+        let isEditing = newValue !== null;
+        if (!isEditing) {
+          this.setAttribute("value", unlockedValue.value);
+        }
+        break;
+      }
+      case "type": {
+        if (newValue == "password") {
+          lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+          unlockedValue.setAttribute("type", "password");
+        } else {
+          lockedValue.textContent = this.getAttribute("value");
+          unlockedValue.setAttribute("type", "text");
+        }
+        break;
+      }
+      case "value": {
+        this.value = newValue;
+        break;
+      }
+    }
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "click": {
+        if (event.target.classList.contains("reveal-button")) {
+          let lockedValue = this.shadowRoot.querySelector(".locked-value");
+          let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+          let editing = this.hasAttribute("editing");
+          if ((editing && unlockedValue.getAttribute("type") == "password") ||
+              (!editing && lockedValue.textContent == this.constructor.LOCKED_PASSWORD_DISPLAY)) {
+            lockedValue.textContent = this.value;
+            unlockedValue.setAttribute("type", "text");
+          } else {
+            lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+            unlockedValue.setAttribute("type", "password");
+          }
+        }
+        break;
+      }
+    }
+  }
+
+  get value() {
+    return this.hasAttribute("editing") ? this.shadowRoot.querySelector(".unlocked-value").value.trim()
+                                        : this.getAttribute("value") || "";
+  }
+
+  set value(val) {
+    if (this.getAttribute("value") != val) {
+      this.setAttribute("value", val);
+      return;
+    }
+    this.shadowRoot.querySelector(".unlocked-value").value = val;
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    if (this.getAttribute("type") == "password" && val && val.length) {
+      lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+    } else {
+      lockedValue.textContent = val;
+    }
+  }
+}
+customElements.define("modal-input", ModalInput);
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/reflected-fluent-element.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+class ReflectedFluentElement extends HTMLElement {
+  _isReflectedAttributePresent(attr) {
+    return this.constructor.reflectedFluentIDs.includes(attr.name);
+  }
+
+  /* This function should be called to apply any localized strings that Fluent
+     may have applied to the element before the custom element was defined. */
+  reflectFluentStrings() {
+    for (let reflectedFluentID of this.constructor.reflectedFluentIDs) {
+      if (this.hasAttribute(reflectedFluentID)) {
+        if (this.handleSpecialCaseFluentString &&
+            this.handleSpecialCaseFluentString(reflectedFluentID)) {
+          continue;
+        }
+
+        let attrValue = this.getAttribute(reflectedFluentID);
+        // Strings that are reflected to their shadowed element are assigned
+        // to an attribute name that matches a className on the element.
+        let shadowedElement = this.shadowRoot.querySelector("." + reflectedFluentID);
+        shadowedElement.textContent = attrValue;
+      }
+    }
+  }
+
+  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
+     need to get reflected in to their targeted element. */
+  attributeChangedCallback(attr, oldValue, newValue) {
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    // Don't respond to attribute changes that aren't related to locale text.
+    if (!this.constructor.reflectedFluentIDs.includes(attr)) {
+      return;
+    }
+
+    if (this.handleSpecialCaseFluentString &&
+        this.handleSpecialCaseFluentString(attr)) {
+      return;
+    }
+
+    // Strings that are reflected to their shadowed element are assigned
+    // to an attribute name that matches a className on the element.
+    let shadowedElement = this.shadowRoot.querySelector("." + attr);
+    shadowedElement.textContent = newValue;
+  }
+}
+customElements.define("reflected-fluent-element", ReflectedFluentElement);
--- a/browser/components/aboutlogins/jar.mn
+++ b/browser/components/aboutlogins/jar.mn
@@ -1,13 +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/.
 
 browser.jar:
+  content/browser/aboutlogins/components/login-filter.css      (content/components/login-filter.css)
+  content/browser/aboutlogins/components/login-filter.js       (content/components/login-filter.js)
   content/browser/aboutlogins/components/login-item.css        (content/components/login-item.css)
   content/browser/aboutlogins/components/login-item.js         (content/components/login-item.js)
   content/browser/aboutlogins/components/login-list.css        (content/components/login-list.css)
   content/browser/aboutlogins/components/login-list.js         (content/components/login-list.js)
   content/browser/aboutlogins/components/login-list-item.css   (content/components/login-list-item.css)
   content/browser/aboutlogins/components/login-list-item.js    (content/components/login-list-item.js)
+  content/browser/aboutlogins/components/modal-input.css       (content/components/modal-input.css)
+  content/browser/aboutlogins/components/modal-input.js        (content/components/modal-input.js)
+  content/browser/aboutlogins/components/reflected-fluent-element.js  (content/components/reflected-fluent-element.js)
+  content/browser/aboutlogins/aboutLogins.css   (content/aboutLogins.css)
   content/browser/aboutlogins/aboutLogins.js    (content/aboutLogins.js)
   content/browser/aboutlogins/aboutLogins.html  (content/aboutLogins.html)
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 prefs =
   signon.management.page.enabled=true
 
 [browser_deleteLogin.js]
 [browser_loginListChanges.js]
+[browser_openSite.js]
 [browser_updateLogin.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openSite.js
@@ -0,0 +1,37 @@
+/* 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");
+const LOGIN_URL = "https://example.com/";
+let TEST_LOGIN1 = new nsLoginInfo(LOGIN_URL, LOGIN_URL, null, "user1", "pass1", "username", "password");
+
+add_task(async function setup() {
+  let storageChangedPromised = TestUtils.topicObserved("passwordmgr-storage-changed",
+                                                       (_, data) => data == "addLogin");
+  TEST_LOGIN1 = Services.logins.addLogin(TEST_LOGIN1);
+  await storageChangedPromised;
+  await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
+  registerCleanupFunction(() => {
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    Services.logins.removeAllLogins();
+  });
+});
+
+add_task(async function test_launch_login_item() {
+  let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, LOGIN_URL);
+
+  let browser = gBrowser.selectedBrowser;
+  await ContentTask.spawn(browser, LoginHelper.loginToVanillaObject(TEST_LOGIN1), async (login) => {
+    let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+    loginItem.setLogin(login);
+    let openSiteButton = loginItem.shadowRoot.querySelector(".open-site-button");
+    openSiteButton.click();
+  });
+
+  info("waiting for new tab to get opened");
+  let newTab = await promiseNewTab;
+  ok(true, "New tab opened to " + LOGIN_URL);
+
+  BrowserTestUtils.removeTab(newTab);
+});
--- a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -39,28 +39,35 @@ add_task(async function test_login_item(
 
     let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
     let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
       return loginItem._login.guid == loginListItem.getAttribute("guid") &&
              loginItem._login.guid == login.guid;
     }, "Waiting for login item to get populated");
     ok(loginItemPopulated, "The login item should get populated");
 
-    let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
-    let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
+    let usernameInput = loginItem.shadowRoot.querySelector("modal-input[name='username']");
+    let passwordInput = loginItem.shadowRoot.querySelector("modal-input[name='password']");
+
+    let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+    editButton.click();
+    await Promise.resolve();
 
     usernameInput.value += "-undome";
     passwordInput.value += "-undome";
 
     let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
     cancelButton.click();
     await Promise.resolve();
     is(usernameInput.value, login.username, "Username change should be reverted");
     is(passwordInput.value, login.password, "Password change should be reverted");
 
+    editButton.click();
+    await Promise.resolve();
+
     usernameInput.value += "-saveme";
     passwordInput.value += "-saveme";
 
     let saveChangesButton = loginItem.shadowRoot.querySelector(".save-changes-button");
     saveChangesButton.click();
 
     await ContentTaskUtils.waitForCondition(() => {
       return loginListItem._login.username == usernameInput.value &&
--- a/browser/components/aboutlogins/tests/mochitest/mochitest.ini
+++ b/browser/components/aboutlogins/tests/mochitest/mochitest.ini
@@ -1,10 +1,16 @@
 [DEFAULT]
 support-files =
    ../../content/aboutLogins.html
+   ../../content/components/login-filter.js
    ../../content/components/login-item.js
    ../../content/components/login-list.js
    ../../content/components/login-list-item.js
+   ../../content/components/modal-input.js
+   ../../content/components/reflected-fluent-element.js
    aboutlogins_common.js
 
+[test_login_filter.html]
 [test_login_item.html]
 [test_login_list.html]
+[test_modal_input.html]
+[test_reflected_fluent_element.html]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the login-filter component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the login-filter component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="login-filter.js"></script>
+  <script src="login-list-item.js"></script>
+  <script src="login-list.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the login-filter component **/
+
+let gLoginFilter;
+let gLoginList;
+add_task(async function setup() {
+  stubFluentL10n({
+    "count": "count",
+  });
+
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gLoginFilter = document.createElement("login-filter");
+  displayEl.appendChild(gLoginFilter);
+
+  gLoginList = document.createElement("login-list");
+  displayEl.appendChild(gLoginList);
+});
+
+add_task(async function test_empty_filter() {
+  ok(gLoginFilter, "loginFilter exists");
+  is(gLoginFilter.shadowRoot.querySelector("input").value, "", "Initially empty");
+});
+
+add_task(async function test_input_events() {
+  let filterEvent = null;
+  window.addEventListener("AboutLoginsFilterLogins", event => filterEvent = event);
+  let input = SpecialPowers.wrap(gLoginFilter.shadowRoot.querySelector("input"));
+  input.setUserInput("test");
+  ok(filterEvent, "Filter event received");
+  is(filterEvent.detail, "test", "Event includes input value");
+});
+
+add_task(async function test_list_filtered() {
+  const LOGINS = [{
+    guid: "123456789",
+    hostname: "https://example.com",
+    username: "user1",
+    password: "pass1",
+  }, {
+    guid: "987654321",
+    hostname: "https://example.com",
+    username: "user2",
+    password: "pass2",
+  }];
+  gLoginList.setLogins(LOGINS);
+
+  let tests = [
+    ["", 2],
+    [LOGINS[0].username, 1],
+    [LOGINS[0].username + "-notfound", 0],
+    [LOGINS[0].username.substr(2, 3), 1],
+    ["", 2],
+    // The password is not used for search.
+    [LOGINS[0].password, 0],
+    [LOGINS[0].password + "-notfound", 0],
+    [LOGINS[0].password.substr(2, 3), 0],
+    ["", 2],
+    [LOGINS[0].hostname, 2],
+    [LOGINS[0].hostname + "-notfound", 0],
+    [LOGINS[0].hostname.substr(2, 3), 2],
+    ["", 2],
+    // The guid is not used for search.
+    [LOGINS[0].guid, 0],
+    [LOGINS[0].guid + "-notfound", 0],
+    [LOGINS[0].guid.substr(0, 2), 0],
+    ["", 2],
+  ];
+
+  let loginFilterInput = gLoginFilter.shadowRoot.querySelector("input");
+  loginFilterInput.focus();
+
+  for (let i = 0; i < tests.length; i++) {
+    info("Testcase: " + i);
+
+    let testObj = {
+      testCase: i,
+      query: tests[i][0],
+      resultExpectedCount: tests[i][1],
+    };
+
+    let filterLength = loginFilterInput.value.length;
+    while (filterLength-- > 0) {
+      sendKey("BACK_SPACE");
+    }
+    sendString(testObj.query);
+
+    await SimpleTest.promiseWaitForCondition(() => {
+      return gLoginList.hasAttribute("count") &&
+             +gLoginList.getAttribute("count") == testObj.resultExpectedCount;
+    }, `Waiting for the search result count to update to ${testObj.resultExpectedCount} (tc#${testObj.testCase})`);
+    let count = +gLoginList.getAttribute("count");
+    is(count, testObj.resultExpectedCount,
+       `The login list count should match the expected result (tc#${testObj.testCase})`);
+  }
+});
+</script>
+
+</body>
+</html>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_item.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_item.html
@@ -2,17 +2,19 @@
 <html>
 <!--
 Test the login-item component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-item component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-item.js"></script>
+  <script src="modal-input.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
   </p>
 <div id="content" style="display: none">
@@ -48,81 +50,81 @@ add_task(async function setup() {
 
   gLoginItem = document.createElement("login-item");
   displayEl.appendChild(gLoginItem);
 });
 
 add_task(async function test_empty_item() {
   ok(gLoginItem, "loginItem exists");
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be blank");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be blank");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank");
 });
 
 add_task(async function test_set_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be populated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
 });
 
 add_task(async function test_different_login_modified() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginModified(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_different_login_removed() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginRemoved(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"});
   gLoginItem.loginModified(modifiedLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, modifiedLogin.hostname, "hostname should be updated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, modifiedLogin.username, "username should be updated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, modifiedLogin.password, "password should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
 });
 
 add_task(async function test_login_removed() {
   gLoginItem.loginRemoved(TEST_LOGIN_1);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be cleared");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be cleared");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be cleared");
 });
 
 </script>
 
 </body>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_list.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_list.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 Test the login-list component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-list component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-list-item.js"></script>
   <script src="login-list.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
@@ -35,16 +36,20 @@ const TEST_LOGIN_1 = {
 const TEST_LOGIN_2 = {
   guid: "987654321",
   hostname: "https://example.com",
   username: "user2",
   password: "pass2",
 };
 
 add_task(async function setup() {
+  stubFluentL10n({
+    "count": "count",
+  });
+
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginList = document.createElement("login-list");
   displayEl.appendChild(gLoginList);
 });
 
@@ -66,16 +71,64 @@ add_task(async function test_populated_l
 
   ok(!loginListItems[0].classList.contains("selected"), "The first item should not be selected by default");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
   loginListItems[0].click();
   ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
 });
 
+add_task(async function test_filtered_list() {
+  is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not([hidden])").length, 2, "Both logins should be visible");
+  let countSpan = gLoginList.shadowRoot.querySelector(".count");
+  is(countSpan.textContent, "2", "Count should match full list length");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user1",
+  }));
+  is(countSpan.textContent, "1", "Count should match result amount");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
+  is(loginListItems[0].shadowRoot.querySelector(".username").textContent, "user1", "user1 is expected first");
+  ok(!loginListItems[0].hidden, "user1 should remain visible");
+  ok(loginListItems[1].hidden, "user2 should be hidden");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user2",
+  }));
+  is(countSpan.textContent, "1", "Count should match result amount");
+  ok(loginListItems[0].hidden, "user1 should be hidden");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user",
+  }));
+  is(countSpan.textContent, "2", "Count should match result amount");
+  ok(!loginListItems[0].hidden, "user1 should be visible");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "foo",
+  }));
+  is(countSpan.textContent, "0", "Count should match result amount");
+  ok(loginListItems[0].hidden, "user1 should be hidden");
+  ok(loginListItems[1].hidden, "user2 should be hidden");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "",
+  }));
+  is(countSpan.textContent, "2", "Count should be reset to full list length");
+  ok(!loginListItems[0].hidden, "user1 should be visible");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+});
+
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
   gLoginList.loginModified(modifiedLogin);
   await asyncElementRendered();
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 2, "Both logins should be displayed");
   is(loginListItems[0].getAttribute("guid"), TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
   is(loginListItems[0].shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname,
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_modal_input.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the modal-input component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the modal-input component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="modal-input.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the modal-input component **/
+
+let gModalInput;
+const TEST_INPUT_VALUE = "fakeValue";
+add_task(async function setup() {
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gModalInput = document.createElement("modal-input");
+  gModalInput.setAttribute("value", TEST_INPUT_VALUE);
+  displayEl.appendChild(gModalInput);
+});
+
+add_task(async function test_initial_state() {
+  ok(gModalInput, "modalInput exists");
+  is(gModalInput.shadowRoot.querySelector(".locked-value").textContent, TEST_INPUT_VALUE, "Values are set initially");
+  is(gModalInput.shadowRoot.querySelector(".unlocked-value").value, TEST_INPUT_VALUE, "Values are set initially");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".locked-value")).display, "inline", ".locked-value is visible by default");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".unlocked-value")).display, "none", ".unlocked-value is hidden by default");
+});
+
+add_task(async function test_editing_set_unset() {
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+  gModalInput.setAttribute("editing", "");
+  is(getComputedStyle(lockedValue).display, "none", ".locked-value is hidden when editing");
+  is(getComputedStyle(unlockedValue).display, "inline", ".unlocked-value is visible when editing");
+
+  const NEW_VALUE = "editedValue";
+  SpecialPowers.wrap(unlockedValue).setUserInput(NEW_VALUE);
+  gModalInput.removeAttribute("editing");
+
+  is(lockedValue.textContent, NEW_VALUE, "Values are updated from edit");
+  is(unlockedValue.value, NEW_VALUE, "Values are updated from edit");
+  is(gModalInput.getAttribute("value"), NEW_VALUE, "The value attribute on the host element is updated from edit");
+  is(getComputedStyle(lockedValue).display, "inline", ".locked-value is visible when not editing");
+  is(getComputedStyle(unlockedValue).display, "none", ".unlocked-value is hidden when not editing");
+});
+
+add_task(async function test_password() {
+  gModalInput.setAttribute("type", "password");
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+
+  is(lockedValue.textContent, gModalInput.constructor.LOCKED_PASSWORD_DISPLAY,
+     "type=password should display masked characters when locked");
+  is(unlockedValue.value, gModalInput.getAttribute("value"), "type=password should have actual value in .unlocked-value");
+  is(unlockedValue.getAttribute("type"), "password", "input[type=password] should be used for .unlocked-value with type=password");
+
+  gModalInput.removeAttribute("value");
+  is(lockedValue.textContent, "",
+     "type=password should display nothing when locked without a value (.locked-value)");
+  is(unlockedValue.value, "",
+     "type=password should display nothing when locked without a value (.unlocked-value)");
+});
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_reflected_fluent_element.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the reflected-fluent-element component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the reflected-fluent-element component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the reflected-fluent-element component **/
+
+const TEST_STRINGS = {
+  loginFilter: {
+    placeholder: "Sample placeholder",
+  },
+  loginItem: {
+    "cancel-button": "Cancel",
+    "delete-button": "Delete",
+    "hostname-label": "Website Address",
+    "password-label": "Password",
+    "save-changes-button": "Save Changes",
+    // See stubFluentL10n for the following three
+    "time-created": "",
+    "time-changed": "",
+    "time-used": "",
+    "username-label": "Username",
+  },
+};
+
+let gLoginFilter;
+let gLoginItem;
+add_task(async function setup() {
+  stubFluentL10n({
+    "time-created": "timeCreated",
+    "time-changed": "timeChanged",
+    "time-used": "timeUsed",
+  });
+
+  let displayEl = document.getElementById("display");
+
+  // Create and append the login-filter element before its template
+  // is cloned the custom element defined.
+  gLoginFilter = document.createElement("login-filter");
+  gLoginFilter.setAttribute("placeholder", TEST_STRINGS.loginFilter.placeholder);
+  displayEl.appendChild(gLoginFilter);
+
+  // ... and do the same with the login-item.
+  gLoginItem = document.createElement("login-item");
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    gLoginItem.setAttribute(attrKey, TEST_STRINGS.loginItem[attrKey]);
+  }
+  displayEl.appendChild(gLoginItem);
+
+  let templateFrame = document.getElementById("templateFrame");
+  importDependencies(templateFrame, displayEl);
+
+  // The script needs to be inserted after the element and template are appended
+  // to match the environment of the locale text being applied before the custom
+  // element is defined.
+  for (let scriptSrc of ["login-filter.js", "login-item.js", "login-list.js"]) {
+    let scriptEl = document.createElement("script");
+    scriptEl.setAttribute("src", scriptSrc);
+    document.head.appendChild(scriptEl);
+  }
+});
+
+add_task(async function test_placeholder_on_login_filter() {
+  ok(gLoginFilter, "loginFilter exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginFilter.shadowRoot, "Wait for shadowRoot");
+  is(gLoginFilter.shadowRoot.querySelector("input").placeholder,
+     TEST_STRINGS.loginFilter.placeholder,
+     "Placeholder text should be present when set before the element is defined");
+});
+
+add_task(async function test_login_item() {
+  ok(gLoginItem, "loginItem exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginItem.shadowRoot, "Wait for shadowRoot");
+
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    let selector = "." + attrKey;
+    is(gLoginItem.shadowRoot.querySelector(selector).textContent,
+       TEST_STRINGS.loginItem[attrKey],
+       selector + " textContent should be present when set before the element is defined");
+  }
+});
+
+add_task(async function test_attribute_changed_callback() {
+  let displayEl = document.getElementById("display");
+  let loginList = document.createElement("login-list");
+  displayEl.appendChild(loginList);
+  await SimpleTest.promiseWaitForCondition(() => !!loginList.shadowRoot, "Wait for element to get templated");
+
+  loginList.setAttribute("count", "1234");
+  await SimpleTest.promiseWaitForCondition(() => loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+                                           "Wait for text to get localized");
+  ok(loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+     "The count attribute should be inherited by the .count span");
+});
+</script>
+
+</body>
+</html>
rename from browser/components/controlcenter/content/panel.inc.xul
rename to browser/components/controlcenter/content/identityPanel.inc.xul
new file mode 100644
--- /dev/null
+++ b/browser/components/controlcenter/content/protectionsPanel.inc.xul
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+
+<panel id="protections-popup"
+       type="arrow"
+       hidden="true"
+       photon="true"
+       role="alertdialog"
+       orient="vertical">
+
+  <panelmultiview id="protections-popup-multiView"
+                  mainViewId="protections-popup-mainView">
+    <panelview id="protections-popup-mainView"
+               descriptionheightworkaround="true">
+      <vbox id="protections-popup-mainView-panel-header">
+        <label>
+          <html:span id="protections-popup-mainView-panel-header-span">
+            <html:h1>Watch this space! :)</html:h1>
+          </html:span>
+        </label>
+      </vbox>
+    </panelview>
+  </panelmultiview>
+</panel>
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -537,89 +537,129 @@ var Policies = {
           Services.prefs.clearUserPref("browser.policies.runOncePerModification.extensionsInstall");
           let addons = await AddonManager.getAddonsByIDs(param.Uninstall);
           for (let addon of addons) {
             if (addon) {
               try {
                 await addon.uninstall();
               } catch (e) {
                 // This can fail for add-ons that can't be uninstalled.
-                // Just ignore.
+                log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
               }
             }
           }
         });
       }
       if ("Install" in param) {
         runOncePerModification("extensionsInstall", JSON.stringify(param.Install), async () => {
           await uninstallingPromise;
           for (let location of param.Install) {
-            let url;
-            if (location.includes("://")) {
-              // Assume location is an URI
-              url = location;
-            } else {
+            let uri;
+            try {
+              uri = Services.io.newURI(location);
+            } catch (e) {
+              // If it's not a URL, it's probably a file path.
               // Assume location is a file path
-              let xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+              // This is done for legacy support (old API)
               try {
-                xpiFile.initWithPath(location);
-              } catch (e) {
+                let xpiFile = new FileUtils.File(location);
+                uri = Services.io.newFileURI(xpiFile);
+              } catch (ex) {
                 log.error(`Invalid extension path location - ${location}`);
-                continue;
-              }
-              url = Services.io.newFileURI(xpiFile).spec;
-            }
-            AddonManager.getInstallForURL(url, {
-              telemetryInfo: {source: "enterprise-policy"},
-            }).then(install => {
-              if (install.addon && install.addon.appDisabled) {
-                log.error(`Incompatible add-on - ${location}`);
-                install.cancel();
                 return;
               }
-              let listener = {
-              /* eslint-disable-next-line no-shadow */
-                onDownloadEnded: (install) => {
-                  if (install.addon && install.addon.appDisabled) {
-                    log.error(`Incompatible add-on - ${location}`);
-                    install.removeListener(listener);
-                    install.cancel();
-                  }
-                },
-                onDownloadFailed: () => {
-                  install.removeListener(listener);
-                  log.error(`Download failed - ${location}`);
-                  clearRunOnceModification("extensionsInstall");
-                },
-                onInstallFailed: () => {
-                  install.removeListener(listener);
-                  log.error(`Installation failed - ${location}`);
-                },
-                onInstallEnded: () => {
-                  install.removeListener(listener);
-                  log.debug(`Installation succeeded - ${location}`);
-                },
-              };
-              install.addListener(listener);
-              install.install();
-            });
+            }
+            installAddonFromURL(uri.spec);
           }
         });
       }
       if ("Locked" in param) {
         for (let ID of param.Locked) {
-          manager.disallowFeature(`modify-extension:${ID}`);
+          manager.disallowFeature(`uninstall-extension:${ID}`);
+          manager.disallowFeature(`disable-extension:${ID}`);
         }
       }
     },
   },
 
   "ExtensionSettings": {
     onBeforeAddons(manager, param) {
-      manager.setExtensionSettings(param);
+      try {
+        manager.setExtensionSettings(param);
+      } catch (e) {
+       log.error("Invalid ExtensionSettings");
+      }
+    },
+    async onBeforeUIStartup(manager, param) {
+      let extensionSettings = param;
+      let blockAllExtensions = false;
+      if ("*" in extensionSettings) {
+        if ("installation_mode" in extensionSettings["*"] &&
+            extensionSettings["*"].installation_mode == "blocked") {
+          blockAllExtensions = true;
+          // Turn off discovery pane in about:addons
+          setAndLockPref("extensions.getAddons.showPane", false);
+          // Block about:debugging
+          blockAboutPage(manager, "about:debugging");
+        }
+      }
+      let {addons} = await AddonManager.getActiveAddons();
+      let allowedExtensions = [];
+      for (let extensionID in extensionSettings) {
+        if (extensionID == "*") {
+          // Ignore global settings
+          continue;
+        }
+        if ("installation_mode" in extensionSettings[extensionID]) {
+          if (extensionSettings[extensionID].installation_mode == "force_installed" ||
+              extensionSettings[extensionID].installation_mode == "normal_installed") {
+            if (!extensionSettings[extensionID].install_url) {
+              throw new Error(`Missing install_url for ${extensionID}`);
+            }
+            if (!addons.find(addon => addon.id == extensionID)) {
+              installAddonFromURL(extensionSettings[extensionID].install_url, extensionID);
+            }
+            manager.disallowFeature(`uninstall-extension:${extensionID}`);
+            if (extensionSettings[extensionID].installation_mode == "force_installed") {
+              manager.disallowFeature(`disable-extension:${extensionID}`);
+            }
+            allowedExtensions.push(extensionID);
+          } else if (extensionSettings[extensionID].installation_mode == "allowed") {
+            allowedExtensions.push(extensionID);
+          } else if (extensionSettings[extensionID].installation_mode == "blocked") {
+            if (addons.find(addon => addon.id == extensionID)) {
+              // Can't use the addon from getActiveAddons since it doesn't have uninstall.
+              let addon = await AddonManager.getAddonByID(extensionID);
+              try {
+                await addon.uninstall();
+              } catch (e) {
+                // This can fail for add-ons that can't be uninstalled.
+                log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
+              }
+            }
+          }
+        }
+      }
+      if (blockAllExtensions) {
+        for (let addon of addons) {
+          if (addon.isSystem || addon.isBuiltin) {
+            continue;
+          }
+          if (!allowedExtensions.includes(addon.id)) {
+            try {
+              // Can't use the addon from getActiveAddons since it doesn't have uninstall.
+              let addonToUninstall = await AddonManager.getAddonByID(addon.id);
+              await addonToUninstall.uninstall();
+            } catch (e) {
+              // This can fail for add-ons that can't be uninstalled.
+              log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
+            }
+          }
+        }
+      }
     },
   },
 
   "ExtensionUpdate": {
     onBeforeAddons(manager, param) {
       if (!param) {
         setAndLockPref("extensions.update.enabled", param);
       }
@@ -1299,16 +1339,64 @@ function clearRunOnceModification(action
 
 function replacePathVariables(path) {
   if (path.includes("${home}")) {
     return path.replace("${home}", FileUtils.getFile("Home", []).path);
   }
   return path;
 }
 
+/**
+ * installAddonFromURL
+ *
+ * Helper function that installs an addon from a URL
+ * and verifies that the addon ID matches.
+*/
+function installAddonFromURL(url, extensionID) {
+  AddonManager.getInstallForURL(url, {
+    telemetryInfo: {source: "enterprise-policy"},
+  }).then(install => {
+    if (install.addon && install.addon.appDisabled) {
+      log.error(`Incompatible add-on - ${location}`);
+      install.cancel();
+      return;
+    }
+    let listener = {
+    /* eslint-disable-next-line no-shadow */
+      onDownloadEnded: (install) => {
+        if (extensionID && install.addon.id != extensionID) {
+          log.error(`Add-on downloaded from ${url} had unexpected id (got ${install.addon.id} expected ${extensionID})`);
+          install.removeListener(listener);
+          install.cancel();
+        }
+        if (install.addon && install.addon.appDisabled) {
+          log.error(`Incompatible add-on - ${url}`);
+          install.removeListener(listener);
+          install.cancel();
+        }
+      },
+      onDownloadFailed: () => {
+        install.removeListener(listener);
+        log.error(`Download failed - ${url}`);
+        clearRunOnceModification("extensionsInstall");
+      },
+      onInstallFailed: () => {
+        install.removeListener(listener);
+        log.error(`Installation failed - ${url}`);
+      },
+      onInstallEnded: () => {
+        install.removeListener(listener);
+        log.debug(`Installation succeeded - ${url}`);
+      },
+    };
+    install.addListener(listener);
+    install.install();
+  });
+}
+
 let gChromeURLSBlocked = false;
 
 // If any about page is blocked, we block the loading of all
 // chrome:// URLs in the browser window.
 function blockAboutPage(manager, feature, neededOnContentProcess = false) {
   manager.disallowFeature(feature, neededOnContentProcess);
   if (!gChromeURLSBlocked) {
     blockAllChromeURLs();
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -318,20 +318,54 @@
             "type": "string"
           }
         }
       }
     },
 
     "ExtensionSettings": {
       "type": "object",
+      "properties": {
+        "*": {
+          "type": "object",
+          "properties": {
+            "installation_mode": {
+              "type": "string",
+              "enum": ["allowed", "blocked"]
+            },
+            "allowed_types": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "enum": ["extension", "dictionary", "locale", "theme"]
+              }
+            },
+            "blocked_install_message": {
+              "type": "string"
+            },
+            "install_sources": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      },
       "patternProperties": {
         "^.*$": {
           "type": "object",
           "properties": {
+            "installation_mode": {
+              "type": "string",
+              "enum": ["allowed", "blocked", "force_installed", "normal_installed"]
+            },
+            "install_url": {
+              "type": "string"
+            },
             "blocked_install_message": {
               "type": "string"
             }
           }
         }
       }
     },
 
@@ -606,33 +640,78 @@
           "type": "boolean"
         }
       }
     },
 
     "Preferences": {
       "type": "object",
       "properties": {
-        "network.IDN_show_punycode": {
+        "app.update.auto ": {
+          "type": "boolean"
+        },
+        "browser.cache.disk.enable": {
           "type": "boolean"
         },
         "browser.fixup.dns_first_for_single_words": {
           "type": "boolean"
         },
+        "browser.search.update": {
+          "type": "boolean"
+        },
+        "browser.tabs.warnOnClose": {
+          "type": "boolean"
+        },
         "browser.cache.disk.parent_directory": {
           "type": "string"
         },
         "browser.urlbar.suggest.openpage": {
           "type": "boolean"
         },
         "browser.urlbar.suggest.history": {
           "type": "boolean"
         },
         "browser.urlbar.suggest.bookmark": {
           "type": "boolean"
+        },
+        "dom.disable_window_flip": {
+          "type": "boolean"
+        },
+        "dom.disable_window_move_resize": {
+          "type": "boolean"
+        },
+        "dom.event.contextmenu.enabled": {
+          "type": "boolean"
+        },
+        "extensions.getAddons.showPane": {
+          "type": "boolean"
+        },
+        "media.gmp-gmpopenh264.enabled": {
+          "type": "boolean"
+        },
+        "media.gmp-widevinecdm.enabled": {
+          "type": "boolean"
+        },
+        "network.dns.disableIPv6": {
+          "type": "boolean"
+        },
+        "network.IDN_show_punycode": {
+          "type": "boolean"
+        },
+        "places.history.enabled": {
+          "type": "boolean"
+        },
+        "security.default_personal_cert": {
+          "type": "string"
+        },
+        "security.ssl.errorReporting.enabled": {
+          "type": "boolean"
+        },
+        "ui.key.menuAccessKeyFocuses": {
+          "type": "boolean"
         }
       }
     },
 
     "PromptForDownloadLocation": {
       "type": "boolean"
     },
 
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -4,16 +4,17 @@ support-files =
   opensearch.html
   opensearchEngine.xml
   policytest_v0.1.xpi
   policytest_v0.2.xpi
   policy_websitefilter_block.html
   policy_websitefilter_exception.html
   ../../../../../toolkit/components/antitracking/test/browser/page.html
   ../../../../../toolkit/components/antitracking/test/browser/subResources.sjs
+  extensionsettings.html
 
 [browser_policies_getActivePolicies.js]
 skip-if = os != 'mac'
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_setAndLockPref_API.js]
 [browser_policy_app_update.js]
 [browser_policy_block_about_addons.js]
 [browser_policy_block_about_config.js]
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
@@ -1,23 +1,207 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-add_task(async function test_extensionsettings() {
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/";
+
+/**
+ * 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.
+ */
+function promisePopupNotificationShown(name) {
+  return new Promise(resolve => {
+    function popupshown() {
+      let notification = PopupNotifications.getNotification(name);
+      if (!notification) { return; }
+
+      ok(notification, `${name} notification shown`);
+      ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+      PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+      resolve(PopupNotifications.panel.firstElementChild);
+    }
+
+    PopupNotifications.panel.addEventListener("popupshown", popupshown);
+  });
+}
+
+add_task(async function test_install_source_blocked_link() {
   await setupPolicyEngineWithJson({
     "policies": {
       "ExtensionSettings": {
-        "extension1@mozilla.com": {
-          "blocked_install_message": "Extension1 error message.",
+        "*": {
+          "install_sources": ["http://blocks.other.install.sources/*"],
         },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_installtrigger() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
         "*": {
-          "blocked_install_message": "Generic error message.",
+          "install_sources": ["http://blocks.other.install.sources/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_installtrigger").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_otherdomain() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
         },
       },
     },
   });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
 
-  let extensionSettings =  Services.policies.getExtensionSettings("extension1@mozilla.com");
-  is(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message.");
-  extensionSettings =  Services.policies.getExtensionSettings("extension2@mozilla.com");
-  is(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message.");
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_otherdomain").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_direct() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://blocks.other.install.sources/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) {
+    content.document.location.href = baseUrl + "policytest_v0.1.xpi";
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_link() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
 });
+
+add_task(async function test_install_source_allowed_installtrigger() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_installtrigger").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_otherdomain() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*", "http://example.org/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_otherdomain").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_direct() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) {
+    content.document.location.href = baseUrl + "policytest_v0.1.xpi";
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/extensionsettings.html
@@ -0,0 +1,23 @@
+
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script type="text/javascript">
+function installTrigger(url) {
+  InstallTrigger.install({extension: url});
+}
+</script>
+</head>
+<body>
+<p>
+<a id="policytest" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+<p>
+<a id="policytest_installtrigger" onclick="installTrigger(this.href);return false;" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+<p>
+<a id="policytest_otherdomain" href="http://example.org:80/browser/browser/components/enterprisepolicies/tests/browser/policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48");
+
+const server = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
+const BASE_URL = `http://example.com/data`;
+
+let addonID = "policytest2@mozilla.com";
+
+add_task(async function setup() {
+  await AddonTestUtils.promiseStartupManager();
+
+  let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {
+        gecko: {
+          id: addonID,
+        },
+      },
+    },
+  });
+
+  server.registerFile("/data/policy_test.xpi", webExtensionFile);
+});
+
+add_task(async function test_extensionsettings() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "extension1@mozilla.com": {
+          "blocked_install_message": "Extension1 error message.",
+        },
+        "*": {
+          "blocked_install_message": "Generic error message.",
+        },
+      },
+    },
+  });
+
+  let extensionSettings =  Services.policies.getExtensionSettings("extension1@mozilla.com");
+  equal(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message.");
+  extensionSettings =  Services.policies.getExtensionSettings("extension2@mozilla.com");
+  equal(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message.");
+});
+
+add_task(async function test_addon_blocked() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "policytest2@mozilla.com": {
+          "installation_mode": "blocked",
+        },
+      },
+    },
+  });
+
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+  equal(install.addon.appDisabled, true, "Addon should be disabled");
+  await install.addon.uninstall();
+});
+
+add_task(async function test_addon_allowed() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "policytest2@mozilla.com": {
+          "installation_mode": "allowed",
+        },
+        "*": {
+          "installation_mode": "blocked",
+        },
+      },
+    },
+  });
+
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+  equal(install.addon.appDisabled, false, "Addon should not be disabled");
+  await install.addon.uninstall();
+});
+
+add_task(async function test_addon_uninstalled() {
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+
+  await Promise.all([
+    AddonTestUtils.promiseAddonEvent("onUninstalled"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "*": {
+            "installation_mode": "blocked",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  equal(addon, null, "Addon should be null");
+});
+
+add_task(async function test_addon_forceinstalled() {
+  await Promise.all([
+    AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "policytest2@mozilla.com": {
+            "installation_mode": "force_installed",
+            "install_url": BASE_URL + "/policy_test.xpi",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  notEqual(addon, null, "Addon should not be null");
+  equal(addon.appDisabled, false, "Addon should not be disabled");
+  equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled.");
+  equal(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should not be able to be disabled.");
+  await addon.uninstall();
+});
+
+add_task(async function test_addon_normalinstalled() {
+  await Promise.all([
+    AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "policytest2@mozilla.com": {
+            "installation_mode": "normal_installed",
+            "install_url": BASE_URL + "/policy_test.xpi",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  notEqual(addon, null, "Addon should not be null");
+  equal(addon.appDisabled, false, "Addon should not be disabled");
+  equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled.");
+  notEqual(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should be able to be disabled.");
+  await addon.uninstall();
+});
--- a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
+++ b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 firefox-appdir = browser
 head = head.js
 
 [test_3rdparty.js]
 [test_appupdateurl.js]
 [test_clear_blocked_cookies.js]
 [test_defaultbrowsercheck.js]
+[test_extensionsettings.js]
 [test_macosparser_unflatten.js]
 skip-if = os != 'mac'
 [test_permissions.js]
 [test_popups_cookies_addons_flash.js]
 support-files = config_popups_cookies_addons_flash.json
 [test_preferences.js]
 [test_proxy.js]
 [test_requestedlocales.js]
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -2291,30 +2291,34 @@ var SessionStoreInternal = {
         if (now - data.closedAt > TIME_TO_LIVE) {
           array.splice(i, 1);
           this._closedObjectsChanged = true;
         }
       }
     }
   },
 
-  async _doTabProcessSwitch(aBrowser, aRemoteType, aChannel, aSwitchId) {
+  async _doTabProcessSwitch(aBrowser, aRemoteType, aChannel, aSwitchId, aReplaceBrowsingContext) {
     debug(`[process-switch]: performing switch from ${aBrowser.remoteType} to ${aRemoteType}`);
 
     // Don't try to switch tabs before delayed startup is completed.
     await aBrowser.ownerGlobal.delayedStartupPromise;
 
     // Perform a navigateAndRestore to trigger the process switch.
     let tab = aBrowser.ownerGlobal.gBrowser.getTabForBrowser(aBrowser);
     let loadArguments = {
       newFrameloader: true,  // Switch even if remoteType hasn't changed.
       remoteType: aRemoteType,  // Don't derive remoteType to switch to.
 
       // Information about which channel should be performing the load.
       redirectLoadSwitchId: aSwitchId,
+
+      // True if this is a process switch due to a policy mismatch, means we
+      // shouldn't preserve our browsing context.
+      replaceBrowsingContext: aReplaceBrowsingContext,
     };
 
     await SessionStore.navigateAndRestore(tab, loadArguments, -1);
 
     // If the process switch seems to have failed, send an error over to our
     // caller, to give it a chance to kill our channel.
     if (aBrowser.remoteType != aRemoteType ||
         !aBrowser.frameLoader || !aBrowser.frameLoader.remoteTab) {
@@ -2327,26 +2331,26 @@ var SessionStoreInternal = {
     return remoteTab;
   },
 
   /**
    * Perform a destructive process switch into a distinct process.
    * This method is asynchronous, as it requires multiple calls into content
    * processes.
    */
-  async _doProcessSwitch(aBrowsingContext, aRemoteType, aChannel, aSwitchId) {
+  async _doProcessSwitch(aBrowsingContext, aRemoteType, aChannel, aSwitchId, aReplaceBrowsingContext) {
     // There are two relevant cases when performing a process switch for a
     // browsing context: in-process and out-of-process embedders.
 
     // If our embedder is in-process (e.g. we're a xul:browser element embedded
     // within <tabbrowser>), then we can perform a process switch using the
     // traditional mechanism.
     if (aBrowsingContext.embedderElement) {
       return this._doTabProcessSwitch(aBrowsingContext.embedderElement,
-                                      aRemoteType, aChannel, aSwitchId);
+                                      aRemoteType, aChannel, aSwitchId, aReplaceBrowsingContext);
     }
 
     let wg = aBrowsingContext.embedderWindowGlobal;
     return wg.changeFrameRemoteness(aBrowsingContext, aRemoteType, aSwitchId);
   },
 
   // Examine the channel response to see if we should change the process
   // performing the given load.
@@ -2438,23 +2442,27 @@ var SessionStoreInternal = {
     }
 
     if (remoteType == E10SUtils.NOT_REMOTE ||
         currentRemoteType == E10SUtils.NOT_REMOTE) {
       debug(`[process-switch]: non-remote source/target - ignoring`);
       return;
     }
 
+    const isCOOPSwitch = E10SUtils.useCrossOriginOpenerPolicy() &&
+          aChannel.hasCrossOriginOpenerPolicyMismatch();
+
     // ------------------------------------------------------------------------
     // DANGER ZONE: Perform a process switch into the new process. This is
     // destructive.
     // ------------------------------------------------------------------------
     let identifier = ++this._switchIdMonotonic;
     let tabPromise = this._doProcessSwitch(browsingContext, remoteType,
-                                           aChannel, identifier);
+                                           aChannel, identifier,
+                                           isCOOPSwitch);
     aChannel.switchProcessTo(tabPromise, identifier);
   },
 
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
     let state = this.getCurrentState();
 
@@ -3291,16 +3299,17 @@ var SessionStoreInternal = {
     let tabState = TabState.clone(tab, TAB_CUSTOM_VALUES.get(tab));
     let options = {
       restoreImmediately: true,
       // We want to make sure that this information is passed to restoreTab
       // whether or not a historyIndex is passed in. Thus, we extract it from
       // the loadArguments.
       newFrameloader: loadArguments.newFrameloader,
       remoteType: loadArguments.remoteType,
+      replaceBrowsingContext: loadArguments.replaceBrowsingContext,
       // Make sure that SessionStore knows that this restoration is due
       // to a navigation, as opposed to us restoring a closed window or tab.
       restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
     };
 
     if (historyIndex >= 0) {
       tabState.index = historyIndex + 1;
       tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
@@ -4253,28 +4262,31 @@ var SessionStoreInternal = {
       if (loadArguments.userContextId) {
         browser.setAttribute("usercontextid", loadArguments.userContextId);
       }
     }
 
     this.markTabAsRestoring(aTab);
 
     let newFrameloader = aOptions.newFrameloader;
-
+    let replaceBrowsingContext = aOptions.replaceBrowsingContext;
     let isRemotenessUpdate;
     if (aOptions.remoteType !== undefined) {
       // We already have a selected remote type so we update to that.
       isRemotenessUpdate =
         tabbrowser.updateBrowserRemoteness(browser,
                                            { remoteType: aOptions.remoteType,
-                                             newFrameloader });
+                                             newFrameloader,
+                                             replaceBrowsingContext,
+                                           });
     } else {
       isRemotenessUpdate =
         tabbrowser.updateBrowserRemotenessByURL(browser, uri, {
           newFrameloader,
+          replaceBrowsingContext,
         });
     }
 
     if (isRemotenessUpdate) {
       // We updated the remoteness, so we need to send the history down again.
       //
       // Start a new epoch to discard all frame script messages relating to a
       // previous epoch. All async messages that are still on their way to chrome
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -95,16 +95,17 @@ skip-if = !e10s || !crashreporter
 skip-if = !e10s || !crashreporter
 [browser_dying_cache.js]
 skip-if = (os == 'win') # bug 1331853
 [browser_dynamic_frames.js]
 [browser_formdata.js]
 skip-if = (verify && debug)
 [browser_formdata_cc.js]
 [browser_formdata_format.js]
+skip-if = !debug && (os == 'linux') # Bug 1535645
 [browser_formdata_password.js]
 support-files = file_formdata_password.html
 [browser_formdata_xpath.js]
 [browser_frametree.js]
 [browser_frame_history.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_global_store.js]
 [browser_history_persist.js]
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -170,16 +170,19 @@ class UrlbarInput {
     this._copyCutController = new CopyCutController(this);
     this.inputField.controllers.insertControllerAt(0, this._copyCutController);
 
     this._initPasteAndGo();
 
     // Tracks IME composition.
     this._compositionState = UrlbarUtils.COMPOSITION.NONE;
     this._compositionClosedPopup = false;
+
+    this.editor.QueryInterface(Ci.nsIPlaintextEditor).newlineHandling =
+      Ci.nsIPlaintextEditor.eNewlinesStripSurroundingWhitespace;
   }
 
   /**
    * Uninitializes this input object, detaching it from the inputField.
    */
   uninit() {
     for (let name of this._inputFieldEvents) {
       this.inputField.removeEventListener(name, this);
--- a/browser/components/urlbar/tests/browser/browser_urlbarStop.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbarStop.js
@@ -27,13 +27,30 @@ add_task(async function() {
   is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
   gBrowser.removeCurrentTab();
 });
 
 async function typeAndSubmitAndStop(url) {
   await promiseAutocompleteResultPopup(url, window, true);
   is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
 
-  let promise =
+  let docLoadPromise =
     BrowserTestUtils.waitForDocLoadAndStopIt(url, gBrowser.selectedBrowser, false);
+
+  // When the load is stopped, tabbrowser calls URLBarSetURI and then calls
+  // onStateChange on its progress listeners.  So to properly wait until the
+  // urlbar value has been updated, add our own progress listener here.
+  let progressPromise = new Promise(resolve => {
+    let listener = {
+      onStateChange(browser, webProgress, request, stateFlags, status) {
+        if (webProgress.isTopLevel &&
+            (stateFlags & Ci.nsIWebProgressListener.STATE_STOP)) {
+          gBrowser.removeTabsProgressListener(listener);
+          resolve();
+        }
+      },
+    };
+    gBrowser.addTabsProgressListener(listener);
+  });
+
   gURLBar.handleCommand();
-  await promise;
+  await Promise.all([docLoadPromise, progressPromise]);
 }
--- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl
+++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl
@@ -85,16 +85,18 @@ policy-DownloadDirectory = Set and lock 
 # “lock” means that the user won’t be able to change this setting
 policy-EnableTrackingProtection = Enable or disable Content Blocking and optionally lock it.
 
 # A “locked” extension can’t be disabled or removed by the user. This policy
 # takes 3 keys (“Install”, ”Uninstall”, ”Locked”), you can either keep them in
 # English or translate them as verbs.
 policy-Extensions = Install, uninstall or lock extensions. The Install option takes URLs or paths as parameters. The Uninstall and Locked options take extension IDs.
 
+policy-ExtensionSettings = Manage all aspects of extension installation.
+
 policy-ExtensionUpdate = Enable or disable automatic extension updates.
 
 policy-FirefoxHome = Configure Firefox Home.
 
 policy-FlashPlugin = Allow or deny usage of the Flash plugin.
 
 policy-HardwareAcceleration = If false, turn off hardware acceleration.
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -36,16 +36,23 @@ xpinstallPromptMessage.dontAllow.accessk
 xpinstallPromptMessage.install=Continue to Installation
 xpinstallPromptMessage.install.accesskey=C
 
 xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
 xpinstallDisabledMessage=Software installation is currently disabled. Click Enable and try again.
 xpinstallDisabledButton=Enable
 xpinstallDisabledButton.accesskey=n
 
+# LOCALIZATION NOTE (addonInstallBlockedByPolicy)
+# This message is shown when the installation of an add-on is blocked by
+# enterprise policy. %1$S is replaced by the name of the add-on.
+# %2$S is replaced by the ID of add-on. %3$S is a custom message that
+# the administration can add to the message.
+addonInstallBlockedByPolicy=%1$S (%2$S) is blocked by your system administrator.%3$S
+
 # LOCALIZATION NOTE (webextPerms.header)
 # This string is used as a header in the webextension permissions dialog,
 # %S is replaced with the localized name of the extension being installed.
 # See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
 # for an example of the full dialog.
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.header=Add %S?
 
--- a/build/moz.configure/rust.configure
+++ b/build/moz.configure/rust.configure
@@ -5,66 +5,91 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 # Rust is required by `rust_compiler` below. We allow_missing here
 # to propagate failures to the better error message there.
 js_option(env='RUSTC', nargs=1, help='Path to the rust compiler')
 js_option(env='CARGO', nargs=1, help='Path to the Cargo package manager')
 
-rustc = check_prog('RUSTC', ['rustc'], paths=toolchain_search_path,
-                   input='RUSTC', allow_missing=True)
+rustc = check_prog('_RUSTC', ['rustc'], what='rustc',
+                   paths=toolchain_search_path, input='RUSTC',
+                   allow_missing=True)
 cargo = check_prog('_CARGO', ['cargo'], what='cargo',
                    paths=toolchain_search_path, input='CARGO',
                    allow_missing=True)
 
 
-@depends(cargo, host)
-@imports('subprocess')
-@imports(_from='__builtin__', _import='open')
-@imports('os')
-def cargo(cargo, host):
-    # The cargo executable can be either a rustup wrapper, or a real,
-    # plain, cargo. In the former case, on OSX, rustup sets
-    # DYLD_LIBRARY_PATH (at least until
-    # https://github.com/rust-lang/rustup.rs/pull/1752 is merged and shipped)
-    # and that can wreck havoc (see bug 1536486).
+@template
+def unwrap_rustup(prog, name):
+    # rustc and cargo can either be rustup wrappers, or they can be the actual,
+    # plain executables. For cargo, on OSX, rustup sets DYLD_LIBRARY_PATH (at
+    # least until https://github.com/rust-lang/rustup.rs/pull/1752 is merged
+    # and shipped) and that can wreak havoc (see bug 1536486). Similarly, for
+    # rustc, rustup silently honors toolchain overrides set by vendored crates
+    # (see bug 1547196).
     #
-    # So if we're in that situation, find the corresponding real plain
-    # cargo.
+    # In either case, we need to find the plain executables.
     #
-    # To achieve that, try to run `cargo +stable`. When it's the rustup
-    # wrapper, it either prints cargo's help and exits with status 0, or print
+    # To achieve that, try to run `PROG +stable`. When the rustup wrapper is in
+    # use, it either prints PROG's help and exits with status 0, or prints
     # an error message (error: toolchain 'stable' is not installed) and exits
-    # with status 1. When it's plain cargo, it exits with a different error
-    # message (error: no such subcommand: `+stable`), and exits with status
-    # 101.
-    if host.os == 'OSX':
-        with open(os.devnull, 'wb') as devnull:
-            retcode = subprocess.call(
-                [cargo, '+stable'], stdout=devnull, stderr=devnull)
-        if retcode != 101:
-            # We now proceed to find the real cargo. We're not sure `rustup`
-            # is in $PATH, but we know the cargo executable location, and that
-            # it /is/ rustup, so we can pass it `rustup` as argv[0], which
-            # will make it act as rustup.
-            # Note we could avoid the `cargo +stable` call above, but there's
-            # the possibility that there's a `cargo-which` command that would
-            # not fail with running `cargo which cargo` with a real cargo.
-            out = check_cmd_output('rustup', 'which', 'cargo',
-                                   executable=cargo).rstrip()
+    # with status 1. In the cargo case, when plain cargo is in use, it exits
+    # with a different error message (e.g. "error: no such subcommand:
+    # `+stable`"), and exits with status 101.
+    #
+    # Unfortunately, in the rustc case, when plain rustc is in use,
+    # `rustc +stable` will exit with status 1, complaining about a missing
+    # "+stable" file. We'll examine the error output to try and distinguish
+    # between failing rustup and failing rustc.
+    @depends(prog, dependable(name))
+    @imports('subprocess')
+    @imports(_from='__builtin__', _import='open')
+    @imports('os')
+    def unwrap(prog, name):
+        def from_rustup_which():
+            out = check_cmd_output('rustup', 'which', name,
+                                   executable=prog).rstrip()
             # If for some reason the above failed to return something, keep the
-            # cargo we found originally.
+            # PROG we found originally.
             if out:
-                cargo = out
-                log.info('Actually using %s', cargo)
-    return cargo
+                log.info('Actually using \'%s\'', out)
+                return out
+
+            log.info('No `rustup which` output, using \'%s\'', prog)
+            return prog
+
+        (retcode, stdout, stderr) = get_cmd_output(prog, '+stable')
+
+        if name == 'cargo' and retcode != 101:
+            prog = from_rustup_which()
+        elif name == 'rustc':
+            if retcode == 0:
+                prog = from_rustup_which()
+            elif "+stable" in stderr:
+                # PROG looks like plain `rustc`.
+                pass
+            else:
+                # Assume PROG looks like `rustup`. This case is a little weird,
+                # insofar as the user doesn't have the "stable" toolchain
+                # installed, but go ahead and unwrap anyway: the user might
+                # have only certain versions, beta, or nightly installed, and
+                # we'll catch invalid versions later.
+                prog = from_rustup_which()
+
+        return prog
+
+    return unwrap
+
+rustc = unwrap_rustup(rustc, 'rustc')
+cargo = unwrap_rustup(cargo, 'cargo')
 
 
 set_config('CARGO', cargo)
+set_config('RUSTC', rustc)
 
 
 @depends_if(rustc)
 @checking('rustc version', lambda info: info.version)
 def rustc_info(rustc):
     out = check_cmd_output(rustc, '--version', '--verbose').splitlines()
     info = dict((s.strip() for s in line.split(':', 1)) for line in out[1:])
     return namespace(
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -1025,16 +1025,22 @@ def compiler(language, host_or_target, c
             if host_or_target.os == 'Android':
                 raise FatalCheckError('GCC is not supported on Android.\n'
                                       'Please use clang from the Android NDK instead.')
             if info.version < '6.1.0':
                 raise FatalCheckError(
                     'Only GCC 6.1 or newer is supported (found version %s).'
                     % info.version)
 
+        if info.type == 'clang-cl':
+            if info.version < '8.0.0':
+                raise FatalCheckError(
+                    'Only clang-cl 8.0 or newer is supported (found version %s)'
+                    % info.version)
+
         # If you want to bump the version check here search for
         # builtin_bitreverse8 above, and see the associated comment.
         if info.type == 'clang' and not info.version:
             raise FatalCheckError(
                 'Only clang/llvm 3.9 or newer is supported.')
 
         if info.flags:
             raise FatalCheckError(
--- a/devtools/client/aboutdebugging-new/documentation/TESTS_REAL_DEVICES.md
+++ b/devtools/client/aboutdebugging-new/documentation/TESTS_REAL_DEVICES.md
@@ -8,17 +8,17 @@ The tests that use a real device are loc
 
 ## Setup environment
 ### Real device side
 1. Enable USB debugging on your device
 2. Launch Firefox
 3. Enable USB debugging on your Firefox
 4. Connect to your PC via USB
 
-You can refer to https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE#Setting_up_the_Android_device.
+You can refer to https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Debugging_over_USB
 
 ### PC side
 Setup the real device information to evaluate the validity in tests.
 
 1. Copy a sample file which is located at `devtools/client/aboutdebugging-new/test/browser/real/usb-runtimes-sample.json` and rename it for example to `devtools/client/aboutdebugging-new/test/browser/real/local-usb-runtimes.json`.
 2. Edit the file.
 
    This is a JSON file like below, write your real device information in here. This example indicates that there should be one USB device and should be displayed `Pixel 2` as device name and `Firefox Nightly` as short name on the sidebar of about:debugging. Regarding the other information, please see `Detail of config file` section of this document.
--- a/devtools/client/aboutdebugging-new/src/components/CompatibilityWarning.js
+++ b/devtools/client/aboutdebugging-new/src/components/CompatibilityWarning.js
@@ -10,17 +10,17 @@ const dom = require("devtools/client/sha
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
 const Message = createFactory(require("./shared/Message"));
 
 const { MESSAGE_LEVEL } = require("../constants");
 const { COMPATIBILITY_STATUS } = require("devtools/client/shared/remote-debugging/version-checker");
 
-const TROUBLESHOOTING_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
+const TROUBLESHOOTING_URL = "https://developer.mozilla.org/docs/Tools/about:debugging#Troubleshooting";
 
 const Types = require("../types/index");
 
 class CompatibilityWarning extends PureComponent {
   static get propTypes() {
     return {
       compatibilityReport: Types.compatibilityReport.isRequired,
     };
--- a/devtools/client/aboutdebugging-new/src/components/ServiceWorkersWarning.js
+++ b/devtools/client/aboutdebugging-new/src/components/ServiceWorkersWarning.js
@@ -8,17 +8,17 @@ const { createFactory, PureComponent } =
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
 const Message = createFactory(require("./shared/Message"));
 
 const { MESSAGE_LEVEL } = require("../constants");
-const DOC_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging#Service_workers_not_compatible";
+const DOC_URL = "https://developer.mozilla.org/docs/Tools/about:debugging#Service_workers_not_compatible";
 
 class ServiceWorkersWarning extends PureComponent {
   render() {
     return Message(
       {
         level: MESSAGE_LEVEL.WARNING,
         isCloseable: true,
       },
--- a/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
+++ b/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
@@ -141,25 +141,25 @@ class ConnectPage extends PureComponent 
         ),
       },
       isAddonInstalled
         ? ConnectSteps(
           {
             steps: [
               {
                 localizationId: "about-debugging-setup-usb-step-enable-dev-menu",
-                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE#Setting_up_the_Android_device",
+                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_over_USB",
               },
               {
                 localizationId: "about-debugging-setup-usb-step-enable-debug",
-                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE#Setting_up_the_Android_device",
+                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_over_USB",
               },
               {
                 localizationId: "about-debugging-setup-usb-step-enable-debug-firefox",
-                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE#Setting_up_the_Android_device",
+                url: "https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_over_USB",
               },
               { localizationId: "about-debugging-setup-usb-step-plug-device" },
             ],
           }
         )
         : Localized(
           {
             id: "about-debugging-setup-usb-disabled",
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -26,16 +26,18 @@ button {
 }
 
 .category {
   align-items: center;
   /* Override a `background-color` set on all buttons by common.inc.css */
   background-color: transparent;
   display: flex;
   flex-direction: row;
+  /* Override button min-width set by common.inc.css for compact width case */
+  min-width: initial;
 }
 
 .category.selected {
   /* Override a `color: inherit !important` forced on all buttons by common.inc.css */
   color: var(--in-content-category-text-selected) !important;
 }
 
 .category-name {
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -358,25 +358,48 @@ body {
   background: var(--theme-toolbar-background);
   font: message-box;
   font-size: var(--accessibility-font-size);
   height: var(--accessibility-toolbar-height);
   color: var(--theme-toolbar-color);
 }
 
 .badge {
+  display: inline-block;
   font: message-box;
+  font-size: var(--theme-body-font-size);
+  line-height: calc(14 / 11);
   border-radius: 3px;
-  padding: 0px 2px;
+  padding: 0px 3px;
   margin-inline-start: 5px;
   color: var(--accessible-label-color);
   background-color: var(--accessible-label-background-color);
   border: 1px solid var(--accessible-label-border-color);
 }
 
+.badge.audit-badge::before {
+  content: "";
+  display: inline-block;
+  vertical-align: -1px;
+  width: 10px;
+  height: 10px;
+  margin-inline-end: 1px;
+  background: url("chrome://devtools/skin/images/alert-tiny.svg") no-repeat;
+  -moz-context-properties: fill;
+  fill: currentColor;
+  opacity: 0.9;
+}
+
+/* improve alignment in high res (where we can use half pixels) */
+@media (min-resolution: 1.5x) {
+  .badge.audit-badge::before {
+    vertical-align: -1.5px;
+  }
+}
+
 .badge.toggle-button {
   color: var(--theme-body-color);
   background-color: var(--badge-interactive-background-color);
   border-color: transparent;
 }
 
 .devtools-toolbar .badge.toggle-button:focus {
   outline: 2px solid var(--accessibility-toolbar-focus);
--- a/devtools/client/accessibility/components/AccessibilityTree.js
+++ b/devtools/client/accessibility/components/AccessibilityTree.js
@@ -179,17 +179,17 @@ class AccessibilityTree extends Componen
       }));
     };
     const className = filtered ? "filtered" : undefined;
 
     return (
       TreeView({
         object: walker,
         mode: MODE.SHORT,
-        provider: new Provider(accessibles, dispatch),
+        provider: new Provider(accessibles, filtered, dispatch),
         columns: columns,
         className,
         renderValue: this.renderValue,
         renderRow,
         label: L10N.getStr("accessibility.treeName"),
         header: true,
         expandedNodes: expanded,
         selected,
--- a/devtools/client/accessibility/components/AccessibilityTreeFilter.js
+++ b/devtools/client/accessibility/components/AccessibilityTreeFilter.js
@@ -70,17 +70,17 @@ class AccessibilityTreeFilter extends Co
 
     this.toggleFilter(filterKey);
   }
 
   render() {
     const { auditing, filters } = this.props;
     const filterButtons = Object.entries(filters).map(([filterKey, active]) =>
       ToggleButton({
-        className: "audit-badge badge",
+        className: "badge",
         key: filterKey,
         active,
         label: L10N.getStr(FILTER_LABELS[filterKey]),
         onClick: this.onClick.bind(this, filterKey),
         onKeyDown: this.onKeyDown.bind(this, filterKey),
         busy: auditing === filterKey,
       }));
 
--- a/devtools/client/accessibility/components/Badge.js
+++ b/devtools/client/accessibility/components/Badge.js
@@ -7,25 +7,28 @@
 const { Component } = require("devtools/client/shared/vendor/react");
 const { span } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 class Badge extends Component {
   static get propTypes() {
     return {
       label: PropTypes.string.isRequired,
+      ariaLabel: PropTypes.string,
       tooltip: PropTypes.string,
     };
   }
 
   render() {
-    const { label, tooltip } = this.props;
+    const { label, ariaLabel, tooltip } = this.props;
 
-    return span({
-      className: "audit-badge badge",
-      title: tooltip,
-      "aria-label": label,
-    },
-      label);
+    return span(
+      {
+        className: "audit-badge badge",
+        title: tooltip,
+        "aria-label": ariaLabel || label,
+      },
+      label
+    );
   }
 }
 
 module.exports = Badge;
--- a/devtools/client/accessibility/components/Checks.js
+++ b/devtools/client/accessibility/components/Checks.js
@@ -16,17 +16,17 @@ const { L10N } = require("../utils/l10n"
 
 const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
 
 function EmptyChecks() {
   return (
     div({
       className: "checks-empty",
       role: "presentation",
-    }, L10N.getStr("accessibility.checks.empty"))
+    }, L10N.getStr("accessibility.checks.empty2"))
   );
 }
 
 // Component that is responsible for rendering accessible audit data in the a11y panel
 // sidebar.
 class Checks extends Component {
   static get propTypes() {
     return {
--- a/devtools/client/accessibility/components/ContrastBadge.js
+++ b/devtools/client/accessibility/components/ContrastBadge.js
@@ -33,14 +33,15 @@ class ContrastBadge extends Component {
     }
 
     if (score !== SCORES.FAIL) {
       return null;
     }
 
     return Badge({
       label: L10N.getStr("accessibility.badge.contrast"),
+      ariaLabel: L10N.getStr("accessibility.badge.contrast.warning"),
       tooltip: L10N.getStr("accessibility.badge.contrast.tooltip"),
     });
   }
 }
 
 module.exports = ContrastBadge;
--- a/devtools/client/accessibility/provider.js
+++ b/devtools/client/accessibility/provider.js
@@ -9,18 +9,19 @@ const { fetchChildren } = require("./act
  * Data provider that is responsible for mapping of an accessibles cache to the
  * data format that is supported by the TreeView component.
  * @param {Map}      accessibles accessibles object cache
  * @param {Function} dispatch    react dispatch function that triggers a redux
  *                               action.
  */
 
 class Provider {
-  constructor(accessibles, dispatch) {
+  constructor(accessibles, filtered, dispatch) {
     this.accessibles = accessibles;
+    this.filtered = filtered;
     this.dispatch = dispatch;
   }
 
   /**
    * Get accessible's cached children if available, if not fetch them from
    * backend.
    * @param {Object}  accessible accessible object whose children to get.
    * @returns {Array} arraof of accessible children.
@@ -82,11 +83,28 @@ class Provider {
    * Get a type of an accesible object. Corresponds to the type of an accessible
    * front.
    * @param {Object}   accessible accessible object
    * @returns {String} accessible object type
    */
   getType(accessible) {
     return accessible.typeName;
   }
+
+  /**
+   * Get the depth of the accesible object in the accessibility tree. When the
+   * tree is filtered it is flattened and the level is set to 0. Otherwise use
+   * internal TreeView level.
+   *
+   * @param {Object}   accessible
+   *                   accessible object
+   * @param {Number}   defaultLevel
+   *                   default level provided by the TreeView component.
+   *
+   * @returns {null|Number}
+   *          depth level of the accessible object.
+   */
+  getLevel(accessible, defaultLevel) {
+    return this.filtered ? 0 : defaultLevel;
+  }
 }
 
 exports.Provider = Provider;
--- a/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js
+++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js
@@ -34,69 +34,80 @@ const tests = [{
   setup: async ({ doc }) => {
     await toggleRow(doc, 0);
     await toggleRow(doc, 1);
   },
   expected: {
     tree: [{
       role: "document",
       name: `"Accessibility Panel Test"`,
+      level: 1,
     }, {
       role: "heading",
       name: `"Top level header"`,
+      level: 2,
     }, {
       role: "text leaf",
       name: `"Top level header "contrast`,
       badges: [ "contrast" ],
+      level: 3,
     }, {
       role: "heading",
       name: `"Second level header"`,
+      level: 2,
     }],
   },
 }, {
   desc: "Click on the contrast filter.",
   setup: async ({ doc }) => {
     await toggleFilter(doc, 0);
   },
   expected: {
     tree: [{
       role: "text leaf",
       name: `"Top level header "contrast`,
       badges: [ "contrast" ],
+      level: 1,
     }, {
       role: "text leaf",
       name: `"Second level header "contrast`,
       badges: [ "contrast" ],
       selected: true,
+      level: 1,
     }],
   },
 }, {
   desc: "Click on the contrast filter again.",
   setup: async ({ doc }) => {
     await toggleFilter(doc, 0);
   },
   expected: {
     tree: [{
       role: "document",
       name: `"Accessibility Panel Test"`,
+      level: 1,
     }, {
       role: "heading",
       name: `"Top level header"`,
+      level: 2,
     }, {
       role: "text leaf",
       name: `"Top level header "contrast`,
       badges: [ "contrast" ],
+      level: 3,
     }, {
       role: "heading",
       name: `"Second level header"`,
+      level: 2,
     }, {
       role: "text leaf",
       name: `"Second level header "contrast`,
       badges: [ "contrast" ],
       selected: true,
+      level: 3,
     }],
   },
 }];
 
 /**
  * Simple test that checks content of the Accessibility panel tree when one of
  * the tree rows has a "contrast" badge and auditing is activated via toolbar
  * filter.
--- a/devtools/client/accessibility/test/browser/head.js
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -232,29 +232,48 @@ function checkSelected(row, expected) {
   if (row.classList.contains("selected") !== expected) {
     return false;
   }
 
   return isVisible(row);
 }
 
 /**
+ * Check level for a given row in the accessibility tree.
+ * @param   {DOMNode} row
+ *          DOMNode for a given accessibility row.
+ * @param   {Boolean} expected
+ *          Expected row level (aria-level).
+ *
+ * @returns {Boolean}
+ *          True if the aria-level for the row is as expected.
+ */
+function checkLevel(row, expected) {
+  if (!expected) {
+    return true;
+  }
+
+  return parseInt(row.getAttribute("aria-level"), 10) === expected;
+}
+
+/**
  * Check the state of the accessibility tree.
  * @param  {document} doc       panel documnent.
  * @param  {Array}    expected  an array that represents an expected row list.
  */
 async function checkTreeState(doc, expected) {
   info("Checking tree state.");
   const hasExpectedStructure = await BrowserTestUtils.waitForCondition(() =>
     [...doc.querySelectorAll(".treeRow")].every((row, i) => {
-      const { role, name, badges, selected } = expected[i];
+      const { role, name, badges, selected, level } = expected[i];
       return row.querySelector(".treeLabelCell").textContent === role &&
         row.querySelector(".treeValueCell").textContent === name &&
         compareBadges(row.querySelector(".badges"), badges) &&
-        checkSelected(row, selected);
+        checkSelected(row, selected) &&
+        checkLevel(row, level);
     }), "Wait for the right tree update.");
 
   ok(hasExpectedStructure, "Tree structure is correct.");
 }
 
 /**
  * Check if relations object matches what is expected. Note: targets are matched by their
  * name and role.
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
@@ -1,11 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`AccessibilityTreeFilter component: audit filter filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button checked\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"false\\" class=\\"badge toggle-button checked\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button checked devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"true\\" class=\\"badge toggle-button checked devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter not filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter not filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter not filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter not filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"true\\" class=\\"badge toggle-button devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: toggle filter 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: toggle filter 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/badges.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/badges.test.js.snap
@@ -1,13 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Badges component: contrast ratio fail range render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span></span>"`;
+exports[`Badges component: contrast ratio fail range render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span></span>"`;
 
-exports[`Badges component: contrast ratio fail render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span></span>"`;
+exports[`Badges component: contrast ratio fail render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span></span>"`;
 
 exports[`Badges component: contrast ratio success render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"></span>"`;
 
 exports[`Badges component: empty checks render 1`] = `null`;
 
 exports[`Badges component: no props render 1`] = `null`;
 
 exports[`Badges component: null checks render 1`] = `null`;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/contrast-badge.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/contrast-badge.test.js.snap
@@ -1,11 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ContrastBadge component: error render 1`] = `null`;
 
-exports[`ContrastBadge component: fail render 1`] = `"<span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span>"`;
+exports[`ContrastBadge component: fail render 1`] = `"<span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span>"`;
 
 exports[`ContrastBadge component: success large text render 1`] = `null`;
 
 exports[`ContrastBadge component: success range render 1`] = `null`;
 
 exports[`ContrastBadge component: success render 1`] = `null`;
--- a/devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
+++ b/devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
@@ -33,19 +33,19 @@ describe("AccessibilityTreeFilter compon
     expect(toolbar.is("div")).toBe(true);
     expect(toolbar.prop("role")).toBe("toolbar");
 
     const filterButtons = filters.find(ToggleButton);
     expect(filterButtons.length).toBe(1);
 
     const button = filterButtons.at(0).childAt(0);
     expect(button.is("button")).toBe(true);
-    expect(button.hasClass("audit-badge")).toBe(true);
     expect(button.hasClass("badge")).toBe(true);
     expect(button.hasClass("toggle-button")).toBe(true);
+    expect(button.hasClass("audit-badge")).toBe(false);
     expect(button.prop("aria-pressed")).toBe(false);
     expect(button.text()).toBe("accessibility.badge.contrast");
   });
 
   it("audit filter filtered", () => {
     const store = setupStore({
       preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
     });
@@ -100,18 +100,18 @@ describe("AccessibilityTreeFilter compon
 
   it("toggle filter", () => {
     const store = setupStore();
     const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
     expect(wrapper.html()).toMatchSnapshot();
 
     const filterInstance = wrapper.find(AccessibilityTreeFilterClass).instance();
     filterInstance.toggleFilter = jest.fn();
-    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: " " });
+    wrapper.find("button.toggle-button.badge").simulate("keydown", { key: " " });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(1);
 
-    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: "Enter" });
+    wrapper.find("button.toggle-button.badge").simulate("keydown", { key: "Enter" });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(2);
 
-    wrapper.find("button.audit-badge.badge").simulate("click", { clientX: 1 });
+    wrapper.find("button.toggle-button.badge").simulate("click", { clientX: 1 });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(3);
   });
 });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
@@ -49,17 +49,18 @@ function generateDefaults(overrides) {
   };
 }
 const LongStringClientMock = require("../__mocks__/long-string-client");
 
 function mount(props, { initialState } = {}) {
   const client = {
     createObjectClient: grip =>
       ObjectClient(grip, {
-        getPrototype: () => Promise.resolve(protoStub)
+        getPrototype: () => Promise.resolve(protoStub),
+        getProxySlots: () => Promise.resolve(gripRepStubs.get("testProxySlots"))
       }),
 
     createLongStringClient: grip =>
       LongStringClientMock(grip, {
         substring: function(initiaLength, length, cb) {
           cb({
             substring: "<<<<"
           });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
@@ -1,17 +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/>. */
 
 /* global jest */
 const { mountObjectInspector } = require("../test-utils");
 
 const { MODE } = require("../../../reps/constants");
-const stub = require("../../../reps/stubs/grip").get("testProxy");
+const gripStubs = require("../../../reps/stubs/grip");
+const stub = gripStubs.get("testProxy");
+const proxySlots = gripStubs.get("testProxySlots");
 const { formatObjectInspector } = require("../test-utils");
 
 const ObjectClient = require("../__mocks__/object-client");
 function generateDefaults(overrides) {
   return {
     roots: [
       {
         path: "root",
@@ -27,63 +29,72 @@ function generateDefaults(overrides) {
 function getEnumPropertiesMock() {
   return jest.fn(() => ({
     iterator: {
       slice: () => ({})
     }
   }));
 }
 
+function getProxySlotsMock() {
+  return jest.fn(() => proxySlots);
+}
+
 function mount(props, { initialState } = {}) {
   const enumProperties = getEnumPropertiesMock();
+  const getProxySlots = getProxySlotsMock();
 
   const client = {
-    createObjectClient: grip => ObjectClient(grip, { enumProperties })
+    createObjectClient: grip =>
+      ObjectClient(grip, { enumProperties, getProxySlots })
   };
 
   const obj = mountObjectInspector({
     client,
     props: generateDefaults(props),
     initialState
   });
 
-  return { ...obj, enumProperties };
+  return { ...obj, enumProperties, getProxySlots };
 }
 
 describe("ObjectInspector - Proxy", () => {
   it("renders Proxy as expected", () => {
-    const { wrapper, enumProperties } = mount(
+    const { wrapper, enumProperties, getProxySlots } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     expect(formatObjectInspector(wrapper)).toMatchSnapshot();
 
     // enumProperties should not have been called.
     expect(enumProperties.mock.calls).toHaveLength(0);
+
+    // getProxySlots should not have been called.
+    expect(getProxySlots.mock.calls).toHaveLength(0);
   });
 
   it("calls enumProperties on <target> and <handler> clicks", () => {
     const { wrapper, enumProperties } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     const nodes = wrapper.find(".node");
 
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
@@ -70,24 +70,25 @@ describe("getChildren", () => {
     expect(paths).toEqual([
       "Symbol(rootpath/x)",
       "Symbol(rootpath/<get x()>)",
       "Symbol(rootpath/<set x()>)"
     ]);
   });
 
   it("returns the expected nodes for Proxy", () => {
-    const nodes = getChildren({
-      item: createNode({
-        name: "root",
-        path: "rootpath",
-        contents: { value: gripStubs.get("testProxy") }
-      })
+    const proxyNode = createNode({
+      name: "root",
+      path: "rootpath",
+      contents: { value: gripStubs.get("testProxy") }
     });
-
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const nodes = getChildren({ item: proxyNode, loadedProperties });
     const names = nodes.map(n => n.name);
     const paths = nodes.map(n => n.path.toString());
 
     expect(names).toEqual(["<target>", "<handler>"]);
     expect(paths).toEqual([
       "Symbol(rootpath/<target>)",
       "Symbol(rootpath/<handler>)"
     ]);
@@ -243,17 +244,17 @@ describe("getChildren", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
       contents: { value: gripStubs.get("testProxy") }
     });
     const children = getChildren({
       cachedNodes,
       item: node,
-      loadedProperties: new Map([[node.path, { prototype: {} }]])
+      loadedProperties: new Map([[node.path, gripStubs.get("testProxySlots")]])
     });
     expect(cachedNodes.get(node.path)).toBe(children);
   });
 
   it("doesn't cache children on node with buckets and no loaded props", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
@@ -205,17 +205,20 @@ describe("shouldLoadItemIndexedPropertie
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
@@ -168,17 +168,20 @@ describe("shouldLoadItemNonIndexedProper
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemNonIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
@@ -147,34 +147,37 @@ describe("shouldLoadItemPrototype", () =
     expect(shouldLoadItemPrototype(defaultPropertiesNode)).toBeFalsy();
   });
 
   it("returns false for a MapEntry node", () => {
     const node = GripMapEntryRep.createGripMapEntry("key", "value");
     expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
-  it("returns true for a Proxy node", () => {
+  it("returns false for a Proxy node", () => {
     const node = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    expect(shouldLoadItemPrototype(node)).toBeTruthy();
+    expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemPrototype(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
@@ -164,17 +164,20 @@ describe("shouldLoadItemSymbols", () => 
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemSymbols(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
@@ -68,17 +68,18 @@ export type PropertiesIterator = {
   count: number,
   slice: (start: number, count: number) => Promise<GripProperties>
 };
 
 export type ObjectClient = {
   enumEntries: () => Promise<PropertiesIterator>,
   enumProperties: (options: Object) => Promise<PropertiesIterator>,
   enumSymbols: () => Promise<PropertiesIterator>,
-  getPrototype: () => Promise<{ prototype: Object }>
+  getPrototype: () => Promise<{ prototype: Object }>,
+  getProxySlots: () => Promise<{ proxyTarget: Object, proxyHandler: Object }>
 };
 
 export type LongStringClient = {
   substring: (
     start: number,
     end: number,
     response: {
       substring?: string,
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
@@ -112,16 +112,22 @@ async function getFullText(
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(
+  objectClient: ObjectClient
+): Promise<{ proxyTarget?: Object, proxyHandler?: Object }> {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(
   iterator: PropertiesIterator,
   start: ?number,
   end: ?number
 ): Promise<GripProperties> {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
@@ -132,10 +138,11 @@ function iteratorSlice(
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = require("./client");
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -72,16 +73,20 @@ function loadItemProperties(
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses: Array<Object>): Object {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -94,16 +99,21 @@ function mergeResponses(responses: Array
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
@@ -168,17 +178,18 @@ function shouldLoadItemPrototype(
     value &&
     !loadedProperties.has(item.path) &&
     !nodeIsBucket(item) &&
     !nodeIsMapEntry(item) &&
     !nodeIsEntries(item) &&
     !nodeIsDefaultProperties(item) &&
     !nodeHasAccessors(item) &&
     !nodeIsPrimitive(item) &&
-    !nodeIsLongString(item)
+    !nodeIsLongString(item) &&
+    !nodeIsProxy(item)
   );
 }
 
 function shouldLoadItemSymbols(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ): boolean {
   const value = getValue(item);
@@ -199,18 +210,26 @@ function shouldLoadItemSymbols(
 
 function shouldLoadItemFullText(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(
+  item: Node,
+  loadedProperties: LoadedProperties = new Map()
+): boolean {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
@@ -341,18 +341,21 @@ function makeNodesForPromiseProperties(i
         type: NODE_TYPES.PROMISE_VALUE
       })
     );
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item: Node): Array<Node> {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(
+  loadedProps: GripProperties,
+  item: Node
+): Array<Node> {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [
     createNode({
       parent: item,
       name: "<target>",
       contents: { value: proxyTarget },
       type: NODE_TYPES.PROXY_TARGET
     }),
@@ -787,18 +790,18 @@ function getChildren(options: {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
@@ -298,32 +298,16 @@ stubs.set("testStringObject", {
     safeGetterValues: {},
     wrappedValue: "foo"
   }
 });
 stubs.set("testProxy", {
   type: "object",
   actor: "server1.conn1.child1/obj47",
   class: "Proxy",
-  proxyTarget: {
-    type: "object",
-    actor: "server1.conn1.child1/obj48",
-    class: "Object",
-    ownPropertyLength: 1
-  },
-  proxyHandler: {
-    type: "object",
-    actor: "server1.conn1.child1/obj49",
-    class: "Array",
-    ownPropertyLength: 4,
-    preview: {
-      kind: "ArrayLike",
-      length: 3
-    }
-  },
   preview: {
     kind: "Object",
     ownProperties: {
       "<target>": {
         value: {
           type: "object",
           actor: "server1.conn1.child1/obj48",
           class: "Object",
@@ -341,16 +325,34 @@ stubs.set("testProxy", {
             length: 3
           }
         }
       }
     },
     ownPropertiesLength: 2
   }
 });
+stubs.set("testProxySlots", {
+  proxyTarget: {
+    type: "object",
+    actor: "server1.conn1.child1/obj48",
+    class: "Object",
+    ownPropertyLength: 1
+  },
+  proxyHandler: {
+    type: "object",
+    actor: "server1.conn1.child1/obj49",
+    class: "Array",
+    ownPropertyLength: 4,
+    preview: {
+      kind: "ArrayLike",
+      length: 3
+    }
+  }
+});
 stubs.set("testArrayBuffer", {
   type: "object",
   actor: "server1.conn1.child1/obj170",
   class: "ArrayBuffer",
   extensible: true,
   frozen: false,
   sealed: false,
   ownPropertyLength: 0,
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
@@ -119,17 +119,18 @@ describe("Grip - Proxy", () => {
   const object = stubs.get("testProxy");
 
   it("correctly selects Grip Rep", () => {
     expect(getRep(object)).toBe(Grip.rep);
   });
 
   it("renders as expected", () => {
     const renderRep = props => shallowRenderRep(object, props);
-    const handlerLength = getGripLengthBubbleText(object.proxyHandler, {
+    const handler = object.preview.ownProperties["<handler>"].value;
+    const handlerLength = getGripLengthBubbleText(handler, {
       mode: MODE.TINY
     });
     const out = `Proxy { <target>: {…}, <handler>: ${handlerLength} […] }`;
 
     expect(renderRep({ mode: undefined }).text()).toBe(out);
     expect(renderRep({ mode: MODE.TINY }).text()).toBe("Proxy");
     expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
     expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
--- a/devtools/client/debugger/src/components/App.js
+++ b/devtools/client/debugger/src/components/App.js
@@ -265,24 +265,22 @@ class App extends Component<Props, State
       editorPane.dispatchEvent(new Event("resizeend"));
     }
   }
 
   renderLayout = () => {
     const { startPanelCollapsed, endPanelCollapsed } = this.props;
     const horizontal = this.isHorizontal();
 
-    const maxSize = horizontal ? "70%" : "95%";
-
     return (
       <SplitBox
         style={{ width: "100vw" }}
         initialSize={prefs.endPanelSize}
         minSize={30}
-        maxSize={maxSize}
+        maxSize="70%"
         splitterSize={1}
         vert={horizontal}
         onResizeEnd={num => {
           prefs.endPanelSize = num;
           this.triggerEditorPaneResize();
         }}
         startPanel={
           <SplitBox
--- a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
+++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
@@ -24,17 +24,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -160,17 +159,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -244,17 +242,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
       <span
         className="suffix"
       >
         (mapped)
       </span>
     </span>
@@ -389,17 +386,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
             <span
               className="suffix"
             >
               (mapped)
             </span>
           </span>
@@ -480,17 +476,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       root
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -620,17 +615,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             root
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -694,17 +688,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -802,17 +795,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             http://mdn.com
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -866,17 +858,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -976,17 +967,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             http://mdn.com
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1041,17 +1031,16 @@ Object {
       className="arrow expanded"
     />
     <AccessibleImage
       className="globe-small"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1129,17 +1118,16 @@ Object {
             className="arrow expanded"
           />
           <AccessibleImage
             className="globe-small"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1184,17 +1172,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="angular"
     />
     <span
       className="label"
     >
-       
       Angular
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1290,17 +1277,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="angular"
           />
           <span
             className="label"
           >
-             
             Angular
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1353,17 +1339,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1437,17 +1422,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1490,17 +1474,16 @@ Object {
       className="arrow expanded"
     />
     <AccessibleImage
       className="globe-small"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1578,17 +1561,16 @@ Object {
             className="arrow expanded"
           />
           <AccessibleImage
             className="globe-small"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1633,17 +1615,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1741,17 +1722,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1805,17 +1785,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="webpack"
     />
     <span
       className="label"
     >
-       
       Webpack
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1911,17 +1890,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="webpack"
           />
           <span
             className="label"
           >
-             
             Webpack
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1986,17 +1964,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2124,17 +2101,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -2209,17 +2185,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2349,17 +2324,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -2435,17 +2409,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       external file
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2573,17 +2546,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             external file
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
--- a/devtools/client/inspector/computed/test/browser.ini
+++ b/devtools/client/inspector/computed/test/browser.ini
@@ -29,20 +29,20 @@ support-files =
 [browser_computed_media-queries.js]
 [browser_computed_no-results-placeholder.js]
 [browser_computed_original-source-link.js]
 [browser_computed_pseudo-element_01.js]
 [browser_computed_refresh-on-style-change_01.js]
 [browser_computed_search-filter.js]
 [browser_computed_search-filter_clear.js]
 [browser_computed_search-filter_context-menu.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_search-filter_escape-keypress.js]
 [browser_computed_search-filter_noproperties.js]
 [browser_computed_select-and-copy-styles-01.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_select-and-copy-styles-02.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_style-editor-link.js]
 skip-if = true # bug 1307846
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -60,16 +60,17 @@ devtools.jar:
     skin/dark-theme.css (themes/dark-theme.css)
     skin/light-theme.css (themes/light-theme.css)
     skin/toolbars.css (themes/toolbars.css)
     skin/toolbox.css (themes/toolbox.css)
     skin/tooltips.css (themes/tooltips.css)
     skin/images/accessibility.svg (themes/images/accessibility.svg)
     skin/images/add.svg (themes/images/add.svg)
     skin/images/alert.svg (themes/images/alert.svg)
+    skin/images/alert-tiny.svg (themes/images/alert-tiny.svg)
     skin/images/arrow.svg (themes/images/arrow.svg)
     skin/images/arrow-big.svg (themes/images/arrow-big.svg)
     skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
     skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg)
     skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg)
     skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg)
     skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg)
     skin/images/checkbox.svg (themes/images/checkbox.svg)
--- a/devtools/client/locales/en-US/accessibility.properties
+++ b/devtools/client/locales/en-US/accessibility.properties
@@ -106,20 +106,20 @@ accessibility.description.oldVersion=You
 # context menu item for printing an accessible tree to JSON is rendered after triggering a
 # context menu for an accessible tree row.
 accessibility.tree.menu.printToJSON=Print to JSON
 
 # LOCALIZATION NOTE (accessibility.checks): A title text used for header for checks
 # section in Accessibility details sidebar.
 accessibility.checks=Checks
 
-# LOCALIZATION NOTE (accessibility.checks.empty): A title text used for indicating that
+# LOCALIZATION NOTE (accessibility.checks.empty2): A title text used for indicating that
 # accessibility checks for a node yielded no results and another node should be
 # selected.
-accessibility.checks.empty=Select another node to continue.
+accessibility.checks.empty2=No checks for this node.
 
 # LOCALIZATION NOTE (accessibility.contrast.header): A title text used for header for
 # checks related to color and contrast.
 accessibility.contrast.header=Color and Contrast
 
 # LOCALIZATION NOTE (accessibility.contrast.error): A title text for the color
 # contrast ratio, used when the tool is unable to calculate the contrast ratio value.
 accessibility.contrast.error=Unable to calculate
@@ -158,16 +158,22 @@ accessibility.contrast.annotation.fail=D
 accessibility.badges=Accessibility checks
 
 # LOCALIZATION NOTE (accessibility.badge.contrast): A title text for the badge
 # that is rendered within the accessible row in the accessibility tree for a
 # given accessible object that does not satisfy the WCAG guideline for colour
 # contrast.
 accessibility.badge.contrast=contrast
 
+# LOCALIZATION NOTE (accessibility.badge.contrast.warning): A label for the
+# badge and attached warning icon that is rendered within the accessible row in
+# the accessibility tree for a given accessible object that does not satisfy the
+# WCAG guideline for colour contrast.
+accessibility.badge.contrast.warning=contrast warning
+
 # LOCALIZATION NOTE (accessibility.badge.contrast.tooltip): A title text for the
 # badge tooltip that is rendered on mouse hover over the badge in the accessible
 # row in the accessibility tree for a given accessible object that does not
 # satisfy the WCAG guideline for colour contrast.
 accessibility.badge.contrast.tooltip=Does not meet WCAG standards for accessible text.
 
 # LOCALIZATION NOTE (accessibility.tree.filters): A title text for the toolbar
 # within the main accessibility panel that contains a list of filters to be for
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -162,16 +162,22 @@ webconsole.menu.openURL.label=Open URL i
 webconsole.menu.openURL.accesskey=T
 
 # LOCALIZATION NOTE (webconsole.menu.openInNetworkPanel.label)
 # Label used for a context-menu item displayed for network message logs. Clicking on it
 # opens the network message in the Network panel
 webconsole.menu.openInNetworkPanel.label=Open in Network Panel
 webconsole.menu.openInNetworkPanel.accesskey=N
 
+# LOCALIZATION NOTE (webconsole.menu.resendNetworkRequest.label)
+# Label used for a context-menu item displayed for network message logs. Clicking on it
+# resends the network request
+webconsole.menu.resendNetworkRequest.label=Resend Request
+webconsole.menu.resendNetworkRequest.accesskey=n
+
 # LOCALIZATION NOTE (webconsole.menu.storeAsGlobalVar.label)
 # Label used for a context-menu item displayed for object/variable logs. Clicking on it
 # creates a new global variable pointing to the logged variable.
 webconsole.menu.storeAsGlobalVar.label=Store as global variable
 webconsole.menu.storeAsGlobalVar.accesskey=S
 
 # LOCALIZATION NOTE (webconsole.menu.copyMessage.label)
 # Label used for a context-menu item displayed for any log. Clicking on it will copy the
--- a/devtools/client/netmonitor/src/actions/batching.js
+++ b/devtools/client/netmonitor/src/actions/batching.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   BATCH_ACTIONS,
   BATCH_ENABLE,
   BATCH_RESET,
+  BATCH_FLUSH,
 } = require("../constants");
 
 /**
  * Process multiple actions at once as part of one dispatch, and produce only one
  * state update at the end. This action is not processed by any reducer, but by a
  * special store enhancer.
  */
 function batchActions(actions) {
@@ -30,13 +31,20 @@ function batchEnable(enabled) {
 }
 
 function batchReset() {
   return {
     type: BATCH_RESET,
   };
 }
 
+function batchFlush() {
+  return {
+    type: BATCH_FLUSH,
+  };
+}
+
 module.exports = {
   batchActions,
   batchEnable,
   batchReset,
+  batchFlush,
 };
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -8,17 +8,17 @@ const {
   ADD_REQUEST,
   CLEAR_REQUESTS,
   CLONE_SELECTED_REQUEST,
   REMOVE_SELECTED_CUSTOM_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   UPDATE_REQUEST,
 } = require("../constants");
-const { getSelectedRequest } = require("../selectors/index");
+const { getSelectedRequest, getRequestById } = require("../selectors/index");
 
 function addRequest(id, data, batch) {
   return {
     type: ADD_REQUEST,
     id,
     data,
     meta: { batch },
   };
@@ -41,36 +41,41 @@ function cloneSelectedRequest() {
   return {
     type: CLONE_SELECTED_REQUEST,
   };
 }
 
 /**
  * Send a new HTTP request using the data in the custom request form.
  */
-function sendCustomRequest(connector) {
+function sendCustomRequest(connector, requestId = null) {
   return (dispatch, getState) => {
-    const selected = getSelectedRequest(getState());
+    let request;
+    if (requestId) {
+      request = getRequestById(getState(), requestId);
+    } else {
+      request = getSelectedRequest(getState());
+    }
 
-    if (!selected) {
+    if (!request) {
       return;
     }
 
     // Send a new HTTP request using the data in the custom request form
     const data = {
-      cause: selected.cause,
-      url: selected.url,
-      method: selected.method,
-      httpVersion: selected.httpVersion,
+      cause: request.cause,
+      url: request.url,
+      method: request.method,
+      httpVersion: request.httpVersion,
     };
-    if (selected.requestHeaders) {
-      data.headers = selected.requestHeaders.headers;
+    if (request.requestHeaders) {
+      data.headers = request.requestHeaders.headers;
     }
-    if (selected.requestPostData) {
-      data.body = selected.requestPostData.postData.text;
+    if (request.requestPostData) {
+      data.body = request.requestPostData.postData.text;
     }
 
     connector.sendHTTPRequest(data, (response) => {
       return dispatch({
         type: SEND_CUSTOM_REQUEST,
         id: response.eventActor.actor,
       });
     });
--- a/devtools/client/netmonitor/src/api.js
+++ b/devtools/client/netmonitor/src/api.js
@@ -198,11 +198,24 @@ NetMonitorAPI.prototype = {
     };
 
     this.harExportConnector = new Connector();
     this.harExportConnectorReady =
       this.connectBackend(this.harExportConnector, connection);
     await this.harExportConnectorReady;
     return this.harExportConnector;
   },
+
+  /**
+   * Resends a given network request
+   * @param {String} requestId
+   *        Id of the network request
+   */
+  resendRequest(requestId) {
+    // Flush queued requests.
+    this.store.dispatch(Actions.batchFlush());
+    // Send custom request with same url, headers and body as the request
+    // with the given requestId.
+    this.store.dispatch(Actions.sendCustomRequest(this.connector, requestId));
+  },
 };
 
 exports.NetMonitorAPI = NetMonitorAPI;
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const actionTypes = {
   ADD_REQUEST: "ADD_REQUEST",
   ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   BATCH_ENABLE: "BATCH_ENABLE",
+  BATCH_FLUSH: "BATCH_FLUSH",
   CLEAR_REQUESTS: "CLEAR_REQUESTS",
   CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS",
   CLONE_SELECTED_REQUEST: "CLONE_SELECTED_REQUEST",
   ENABLE_REQUEST_FILTER_TYPE_ONLY: "ENABLE_REQUEST_FILTER_TYPE_ONLY",
   OPEN_NETWORK_DETAILS: "OPEN_NETWORK_DETAILS",
   RESIZE_NETWORK_DETAILS: "RESIZE_NETWORK_DETAILS",
   ENABLE_PERSISTENT_LOGS: "ENABLE_PERSISTENT_LOGS",
   DISABLE_BROWSER_CACHE: "DISABLE_BROWSER_CACHE",
--- a/devtools/client/netmonitor/src/middleware/batching.js
+++ b/devtools/client/netmonitor/src/middleware/batching.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { BATCH_ACTIONS, BATCH_ENABLE, BATCH_RESET } = require("../constants");
+const { BATCH_ACTIONS, BATCH_ENABLE, BATCH_RESET, BATCH_FLUSH } = require("../constants");
 
 const REQUESTS_REFRESH_RATE = 50; // ms
 
 /**
  * Middleware that watches for actions with a "batch = true" value in their meta field.
  * These actions are queued and dispatched as one batch after a timeout.
  * Special actions that are handled by this middleware:
  * - BATCH_ENABLE can be used to enable and disable the batching.
@@ -25,16 +25,20 @@ function batchingMiddleware(store) {
       if (action.type === BATCH_ENABLE) {
         return setEnabled(action.enabled);
       }
 
       if (action.type === BATCH_RESET) {
         return resetQueue();
       }
 
+      if (action.type === BATCH_FLUSH) {
+        return flushQueue();
+      }
+
       if (action.meta && action.meta.batch) {
         if (!enabled) {
           next(action);
           return Promise.resolve();
         }
 
         queuedActions.push(action);
 
@@ -61,16 +65,22 @@ function batchingMiddleware(store) {
       queuedActions = [];
 
       if (flushTask) {
         flushTask.cancel();
         flushTask = null;
       }
     }
 
+    function flushQueue() {
+      if (flushTask) {
+        flushTask.runNow();
+      }
+    }
+
     function flushActions() {
       const actions = queuedActions;
       queuedActions = [];
 
       next({
         type: BATCH_ACTIONS,
         actions,
       });
--- a/devtools/client/shared/components/Accordion.js
+++ b/devtools/client/shared/components/Accordion.js
@@ -69,25 +69,25 @@ class Accordion extends Component {
   renderContainer(item, i) {
     const { buttons, className, component, componentProps, labelledby, header } = item;
     const opened = this.state.opened[i];
 
     return (
       li(
         {
           className,
-          "aria-expanded": opened,
           "aria-labelledby": labelledby,
           key: labelledby,
         },
         h2(
           {
             className: "accordion-header",
             id: labelledby,
             tabIndex: 0,
+            "aria-expanded": opened,
             onKeyDown: e => this.onHandleHeaderKeyDown(e, i),
             onClick: () => this.handleHeaderClick(i),
           },
           div(
             {
               className: `arrow theme-twisty${opened ? " open" : ""}`,
               role: "presentation",
             }
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1707,18 +1707,18 @@ function makeNodesForPromiseProperties(i
       contents: { value: value },
       type: NODE_TYPES.PROMISE_VALUE
     }));
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item) {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(loadedProps, item) {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [createNode({
     parent: item,
     name: "<target>",
     contents: { value: proxyTarget },
     type: NODE_TYPES.PROXY_TARGET
   }), createNode({
     parent: item,
@@ -2101,18 +2101,18 @@ function getChildren(options) {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
@@ -3476,17 +3476,18 @@ module.exports = {
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = __webpack_require__(197);
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -3530,16 +3531,20 @@ function loadItemProperties(item, create
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses) {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -3552,16 +3557,21 @@ function mergeResponses(responses) {
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
   const gripItem = getClosestGripNode(item);
   const value = getValue(gripItem);
@@ -3585,38 +3595,43 @@ function shouldLoadItemEntries(item, loa
   const value = getValue(gripItem);
 
   return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
 }
 
 function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
   return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemFullText(item, loadedProperties = new Map()) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
 
 /***/ }),
 
 /***/ 197:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -3701,33 +3716,38 @@ async function getFullText(longStringCli
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(objectClient) {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(iterator, start, end) {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
   if (count === 0) {
     return Promise.resolve({});
   }
   return iterator.slice(start, count);
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
 
 /***/ }),
 
 /***/ 2:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
--- a/devtools/client/shared/components/test/mochitest/accordion.snapshots.js
+++ b/devtools/client/shared/components/test/mochitest/accordion.snapshots.js
@@ -10,26 +10,26 @@ window._snapshots = {
       "className": "accordion",
       "tabIndex": -1,
     },
     "children": [
       {
         "type": "li",
         "props": {
           "className": "accordion-item-1",
-          "aria-expanded": false,
           "aria-labelledby": "label-id-1",
         },
         "children": [
           {
             "type": "h2",
             "props": {
               "className": "accordion-header",
               "id": "label-id-1",
               "tabIndex": 0,
+              "aria-expanded": false,
               "onKeyDown": "e => this.onHandleHeaderKeyDown(e, i)",
               "onClick": "() => this.handleHeaderClick(i)",
             },
             "children": [
               {
                 "type": "div",
                 "props": {
                   "className": "arrow theme-twisty",
@@ -41,26 +41,26 @@ window._snapshots = {
             ],
           },
         ],
       },
       {
         "type": "li",
         "props": {
           "className": "accordion-item-2",
-          "aria-expanded": false,
           "aria-labelledby": "label-id-2",
         },
         "children": [
           {
             "type": "h2",
             "props": {
               "className": "accordion-header",
               "id": "label-id-2",
               "tabIndex": 0,
+              "aria-expanded": false,
               "onKeyDown": "e => this.onHandleHeaderKeyDown(e, i)",
               "onClick": "() => this.handleHeaderClick(i)",
             },
             "children": [
               {
                 "type": "div",
                 "props": {
                   "className": "arrow theme-twisty",
@@ -86,26 +86,26 @@ window._snapshots = {
             ],
           },
         ],
       },
       {
         "type": "li",
         "props": {
           "className": "accordion-item-3",
-          "aria-expanded": true,
           "aria-labelledby": "label-id-3",
         },
         "children": [
           {
             "type": "h2",
             "props": {
               "className": "accordion-header",
               "id": "label-id-3",
               "tabIndex": 0,
+              "aria-expanded": true,
               "onKeyDown": "e => this.onHandleHeaderKeyDown(e, i)",
               "onClick": "() => this.handleHeaderClick(i)",
             },
             "children": [
               {
                 "type": "div",
                 "props": {
                   "className": "arrow theme-twisty open",
--- a/devtools/client/shared/components/tree/TreeRow.js
+++ b/devtools/client/shared/components/tree/TreeRow.js
@@ -23,16 +23,17 @@ define(function(require, exports, module
 
   const { focusableSelector } = require("devtools/client/shared/focus");
 
   const UPDATE_ON_PROPS = [
     "name",
     "open",
     "value",
     "loading",
+    "level",
     "selected",
     "active",
     "hasChildren",
   ];
 
   /**
    * This template represents a node in TreeView component. It's rendered
    * using <tr> element (the entire tree is one big <table>).
@@ -225,17 +226,17 @@ define(function(require, exports, module
     render() {
       const member = this.props.member;
       const decorator = this.props.decorator;
 
       const props = {
         id: this.props.id,
         ref: this.treeRowRef,
         role: "treeitem",
-        "aria-level": member.level,
+        "aria-level": member.level + 1,
         "aria-selected": !!member.selected,
         onClick: this.props.onClick,
         onContextMenu: this.props.onContextMenu,
         onKeyDownCapture: member.active ? this._onKeyDown : undefined,
         onMouseOver: this.props.onMouseOver,
         onMouseOut: this.props.onMouseOut,
       };
 
--- a/devtools/client/shared/components/tree/TreeView.js
+++ b/devtools/client/shared/components/tree/TreeView.js
@@ -60,16 +60,17 @@ define(function(require, exports, module
    * The tree is maintaining its (presentation) state, which consists
    * from list of expanded nodes and list of columns.
    *
    * Complete data provider interface:
    * var TreeProvider = {
    *   getChildren: function(object);
    *   hasChildren: function(object);
    *   getLabel: function(object, colId);
+   *   getLevel: function(object); // optional
    *   getValue: function(object, colId);
    *   getKey: function(object);
    *   getType: function(object);
    * }
    *
    * Complete tree decorator interface:
    * var TreeDecorator = {
    *   getRowClass: function(object);
@@ -516,17 +517,17 @@ define(function(require, exports, module
           object: child,
           // A label for the child node
           name: provider.getLabel(child),
           // Data type of the child node (used for CSS customization)
           type: type,
           // Class attribute computed from the type.
           rowClass: "treeRow-" + type,
           // Level of the child within the hierarchy (top == 0)
-          level: level,
+          level: provider.getLevel ? provider.getLevel(child, level) : level,
           // True if this node has children.
           hasChildren: hasChildren,
           // Value associated with this node (as provided by the data provider)
           value: value,
           // True if the node is expanded.
           open: this.isExpanded(nodePath),
           // Node path
           path: nodePath,
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -313,26 +313,28 @@ VariablesViewController.prototype = {
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function(aTarget, aGrip) {
     if (aGrip.class === "Proxy") {
-      this.addExpander(
-        aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
-        aGrip.proxyTarget);
-      this.addExpander(
-        aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
-        aGrip.proxyHandler);
-
-      // Refuse to play the proxy's stupid game and return immediately
+      // Refuse to play the proxy's stupid game and just expose the target and handler.
       const deferred = defer();
-      deferred.resolve();
+      const objectClient = this._getObjectClient(aGrip);
+      objectClient.getProxySlots(aResponse => {
+        const target = aTarget.addItem("<target>", { value: aResponse.proxyTarget },
+          { internalItem: true });
+        this.addExpander(target, aResponse.proxyTarget);
+        const handler = aTarget.addItem("<handler>", { value: aResponse.proxyHandler },
+          { internalItem: true });
+        this.addExpander(handler, aResponse.proxyHandler);
+        deferred.resolve();
+      });
       return deferred.promise;
     }
 
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state }, { internalItem: true });
       if (state === "fulfilled") {
         this.addExpander(
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/alert-tiny.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" width="10" height="10">
+  <path fill="context-fill" fill-rule="evenodd" d="M4.94.76a.5.5 0 0 0-.88 0l-4 7.5A.5.5 0 0 0 .5 9h8a.5.5 0 0 0 .44-.74l-4-7.5zM4.5 2.8a.7.7 0 0 0-.7.7v1.8a.7.7 0 1 0 1.4 0V3.5a.7.7 0 0 0-.7-.7zm0 5.4a.7.7 0 1 0 0-1.4.7.7 0 0 0 0 1.4z"/>
+</svg>
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -1,49 +1,69 @@
 /* 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/. */
 
 /* Webconsole specific theme variables */
 :root {
-  /* Output rows should be 20px tall for a single line of text;
-   * 20 = 3 (top padding) + 14 (line-height) + 3 (bottom padding)
-   */
-  --console-output-font-size: 11px;
-  --console-output-line-height: calc(14 / 11);
+  --console-output-line-height: 14px;
   --console-output-vertical-padding: 3px;
   /* Width of the left gutter where icons appear */
   --console-inline-start-gutter: 32px;
   /* Icons perfectly centered in the left gutter "feel" closer to the window
    * edge than to message text. This value pushes them slightly to the right. */
   --console-icon-horizontal-offset: 1px;
 }
 
 .theme-dark {
-  --console-output-icon-error-color: var(--red-40);
-  --console-output-icon-warning-color: var(--yellow-60);
-  --console-output-indent-border-color: var(--theme-highlight-blue);
+  --console-input-background: var(--theme-tab-toolbar-background);
+  --console-message-background: var(--theme-body-background);
+  --console-message-border: var(--theme-splitter-color);
+  --console-message-color: var(--theme-text-color-strong);
+  --console-error-background: hsl(345, 23%, 24%);
+  --console-error-border: hsl(345, 30%, 35%);
+  --console-error-color: var(--red-20);
+  --console-error-icon-color: var(--red-40);
+  --console-warning-background: hsl(42, 37%, 19%);
+  --console-warning-border: hsl(60, 30%, 26%);
+  --console-warning-color: hsl(43, 94%, 81%);
+  --console-warning-icon-color: var(--yellow-60);
+  --console-navigation-color: var(--theme-highlight-blue);
+  --console-navigation-border: var(--blue-60);
+  --console-indent-border-color: var(--theme-highlight-blue);
+  --console-repeat-bubble-background: var(--blue-60);
+
+  /* TODO in bug 1549195: colors used in shared components (e.g. Reps) should
+     be renamed and/or moved to variables.css so they work everywhere */
   --error-color: var(--red-20);
-  --error-background-color: hsl(345, 23%, 24%);
-  --warning-color: hsl(43, 94%, 81%);
-  --warning-background-color: hsl(42, 37%, 19%);
   --console-output-color: white;
-  --repeat-bubble-background-color: var(--blue-60);
 }
 
 .theme-light {
-  --console-output-icon-error-color: var(--red-60);
-  --console-output-icon-warning-color: var(--yellow-65);
-  --console-output-indent-border-color: var(--theme-highlight-blue);
+  --console-input-background: var(--theme-body-background);
+  --console-message-background: var(--theme-body-background);
+  --console-message-border: #f2f2f4; /* between Grey 10 and Grey 20 */
+  --console-message-color: var(--theme-text-color-strong);
+  --console-error-background: hsl(344, 73%, 97%);
+  --console-error-border: rgba(215, 0, 34, 0.12); /* Red 60 + opacity */
+  --console-error-color: var(--red-70);
+  --console-error-icon-color: var(--red-60);
+  --console-warning-background: hsl(54, 100%, 92%);
+  --console-warning-border: rgba(215, 182, 0, 0.28); /* Yellow 60 + opacity */
+  --console-warning-color: var(--yellow-80);
+  --console-warning-icon-color: var(--yellow-65);
+  --console-navigation-color: var(--theme-highlight-blue);
+  --console-navigation-border: var(--blue-30);
+  --console-indent-border-color: var(--theme-highlight-blue);
+  --console-repeat-bubble-background: var(--theme-highlight-blue);
+
+  /* TODO in bug 1549195: colors used in shared components (e.g. Reps) should
+     be renamed and/or moved to variables.css so they work everywhere */
   --error-color: var(--red-70);
-  --error-background-color: hsl(344, 73%, 97%);
-  --warning-color: var(--yellow-80);
-  --warning-background-color: hsl(54, 100%, 92%);
   --console-output-color: var(--grey-90);
-  --repeat-bubble-background-color: var(--theme-highlight-blue);
 }
 
 /* General output styles */
 
 a {
   -moz-user-focus: normal;
   cursor: pointer;
   text-decoration: underline;
@@ -52,97 +72,149 @@ a {
 /* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
  * assertion when loading HTML page with links in XUL iframe */
 *:visited { }
 
 * {
   box-sizing: border-box;
 }
 
+/*
+ * Stack messages on the z axis so that we can make their borders overlap
+ * and show color borders on top:
+ *
+ *   -----------------  <-- Red
+ *   Error message
+ *   -----------------  <-- Red
+ *   Normal message
+ *   -----------------  <-- Grey
+ *
+ * and:
+ *
+ *   -----------------  <-- Grey
+ *   Normal message
+ *   -----------------  <-- Red
+ *   Error message
+ *   -----------------  <-- Red
+ *
+ * The exact stacking order is:
+ * 
+ *   - z-index: 3 = Navigation and Paused markers
+ *   - z-index: 2 = Errors and warnings
+ *   - z-index: 1 = Other (console.log, console.info, requests, etc.)
+ */
 .message {
+  position: relative;
+  z-index: 1;
   display: flex;
   width: 100%;
+  /* Make the top border cover the previous message's bottom border */
+  margin-top: -1px;
+  border-top: 1px solid var(--console-message-border);
+  border-bottom: 1px solid var(--console-message-border);
   /* Avoid vertical padding, so that we can draw full-height items (e.g. indent guides).
-   * Use vertical margins on children instead. */
-  padding-inline-start: 1px;
+   * Use vertical margins of --console-output-vertical-padding on children instead. */
+  padding-block: 0;
+  /* Layout of the 32px-wide left gutter:
+   * | 4px message padding | 24px icon container | 4px icon margin |
+   * Note: on hover we show a 3px pseudo-border on top of the left padding. */
+  padding-inline-start: 4px;
   padding-inline-end: 8px;
-  border-inline-start: solid 3px transparent;
-  font-size: var(--console-output-font-size);
+  font-size: var(--theme-code-font-size);
   line-height: var(--console-output-line-height);
-  position: relative;
+  color: var(--console-message-color);
+  background-color: var(--console-message-background);
 }
 
 @media (min-width: 1000px) {
   .message {
     padding-inline-end: 12px;
   }
 }
 
+/* We already paint a top border on jsterm-input-container (and we need to keep
+ * it when scrolling console content), so remove the last item's border. */
+.message:last-child {
+  border-bottom-width: 0;
+}
+
+/*
+ * Show a blue border on the left side of rows on hover. Avoid using border
+ * properties because the top/bottom borders would not span the full width.
+ */
+.message:hover::before {
+  content: "";
+  position: absolute;
+  inset-inline: 0;
+  top: 0;
+  bottom: 0;
+  background: var(--theme-highlight-blue);
+  width: 3px;
+}
+
 /*
  * By default, prevent any element in message to overflow.
  * This makes console reflows faster (See Bug 1487457).
  */
 .message * {
   overflow: hidden;
 }
 
 /* Reset the overflow for the network info panel (when a network message is expanded) */
 .message .network-info * {
   overflow: unset;
 }
 
-.message:hover {
-  border-inline-start-color: var(--theme-highlight-blue);
-}
-
 .message.error {
-  color: var(--error-color);
-  background-color: var(--error-background-color);
-}
-
-.message.navigationMarker {
-  border-top: 1px solid var(--theme-emphasized-splitter-color);
-  color: var(--object-color);
-}
-
-/* Removes the top border for message in console (See Bug 1512621). */
-.webconsole-output:first-child.message.navigationMarker {
-  border-top: none;
+  z-index: 2;
+  color: var(--console-error-color);
+  border-color: var(--console-error-border);
+  background-color: var(--console-error-background);
 }
 
 .message.warn {
-  color: var(--warning-color);
-  background-color: var(--warning-background-color);
+  z-index: 2;
+  color: var(--console-warning-color);
+  border-color: var(--console-warning-border);
+  background-color: var(--console-warning-background);
+}
+
+.message.navigationMarker {
+  z-index: 3;
+  color: var(--console-navigation-color);
+  border-color: var(--console-navigation-border);
 }
 
-.message.paused::before {
-  background: var(--purple-50);
-  opacity: 0.6;
-  width: 100vw;
-  height: 1px;
-  bottom: 0px;
-  left: -3px;
-  display: block;
-  content: "";
-  position: absolute;
+.message.paused {
+  z-index: 3;
 }
 
-.message.paused.paused-before::before {
-  top: 0px;
-  bottom: inherit;
+.message.paused.paused-before {
+  border-top-color: var(--purple-50);
+}
+
+.message.paused:not(.paused-before) {
+  border-bottom-color: var(--purple-50);
+  /* always show the border, even for the last child */
+  border-bottom-width: 1px;
 }
 
 .message.paused ~ .message:not(.command):not(.result) .message-body-wrapper,
 .message.paused.paused-before .message-body-wrapper {
   opacity: 0.5;
 }
 
 .message.startGroup,
 .message.startGroupCollapsed {
-  --console-output-indent-border-color: transparent;
+  --console-indent-border-color: transparent;
+}
+
+/* Hide border between a command and its result */
+.message.command + .result.log {
+  border-top-width: 0;
 }
 
 .message > .prefix,
 .message > .timestamp {
   flex: none;
   color: var(--theme-comment);
   margin: var(--console-output-vertical-padding) 4px;
 }
@@ -151,22 +223,25 @@ a {
   .message > .timestamp {
     display: none;
   }
 }
 
 .message > .indent {
   flex: none;
   display: inline-block;
+  /* Display indent borders above the message's top and bottom border.
+   * This avoids interrupted indent lines (looking like dashes). */
+  margin-block: -1px;
   margin-inline-start: 12px;
-  border-inline-end: solid 1px var(--console-output-indent-border-color);
+  border-inline-end: solid 1px var(--console-indent-border-color);
 }
 
 .message > .indent.warning-indent {
-  border-inline-end-color: var(--warning-color);
+  border-inline-end-color: var(--console-warning-color);
 }
 
 .message > .indent[data-indent="0"] {
   display: none;
 }
 
 /* Center first level indent within the left gutter */
 .message:not(.startGroup):not(.startGroupCollapsed) > .indent[data-indent="1"] {
@@ -208,27 +283,27 @@ a {
 }
 
 .message.info > .icon {
   color: var(--theme-icon-color);
   background-image: url(chrome://devtools/skin/images/webconsole/info.svg);
 }
 
 .message.error > .icon {
-  color: var(--console-output-icon-error-color);
+  color: var(--console-error-icon-color);
   background-image: url(chrome://devtools/skin/images/webconsole/error.svg);
 }
 
 .message.warn > .icon {
-  color: var(--console-output-icon-warning-color);
+  color: var(--console-warning-icon-color);
   background-image: url(chrome://devtools/skin/images/alert.svg);
 }
 
 .message.navigationMarker > .icon {
-  color: var(--object-color);
+  color: var(--console-navigation-color);
   background-image: url(chrome://devtools/skin/images/webconsole/navigation.svg);
 }
 
 .message:hover > .icon.rewindable {
   background-image: url(chrome://devtools/skin/images/next-circle.svg);
   cursor: pointer;
   transform: rotate(180deg);
 }
@@ -273,27 +348,27 @@ a {
   font: message-box;
   font-size: 0.8em;
   font-weight: normal;
 }
 
 .message-repeats {
   display: inline-block;
   color: white;
-  background-color: var(--repeat-bubble-background-color);
+  background-color: var(--console-repeat-bubble-background);
 }
 
 .message-repeats[value="1"] {
   display: none;
 }
 
 .warning-group-badge {
   display: inline-block;
-  color: var(--warning-background-color);
-  background-color: var(--warning-color);
+  color: var(--console-warning-background);
+  background-color: var(--console-warning-color);
 }
 
 .message-location {
   max-width: 40vw;
   flex-shrink: 0;
   color: var(--frame-link-source);
   margin-left: 1ch;
   /* Makes the file name truncated (and ellipsis shown) on the left side */
@@ -395,17 +470,17 @@ a {
 
 html .jsterm-input-node-html,
 html #webconsole-notificationbox {
   flex: 0;
   width: 100vw;
 }
 
 .jsterm-input-container {
-  background-color: var(--theme-tab-toolbar-background);
+  background-color: var(--console-input-background);
   border-top: 1px solid var(--theme-splitter-color);
   position: relative;
 }
 
 .jsterm-input-node {
   box-sizing: border-box;
   min-height: 100%;
   color: var(--theme-text-color-strong);
@@ -418,23 +493,16 @@ html #webconsole-notificationbox {
   -moz-context-properties: fill;
   fill: var(--theme-icon-dimmed-color);
 }
 
 .jsterm-complete-node {
   color: var(--theme-comment);
 }
 
-.theme-light .jsterm-input-container {
-  /* For light theme use a white background for the input - it looks better
-     than off-white */
-  background-color: #fff;
-  border-top-color: #e0e0e0;
-}
-
 textarea.jsterm-input-node,
 textarea.jsterm-complete-node {
   width: 100%;
   margin: 0;
   border: none;
   background-color: transparent;
   resize: none;
   font-size: inherit;
@@ -455,17 +523,17 @@ textarea.jsterm-complete-node {
 textarea.jsterm-input-node:focus {
   fill: var(--theme-icon-checked-color);
   box-shadow: none;
   outline: none;
 }
 
 /* CodeMirror-powered JsTerm */
 .jsterm-cm .jsterm-input-container > .CodeMirror {
-  font-size: var(--console-output-font-size);
+  font-size: var(--theme-code-font-size);
   line-height: var(--console-output-line-height);
   /* aim for a 32px left space (a descendent has 4px padding) */
   padding-inline-start: calc(var(--console-inline-start-gutter) - 4px);
   /* input icon */
   background-image: url(chrome://devtools/skin/images/webconsole/input.svg);
   background-position-x: calc(10px + var(--console-icon-horizontal-offset));
   background-position-y: 5px;
   background-repeat: no-repeat;
@@ -675,17 +743,17 @@ a.learn-more-link.webconsole-learn-more-
 .theme-dark .message.warn .objectLeftBrace,
 .theme-dark .message.warn .objectRightBrace,
 .theme-dark .message.warn .arrayLeftBracket,
 .theme-dark .message.warn .arrayRightBracket {
   color: var(--theme-body-color);
 }
 .theme-dark .message.error .tree.object-inspector,
 .theme-dark .message.warn .tree.object-inspector {
-  --console-output-indent-border-color: var(--theme-body-color);
+  --console-indent-border-color: var(--theme-body-color);
 }
 
 .webconsole-app .message-flex-body > .message-body {
   overflow: hidden;
 }
 
 .webconsole-app .message-body > * {
   flex-shrink: 0;
@@ -892,17 +960,17 @@ body {
 
 /* Object Inspector */
 .webconsole-app .object-inspector.tree {
   display: inline-block;
   max-width: 100%;
 }
 
 .webconsole-app .object-inspector.tree .tree-indent {
-  border-inline-start-color: var(--console-output-indent-border-color);
+  border-inline-start-color: var(--console-indent-border-color);
 }
 
 .webconsole-app .object-inspector.tree .tree-node:hover:not(.focused) {
   background-color: var(--object-inspector-hover-background);
 }
 
 /*
  * Make console.group, exception and XHR message's arrow look the same as the arrow
--- a/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
+++ b/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
@@ -88,18 +88,27 @@ function ConsoleApiCall(props) {
     messageBody = dom.span({className: "cm-variable"}, "console.table()");
   } else if (parameters) {
     messageBody = formatReps(messageBodyConfig);
     if (prefix) {
       messageBody.unshift(dom.span({
         className: "console-message-prefix",
       }, `${prefix}: `));
     }
-  } else {
+  } else if (typeof messageText === "string") {
     messageBody = messageText;
+  } else if (messageText) {
+    messageBody = GripMessageBody({
+      dispatch,
+      messageId,
+      grip: messageText,
+      serviceContainer,
+      useQuotes: false,
+      type,
+    });
   }
 
   let attachment = null;
   if (type === "table") {
     attachment = ConsoleTable({
       dispatch,
       id: message.id,
       serviceContainer,
@@ -135,16 +144,17 @@ function ConsoleApiCall(props) {
     stacktrace,
     attachment,
     serviceContainer,
     dispatch,
     indent,
     timeStamp,
     timestampsVisible,
     parameters,
+    message,
     maybeScrollToBottom,
   });
 }
 
 function formatReps(options = {}) {
   const {
     dispatch,
     loadedObjectProperties,
--- a/devtools/client/webconsole/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/components/message-types/NetworkEventMessage.js
@@ -22,16 +22,17 @@ const Services = require("Services");
 const isMacOS = Services.appinfo.OS === "Darwin";
 
 NetworkEventMessage.displayName = "NetworkEventMessage";
 
 NetworkEventMessage.propTypes = {
   message: PropTypes.object.isRequired,
   serviceContainer: PropTypes.shape({
     openNetworkPanel: PropTypes.func.isRequired,
+    resendNetworkRequest: PropTypes.func.isRequired,
   }),
   timestampsVisible: PropTypes.bool.isRequired,
   networkMessageUpdate: PropTypes.object.isRequired,
 };
 
 /**
  * This component is responsible for rendering network messages
  * in the Console panel.
--- a/devtools/client/webconsole/test/components/console-api-call.log-messages.test.js
+++ b/devtools/client/webconsole/test/components/console-api-call.log-messages.test.js
@@ -3,16 +3,18 @@
 "use strict";
 
 // Test utils.
 const expect = require("expect");
 const { render } = require("enzyme");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
+const { setupStore } = require("devtools/client/webconsole/test/helpers");
+const Provider = createFactory(require("react-redux").Provider);
 
 // Components under test.
 const ConsoleApiCall = createFactory(require("devtools/client/webconsole/components/message-types/ConsoleApiCall"));
 
 const { prepareMessage } = require("devtools/client/webconsole/utils/messages");
 const serviceContainer = require("devtools/client/webconsole/test/fixtures/serviceContainer");
 
 describe("ConsoleAPICall component:", () => {
@@ -31,16 +33,29 @@ describe("ConsoleAPICall component:", ()
       const message = prepareMessage(logMessageStubPacket, {getNextId: () => "1"});
       const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
 
       expect(wrapper.find(".message-body").text()).toBe("foobar test");
 
       // There should not be the location
       expect(wrapper.find(".message-location").text()).toBe("");
     });
+
+    it("renders longString logMessage grips", () => {
+      const message =
+        prepareMessage(logMessageLongStringStubPacket, {getNextId: () => "1"});
+
+      // We need to wrap the ConsoleApiElement in a Provider in order for the
+      // ObjectInspector to work.
+      const wrapper = render(
+        Provider({ store: setupStore() }, ConsoleApiCall({ message, serviceContainer }))
+      );
+
+      expect(wrapper.find(".message-body").text()).toInclude(initialText);
+    });
   });
 });
 
 // Stub packet
 const cachedLogMessageStubPacket = {
   "from": "server1.conn1.consoleActor2",
   "message": "foobar test",
   "timeStamp": "1493370184067",
@@ -48,8 +63,25 @@ const cachedLogMessageStubPacket = {
 };
 
 const logMessageStubPacket = {
   "from": "server1.conn0.consoleActor2",
   "type": "logMessage",
   "message": "foobar test",
   "timeStamp": 1519052480060,
 };
+
+const multilineFullText = `a\n${Array(20000)
+  .fill("a")
+  .join("")}`;
+const fullTextLength = multilineFullText.length;
+const initialText = multilineFullText.substring(0, 10000);
+const logMessageLongStringStubPacket = {
+  "from": "server1.conn0.consoleActor2",
+  "type": "logMessage",
+  "message": {
+    type: "longString",
+    initial: initialText,
+    length: fullTextLength,
+    actor: "server1.conn1.child1/longString58",
+  },
+  "timeStamp": 1519052480060,
+};
--- a/devtools/client/webconsole/test/fixtures/serviceContainer.js
+++ b/devtools/client/webconsole/test/fixtures/serviceContainer.js
@@ -10,16 +10,17 @@ module.exports = {
   proxy: {
     client: {},
     releaseActor: actor => console.log("Release actor", actor),
   },
   onViewSourceInDebugger: () => {},
   onViewSourceInStyleEditor: () => {},
   onViewSourceInScratchpad: () => {},
   openNetworkPanel: () => {},
+  resendNetworkRequest: () => {},
   sourceMapService: {
     subscribe: () => {},
     originalPositionFor: () => {
       return new Promise(resolve => {
         resolve();
       });
     },
   },
--- a/devtools/client/webconsole/test/fixtures/stubs/evaluationResult.js
+++ b/devtools/client/webconsole/test/fixtures/stubs/evaluationResult.js
@@ -101,61 +101,18 @@ stubPreparedMessages.set(`1 + @`, new Co
   "level": "error",
   "category": null,
   "messageText": "SyntaxError: illegal character",
   "parameters": [
     {
       "type": "undefined"
     }
   ],
-  "repeatId": "{\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":[{\"type\":\"undefined\"}],\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null,\"stacktrace\":[{\"filename\":\"resource://devtools/server/actors/webconsole/eval-with-debugger.js\",\"sourceId\":null,\"lineNumber\":134,\"columnNumber\":28,\"functionName\":\"getEvalResult\"},{\"filename\":\"resource://devtools/server/actors/webconsole/eval-with-debugger.js\",\"sourceId\":null,\"lineNumber\":105,\"columnNumber\":18,\"functionName\":\"exports.evalWithDebugger\"},{\"filename\":\"resource://devtools/server/actors/webconsole.js\",\"sourceId\":null,\"lineNumber\":1005,\"columnNumber\":22,\"functionName\":\"evaluateJS\"},{\"filename\":\"self-hosted\",\"sourceId\":null,\"lineNumber\":1005,\"columnNumber\":17,\"functionName\":\"evaluateJS\"},{\"filename\":\"resource://devtools/server/main.js\",\"sourceId\":null,\"lineNumber\":1291,\"columnNumber\":58,\"functionName\":\"onPacket\"},{\"filename\":\"resource://devtools/shared/transport/child-transport.js\",\"sourceId\":null,\"lineNumber\":66,\"columnNumber\":16,\"functionName\":\"receiveMessage\"}]}",
-  "stacktrace": [
-    {
-      "filename": "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
-      "sourceId": null,
-      "lineNumber": 134,
-      "columnNumber": 28,
-      "functionName": "getEvalResult"
-    },
-    {
-      "filename": "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
-      "sourceId": null,
-      "lineNumber": 105,
-      "columnNumber": 18,
-      "functionName": "exports.evalWithDebugger"
-    },
-    {
-      "filename": "resource://devtools/server/actors/webconsole.js",
-      "sourceId": null,
-      "lineNumber": 1005,
-      "columnNumber": 22,
-      "functionName": "evaluateJS"
-    },
-    {
-      "filename": "self-hosted",
-      "sourceId": null,
-      "lineNumber": 1005,
-      "columnNumber": 17,
-      "functionName": "evaluateJS"
-    },
-    {
-      "filename": "resource://devtools/server/main.js",
-      "sourceId": null,
-      "lineNumber": 1291,
-      "columnNumber": 58,
-      "functionName": "onPacket"
-    },
-    {
-      "filename": "resource://devtools/shared/transport/child-transport.js",
-      "sourceId": null,
-      "lineNumber": 66,
-      "columnNumber": 16,
-      "functionName": "receiveMessage"
-    }
-  ],
+  "repeatId": "{\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":[{\"type\":\"undefined\"}],\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null,\"stacktrace\":null}",
+  "stacktrace": null,
   "frame": {
     "source": "debugger eval code",
     "line": 1,
     "column": 4
   },
   "groupId": null,
   "errorMessageName": "JSMSG_ILLEGAL_CHARACTER",
   "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
@@ -485,60 +442,17 @@ stubPackets.set(`1 + @`, {
       "stack": "",
       "fileName": "debugger eval code",
       "lineNumber": 1,
       "columnNumber": 4
     }
   },
   "exceptionMessage": "SyntaxError: illegal character",
   "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
-  "exceptionStack": [
-    {
-      "filename": "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
-      "sourceId": null,
-      "lineNumber": 134,
-      "columnNumber": 28,
-      "functionName": "getEvalResult"
-    },
-    {
-      "filename": "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
-      "sourceId": null,
-      "lineNumber": 105,
-      "columnNumber": 18,
-      "functionName": "exports.evalWithDebugger"
-    },
-    {
-      "filename": "resource://devtools/server/actors/webconsole.js",
-      "sourceId": null,
-      "lineNumber": 1005,
-      "columnNumber": 22,
-      "functionName": "evaluateJS"
-    },
-    {
-      "filename": "self-hosted",
-      "sourceId": null,
-      "lineNumber": 1005,
-      "columnNumber": 17,
-      "functionName": "evaluateJS"
-    },
-    {
-      "filename": "resource://devtools/server/main.js",
-      "sourceId": null,
-      "lineNumber": 1291,
-      "columnNumber": 58,
-      "functionName": "onPacket"
-    },
-    {
-      "filename": "resource://devtools/shared/transport/child-transport.js",
-      "sourceId": null,
-      "lineNumber": 66,
-      "columnNumber": 16,
-      "functionName": "receiveMessage"
-    }
-  ],
+  "exceptionStack": null,
   "errorMessageName": "JSMSG_ILLEGAL_CHARACTER",
   "frame": {
     "source": "debugger eval code",
     "line": 1,
     "column": 4
   },
   "helperResult": null,
   "notes": null
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -335,16 +335,17 @@ skip-if = true #	Bug 1404382
 [browser_webconsole_message_categories.js]
 [browser_webconsole_multiple_windows_and_tabs.js]
 [browser_webconsole_network_attach.js]
 [browser_webconsole_network_exceptions.js]
 [browser_webconsole_network_messages_expand.js]
 skip-if = true  # Bug 1438979
 [browser_webconsole_network_message_ctrl_click.js]
 [browser_webconsole_network_messages_openinnet.js]
+[browser_webconsole_network_messages_resend_request.js]
 [browser_webconsole_network_messages_status_code.js]
 [browser_webconsole_network_requests_from_chrome.js]
 [browser_webconsole_network_reset_filter.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
 [browser_webconsole_non_javascript_mime_warning.js]
 [browser_webconsole_non_javascript_mime_worker_error.js]
 [browser_webconsole_object_ctrl_click.js]
@@ -352,16 +353,17 @@ skip-if = true  # Bug 1438979
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_object_inspector__proto__.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_object_inspector_getters.js]
 [browser_webconsole_object_inspector_getters_prototype.js]
 [browser_webconsole_object_inspector_getters_shadowed.js]
 [browser_webconsole_object_inspector_key_sorting.js]
 [browser_webconsole_object_inspector_local_session_storage.js]
+[browser_webconsole_object_inspector_nested_proxy.js]
 [browser_webconsole_object_inspector_selected_text.js]
 [browser_webconsole_object_inspector_scroll.js]
 [browser_webconsole_object_inspector_while_debugging_and_inspecting.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_optimized_out_vars.js]
 [browser_webconsole_output_copy.js]
 tags = clipboard
 [browser_webconsole_output_copy_newlines.js]
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_error.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_error.js
@@ -14,9 +14,23 @@ const TEST_URI = "http://example.com/bro
 add_task(async function() {
   const hud = await openNewTabAndConsole(TEST_URI);
 
   hud.jsterm.execute("throwErrorObject()");
   await checkMessageStack(hud, "ThrowErrorObject", [6, 1]);
 
   hud.jsterm.execute("throwValue(40 + 2)");
   await checkMessageStack(hud, "42", [14, 10, 1]);
+
+  hud.jsterm.execute(`
+    a = () => {throw "bloop"};
+    b =  () => a();
+    c =  () => b();
+    d =  () => c();
+    d();
+  `);
+  await checkMessageStack(hud, "Error: bloop", [2, 3, 4, 5, 6]);
+
+  hud.jsterm.execute(`1 + @`);
+  const messageNode = await waitFor(() => findMessage(hud, "illegal character"));
+  is(messageNode.querySelector(".frames"), null,
+    "There's no stacktrace for a SyntaxError evaluation");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_resend_request.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Test that 'Resend Request' context menu " +
+                "item resends the selected request and select it in netmonitor panel.";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+                  "test/mochitest/";
+
+add_task(async function task() {
+  await pushPref("devtools.webconsole.filter.net", true);
+
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  const documentUrl = TEST_PATH + TEST_FILE;
+  await loadDocument(documentUrl);
+  info("Document loaded.");
+
+  await resendNetworkRequest(hud, documentUrl);
+});
+
+/**
+ * Resends a network request logged in the webconsole
+ *
+ * @param {Object} hud
+ * @param {String} url
+ *        URL of the request as logged in the netmonitor.
+ */
+async function resendNetworkRequest(hud, url) {
+  const message = await waitFor(() => findMessage(hud, url));
+
+  const menuPopup = await openContextMenu(hud, message);
+  const openResendRequestMenuItem =
+    menuPopup.querySelector("#console-menu-resend-network-request");
+  ok(openResendRequestMenuItem, "resend network request item is enabled");
+
+  // Wait for message containing the resent request url
+  const onNewRequestMessage = waitForMessage(hud, url);
+  openResendRequestMenuItem.click();
+  await onNewRequestMessage;
+
+  ok(true, "The resent request url is correct.");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_nested_proxy.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check evaluating and expanding getters in the console.
+const TEST_URI = "data:text/html;charset=utf8,"
+ + "<h1>Object Inspector on deeply nested proxies</h1>";
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let proxy = new Proxy({}, {});
+    for (let i = 0; i < 1e5; ++i) {
+      proxy = new Proxy(proxy, proxy);
+    }
+    content.wrappedJSObject.console.log("oi-test", proxy);
+  });
+
+  const node = await waitFor(() => findMessage(hud, "oi-test"));
+  const oi = node.querySelector(".tree");
+  const [proxyNode] = getObjectInspectorNodes(oi);
+
+  expandObjectInspectorNode(proxyNode);
+  await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+  checkChildren(proxyNode, [`<target>`, `<handler>`]);
+
+  const targetNode = findObjectInspectorNode(oi, "<target>");
+  expandObjectInspectorNode(targetNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0);
+  checkChildren(targetNode, [`<target>`, `<handler>`]);
+
+  const handlerNode = findObjectInspectorNode(oi, "<handler>");
+  expandObjectInspectorNode(handlerNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0);
+  checkChildren(handlerNode, [`<target>`, `<handler>`]);
+});
+
+function checkChildren(node, expectedChildren) {
+  const children = getObjectInspectorChildrenNodes(node);
+  is(children.length, expectedChildren.length,
+    "There is the expected number of children");
+  children.forEach((child, index) => {
+    ok(child.textContent.includes(expectedChildren[index]),
+      `Expected "${expectedChildren[index]}" child`);
+  });
+}
--- a/devtools/client/webconsole/utils/context-menu.js
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -79,16 +79,27 @@ function createContextMenu(webConsoleUI,
       id: "console-menu-open-in-network-panel",
       label: l10n.getStr("webconsole.menu.openInNetworkPanel.label"),
       accesskey: l10n.getStr("webconsole.menu.openInNetworkPanel.accesskey"),
       visible: source === MESSAGE_SOURCE.NETWORK,
       click: () => serviceContainer.openNetworkPanel(message.messageId),
     }));
   }
 
+  // Resend Network message.
+  if (serviceContainer.resendNetworkRequest && request) {
+    menu.append(new MenuItem({
+      id: "console-menu-resend-network-request",
+      label: l10n.getStr("webconsole.menu.resendNetworkRequest.label"),
+      accesskey: l10n.getStr("webconsole.menu.resendNetworkRequest.accesskey"),
+      visible: source === MESSAGE_SOURCE.NETWORK,
+      click: () => serviceContainer.resendNetworkRequest(message.messageId),
+    }));
+  }
+
   // Open URL in a new tab for a network request.
   menu.append(new MenuItem({
     id: "console-menu-open-url",
     label: l10n.getStr("webconsole.menu.openURL.label"),
     accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
     visible: source === MESSAGE_SOURCE.NETWORK,
     click: () => {
       if (!request) {
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -305,16 +305,21 @@ class WebConsoleWrapper {
                                        null, { "session_id": this.toolbox.sessionId }
             );
           }),
           openNetworkPanel: (requestId) => {
             return this.toolbox.selectTool("netmonitor").then((panel) => {
               return panel.panelWin.Netmonitor.inspectRequest(requestId);
             });
           },
+          resendNetworkRequest: (requestId) => {
+            return this.toolbox.getNetMonitorAPI().then((api) => {
+              return api.resendRequest(requestId);
+            });
+          },
           sourceMapService: this.toolbox ? this.toolbox.sourceMapURLService : null,
           highlightDomElement: async (grip, options = {}) => {
             await this.toolbox.initInspector();
             if (!this.toolbox.highlighter) {
               return null;
             }
             const nodeFront = await this.toolbox.walker.gripToNodeFront(grip);
             return this.toolbox.highlighter.highlight(nodeFront, options);
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -861,16 +861,34 @@ const proto = {
       source,
       line: stack.line,
       column: stack.column,
       functionDisplayName: stack.functionDisplayName,
     };
   },
 
   /**
+   * Handle a protocol request to get the target and handler internal slots of a proxy.
+   */
+  proxySlots: function() {
+    // There could be transparent security wrappers, unwrap to check if it's a proxy.
+    // However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing
+    // the unwrapped target and handler.
+    const unwrapped = DevToolsUtils.unwrap(this.obj);
+    if (!unwrapped || !unwrapped.isProxy) {
+      return this.throwError("objectNotProxy",
+        "'proxySlots' request is only valid for grips with a 'Proxy' class.");
+    }
+    return {
+      proxyTarget: this.hooks.createValueGrip(this.obj.proxyTarget),
+      proxyHandler: this.hooks.createValueGrip(this.obj.proxyHandler),
+    };
+  },
+
+  /**
    * Release the actor, when it isn't needed anymore.
    * Protocol.js uses this release method to call the destroy method.
    */
   release: function() {},
 };
 
 exports.ObjectActor = protocol.ActorClassWithSpec(objectSpec, proto);
 exports.ObjectActorProto = proto;
--- a/devtools/server/actors/object/previewers.js
+++ b/devtools/server/actors/object/previewers.js
@@ -283,37 +283,37 @@ const previewers = {
         break;
       }
     }
 
     return true;
   }],
 
   Proxy: [function({obj, hooks}, grip, rawObj) {
+    // Only preview top-level proxies, avoiding recursion. Otherwise, since both the
+    // target and handler can also be proxies, we could get an exponential behavior.
+    if (hooks.getGripDepth() > 1) {
+      return true;
+    }
+
     // The `isProxy` getter of the debuggee object only detects proxies without
     // security wrappers. If false, the target and handler are not available.
     const hasTargetAndHandler = obj.isProxy;
-    if (hasTargetAndHandler) {
-      grip.proxyTarget = hooks.createValueGrip(obj.proxyTarget);
-      grip.proxyHandler = hooks.createValueGrip(obj.proxyHandler);
-    }
 
     grip.preview = {
       kind: "Object",
       ownProperties: Object.create(null),
       ownPropertiesLength: 2 * hasTargetAndHandler,
     };
 
-    if (hooks.getGripDepth() > 1) {
-      return true;
-    }
-
     if (hasTargetAndHandler) {
-      grip.preview.ownProperties["<target>"] = {value: grip.proxyTarget};
-      grip.preview.ownProperties["<handler>"] = {value: grip.proxyHandler};
+      Object.assign(grip.preview.ownProperties, {
+        "<target>": {value: hooks.createValueGrip(obj.proxyTarget)},
+        "<handler>": {value: hooks.createValueGrip(obj.proxyHandler)},
+      });
     }
 
     return true;
   }],
 };
 
 /**
  * Generic previewer for classes wrapping primitives, like String,
--- a/devtools/server/actors/webconsole/utils.js
+++ b/devtools/server/actors/webconsole/utils.js
@@ -202,23 +202,37 @@ var WebConsoleUtils = {
    * @param array stack
    *        An array of frames, with the topmost first, and each of which has a
    *        'filename' property.
    * @return array
    *         An array of stack frames with any devtools server frames removed.
    *         The original array is not modified.
    */
   removeFramesAboveDebuggerEval(stack) {
-    // Remove any frames for server code above the debugger eval.
-    const evalIndex = stack.findIndex(({ filename }) => {
-      return filename == "debugger eval code";
+    const debuggerEvalFilename = "debugger eval code";
+
+    // Remove any frames for server code above the last debugger eval frame.
+    const evalIndex = stack.findIndex(({ filename }, idx, arr) => {
+      const nextFrame = arr[idx + 1];
+      return filename == debuggerEvalFilename
+        && (!nextFrame || nextFrame.filename !== debuggerEvalFilename);
     });
     if (evalIndex != -1) {
       return stack.slice(0, evalIndex + 1);
     }
+
+    // In some cases (e.g. evaluated expression with SyntaxError), we might not have a
+    // "debugger eval code" frame but still have internal ones. If that's the case, we
+    // return null as the end user shouldn't see those frames.
+    if (stack.some(({ filename }) =>
+      filename && filename.startsWith("resource://devtools/"))
+    ) {
+      return null;
+    }
+
     return stack;
   },
 };
 
 exports.WebConsoleUtils = WebConsoleUtils;
 
 /**
  * WebConsole commands manager.
--- a/devtools/server/tests/unit/test_objectgrips-17.js
+++ b/devtools/server/tests/unit/test_objectgrips-17.js
@@ -52,18 +52,22 @@ function test({ threadClient, debuggee }
   return new Promise(function(resolve) {
     threadClient.addOneTimeListener("paused", async function(event, packet) {
       // Get the grips.
       const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] = packet.frame.arguments;
 
       // Check the grip of the proxy object.
       check_proxy_grip(debuggee, testOptions, proxyGrip);
 
+      // Check the target and handler slots of the proxy object.
+      const proxyClient = threadClient.pauseGrip(proxyGrip);
+      const proxySlots = await proxyClient.getProxySlots();
+      check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots);
+
       // Check the prototype and properties of the proxy object.
-      const proxyClient = threadClient.pauseGrip(proxyGrip);
       const proxyResponse = await proxyClient.getPrototypeAndProperties();
       check_properties(testOptions, proxyResponse.ownProperties, true, false);
       check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false);
 
       // Check the prototype and properties of the object which inherits from the proxy.
       const inheritsProxyClient = threadClient.pauseGrip(inheritsProxyGrip);
       const inheritsProxyResponse = await inheritsProxyClient.getPrototypeAndProperties();
       check_properties(testOptions, inheritsProxyResponse.ownProperties, false, false);
@@ -112,47 +116,58 @@ function test({ threadClient, debuggee }
 
 function check_proxy_grip(debuggee, testOptions, grip) {
   const { global, isOpaque, subsumes, globalIsInvisible } = testOptions;
   const {preview} = grip;
 
   if (global === debuggee) {
     // The proxy has no security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(grip.proxyTarget, "There is a [[ProxyTarget]] grip.");
-    ok(grip.proxyHandler, "There is a [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 2, "The preview has 2 properties.");
-    const target = preview.ownProperties["<target>"].value;
-    strictEqual(target, grip.proxyTarget, "<target> contains the [[ProxyTarget]].");
-    const handler = preview.ownProperties["<handler>"].value;
-    strictEqual(handler, grip.proxyHandler, "<handler> contains the [[ProxyHandler]].");
+    const props = preview.ownProperties;
+    ok(props["<target>"].value, "<target> contains the [[ProxyTarget]].");
+    ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]].");
   } else if (isOpaque) {
     // The proxy has opaque security wrappers.
     strictEqual(grip.class, "Opaque", "The grip has an Opaque class.");
     strictEqual(grip.ownPropertyLength, 0, "The grip has no properties.");
   } else if (!subsumes) {
     // The proxy belongs to compartment not subsumed by the debuggee.
-    strictEqual(grip.class, "Restricted", "The grip has an Restricted class.");
+    strictEqual(grip.class, "Restricted", "The grip has a Restricted class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else if (globalIsInvisible) {
     // The proxy belongs to an invisible-to-debugger compartment.
     strictEqual(grip.class, "InvisibleToDebugger: Object",
                 "The grip has an InvisibleToDebugger class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else {
     // The proxy has non-opaque security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(!("proxyTarget" in grip), "There is no [[ProxyTarget]] grip.");
-    ok(!("proxyHandler" in grip), "There is no [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 0, "The preview has no properties.");
     ok(!("<target>" in preview), "The preview has no <target> property.");
     ok(!("<handler>" in preview), "The preview has no <handler> property.");
   }
 }
 
+function check_proxy_slots(debuggee, testOptions, grip, proxySlots) {
+  const { global } = testOptions;
+
+  if (grip.class !== "Proxy") {
+    strictEqual(proxySlots, undefined, "Slots can only be retrived for Proxy grips.");
+  } else if (global === debuggee) {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "object", "There is a [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "object", "There is a [[ProxyHandler]] grip.");
+  } else {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "undefined", "There is no [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "undefined", "There is no [[ProxyHandler]] grip.");
+  }
+}
+
 function check_properties(testOptions, props, isProxy, createdInDebuggee) {
   const { subsumes, globalIsInvisible } = testOptions;
   const ownPropertiesLength = Reflect.ownKeys(props).length;
 
   if (createdInDebuggee || !isProxy && subsumes && !globalIsInvisible) {
     // The debuggee can access the properties.
     strictEqual(ownPropertiesLength, 1, "1 own property was retrieved.");
     strictEqual(props.x.value, 1, "The property has the right value.");
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-nested-proxy.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
+  await new Promise(function(resolve) {
+    threadClient.addOneTimeListener("paused", async function(event, packet) {
+      const [grip] = packet.frame.arguments;
+      const objClient = threadClient.pauseGrip(grip);
+      const {proxyTarget, proxyHandler} = await objClient.getProxySlots();
+
+      strictEqual(grip.class, "Proxy", "Its a proxy grip.");
+      strictEqual(proxyTarget.class, "Proxy", "The target is also a proxy.");
+      strictEqual(proxyHandler.class, "Proxy", "The handler is also a proxy.");
+
+      await threadClient.resume();
+      resolve();
+    });
+    debuggee.eval(function stopMe(arg) {
+      debugger;
+    }.toString());
+    debuggee.eval(`
+      var proxy = new Proxy({}, {});
+      for (let i = 0; i < 1e5; ++i)
+        proxy = new Proxy(proxy, proxy);
+      stopMe(proxy);
+    `);
+  });
+}));
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -165,16 +165,17 @@ skip-if = true # breakpoint sliding is n
 [test_objectgrips-22.js]
 [test_objectgrips-array-like-object.js]
 [test_objectgrips-property-value-01.js]
 [test_objectgrips-property-value-02.js]
 [test_objectgrips-property-value-03.js]
 [test_objectgrips-fn-apply-01.js]
 [test_objectgrips-fn-apply-02.js]
 [test_objectgrips-fn-apply-03.js]
+[test_objectgrips-nested-proxy.js]
 [test_promise_state-01.js]
 [test_promise_state-02.js]
 [test_promise_state-03.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -354,20 +354,21 @@ DebuggerClient.prototype = {
     request.format = "json";
     request.stack = getStack();
 
     // Implement a Promise like API on the returned object
     // that resolves/rejects on request response
     const deferred = promise.defer();
     function listenerJson(resp) {
       removeRequestListeners();
+      resp = safeOnResponse(resp);
       if (resp.error) {
-        deferred.reject(safeOnResponse(resp));
+        deferred.reject(resp);
       } else {
-        deferred.resolve(safeOnResponse(resp));
+        deferred.resolve(resp);
       }
     }
     function listenerBulk(resp) {
       removeRequestListeners();
       deferred.resolve(safeOnResponse(resp));
     }
 
     const removeRequestListeners = () => {
--- a/devtools/shared/client/object-client.js
+++ b/devtools/shared/client/object-client.js
@@ -298,11 +298,34 @@ ObjectClient.prototype = {
     before: function(packet) {
       if (this._grip.class !== "Promise") {
         throw new Error("getPromiseRejectionStack is only valid for " +
           "promise grips.");
       }
       return packet;
     },
   }),
+
+  /**
+   * Request the target and handler internal slots of a proxy.
+   */
+  getProxySlots: DebuggerClient.requester({
+    type: "proxySlots",
+  }, {
+    before: function(packet) {
+      if (this._grip.class !== "Proxy") {
+        throw new Error("getProxySlots is only valid for proxy grips.");
+      }
+      return packet;
+    },
+    after: function(response) {
+      // Before Firefox 68 (bug 1392760), the proxySlots request didn't exist.
+      // The proxy target and handler were directly included in the grip.
+      if (response.error === "unrecognizedPacketType") {
+        const {proxyTarget, proxyHandler} = this._grip;
+        return {proxyTarget, proxyHandler};
+      }
+      return response;
+    },
+  }),
 };
 
 module.exports = ObjectClient;
--- a/devtools/shared/specs/object.js
+++ b/devtools/shared/specs/object.js
@@ -97,16 +97,21 @@ types.addDictType("object.dependentPromi
 
 types.addDictType("object.originalSourceLocation", {
   source: "source",
   line: "number",
   column: "number",
   functionDisplayName: "string",
 });
 
+types.addDictType("object.proxySlots", {
+  proxyTarget: "object.descriptor",
+  proxyHandler: "object.descriptor",
+});
+
 const objectSpec = generateActorSpec({
   typeName: "obj",
 
   methods: {
     allocationStack: {
       request: {},
       response: {
         allocationStack: RetVal("array:object.originalSourceLocation"),
@@ -193,16 +198,20 @@ const objectSpec = generateActorSpec({
       response: RetVal("object.apply"),
     },
     rejectionStack: {
       request: {},
       response: {
         rejectionStack: RetVal("array:object.originalSourceLocation"),
       },
     },
+    proxySlots: {
+      request: {},
+      response: RetVal("object.proxySlots"),
+    },
     release: { release: true },
     scope: {
       request: {},
       response: RetVal("object.scope"),
     },
     // Needed for the PauseScopedObjectActor which extends the ObjectActor.
     threadGrip: {
       request: {},
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -808,16 +808,31 @@ void BrowsingContext::Transaction::Apply
     aBrowsingContext->WillSet##name(*m##name, aSource); \
     aBrowsingContext->m##name = std::move(*m##name);    \
     aBrowsingContext->DidSet##name(aSource);            \
     m##name.reset();                                    \
   }
 #include "mozilla/dom/BrowsingContextFieldList.h"
 }
 
+BrowsingContext::IPCInitializer BrowsingContext::GetIPCInitializer() {
+  MOZ_ASSERT(
+      !mozilla::Preferences::GetBool("fission.preserve_browsing_contexts", false) ||
+      IsContent());
+
+  IPCInitializer init;
+  init.mId = Id();
+  init.mParentId = mParent ? mParent->Id() : 0;
+  init.mCached = IsCached();
+
+#define MOZ_BC_FIELD(name, type) init.m##name = m##name;
+#include "mozilla/dom/BrowsingContextFieldList.h"
+  return init;
+}
+
 already_AddRefed<BrowsingContext> BrowsingContext::IPCInitializer::GetParent() {
   RefPtr<BrowsingContext> parent;
   if (mParentId != 0) {
     parent = BrowsingContext::Get(mParentId);
     MOZ_RELEASE_ASSERT(parent);
   }
   return parent.forget();
 }
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -342,26 +342,17 @@ class BrowsingContext : public nsWrapper
     bool mCached;
     // Include each field, skipping mOpener, as we want to handle it
     // separately.
 #define MOZ_BC_FIELD(name, type) type m##name;
 #include "mozilla/dom/BrowsingContextFieldList.h"
   };
 
   // Create an IPCInitializer object for this BrowsingContext.
-  IPCInitializer GetIPCInitializer() {
-    IPCInitializer init;
-    init.mId = Id();
-    init.mParentId = mParent ? mParent->Id() : 0;
-    init.mCached = IsCached();
-
-#define MOZ_BC_FIELD(name, type) init.m##name = m##name;
-#include "mozilla/dom/BrowsingContextFieldList.h"
-    return init;
-  }
+  IPCInitializer GetIPCInitializer();
 
   // Create a BrowsingContext object from over IPC.
   static already_AddRefed<BrowsingContext> CreateFromIPC(
       IPCInitializer&& aInitializer, BrowsingContextGroup* aGroup,
       ContentParent* aOriginProcess);
 
  protected:
   virtual ~BrowsingContext();
--- a/docshell/base/BrowsingContextGroup.cpp
+++ b/docshell/base/BrowsingContextGroup.cpp
@@ -6,16 +6,22 @@
 
 #include "mozilla/dom/BrowsingContextGroup.h"
 #include "mozilla/dom/BrowsingContextBinding.h"
 #include "mozilla/dom/BindingUtils.h"
 
 namespace mozilla {
 namespace dom {
 
+BrowsingContextGroup::BrowsingContextGroup() {
+  if (XRE_IsContentProcess()) {
+    ContentChild::GetSingleton()->HoldBrowsingContextGroup(this);
+  }
+}
+
 bool BrowsingContextGroup::Contains(BrowsingContext* aBrowsingContext) {
   return aBrowsingContext->Group() == this;
 }
 
 void BrowsingContextGroup::Register(BrowsingContext* aBrowsingContext) {
   MOZ_DIAGNOSTIC_ASSERT(aBrowsingContext);
   mContexts.PutEntry(aBrowsingContext);
 }
--- a/docshell/base/BrowsingContextGroup.h
+++ b/docshell/base/BrowsingContextGroup.h
@@ -57,17 +57,17 @@ class BrowsingContextGroup final : publi
   void GetToplevels(BrowsingContext::Children& aToplevels) {
     aToplevels.AppendElements(mToplevels);
   }
 
   nsISupports* GetParentObject() const;
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
 
-  BrowsingContextGroup() = default;
+  BrowsingContextGroup();
 
   static already_AddRefed<BrowsingContextGroup> Select(
       BrowsingContext* aParent, BrowsingContext* aOpener) {
     if (aParent) {
       return do_AddRef(aParent->Group());
     }
     if (aOpener) {
       return do_AddRef(aOpener->Group());
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -380,17 +380,18 @@ nsDocShell::nsDocShell(BrowsingContext* 
       mIsPrintingOrPP(false),
       mSavingOldViewer(false),
       mDynamicallyCreated(false),
       mAffectPrivateSessionLifetime(true),
       mInvisible(false),
       mHasLoadedNonBlankURI(false),
       mBlankTiming(false),
       mTitleValidForCurrentURI(false),
-      mIsFrame(false) {
+      mIsFrame(false),
+      mSkipBrowsingContextDetachOnDestroy(false) {
   mHistoryID.m0 = 0;
   mHistoryID.m1 = 0;
   mHistoryID.m2 = 0;
   AssertOriginAttributesMatchPrivateBrowsing();
 
   nsContentUtils::GenerateUUIDInPlace(mHistoryID);
 
   if (gDocShellCount++ == 0) {
@@ -5031,17 +5032,21 @@ nsDocShell::Destroy() {
   if (mSessionHistory) {
     // We want to destroy these content viewers now rather than
     // letting their destruction wait for the session history
     // entries to get garbage collected.  (Bug 488394)
     mSessionHistory->EvictLocalContentViewers();
     mSessionHistory = nullptr;
   }
 
-  mBrowsingContext->Detach();
+  // This will be skipped in cases where we want to preserve the browsing
+  // context between loads.
+  if (!mSkipBrowsingContextDetachOnDestroy) {
+    mBrowsingContext->Detach();
+  }
 
   SetTreeOwner(nullptr);
 
   mBrowserChild = nullptr;
 
   mChromeEventHandler = nullptr;
 
   mOnePermittedSandboxedNavigator = nullptr;
@@ -7207,18 +7212,62 @@ bool nsDocShell::CanSavePresentation(uin
   nsCOMPtr<nsIDocShellTreeItem> root;
   GetSameTypeParent(getter_AddRefs(root));
   if (root && root != this) {
     return false;  // this is a subframe load
   }
 
   // If the document does not want its presentation cached, then don't.
   RefPtr<Document> doc = mScriptGlobal->GetExtantDoc();
-  return doc && doc->CanSavePresentation(aNewRequest);
-}
+
+  uint16_t bfCacheCombo = 0;
+  bool canSavePresentation =
+      doc->CanSavePresentation(aNewRequest, bfCacheCombo);
+  ReportBFCacheComboTelemetry(bfCacheCombo);
+
+  return doc && canSavePresentation;
+}
+
+void nsDocShell::ReportBFCacheComboTelemetry(uint16_t aCombo) {
+  switch (aCombo) {
+    case BFCACHE_SUCCESS:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::BFCache_Success);
+      break;
+    case UNLOAD:
+      Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Unload);
+      break;
+    case UNLOAD_REQUEST:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::Unload_Req);
+      break;
+    case REQUEST:
+      Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Req);
+      break;
+    case UNLOAD_REQUEST_PEER:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer);
+      break;
+    case UNLOAD_REQUEST_PEER_MSE:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer_MSE);
+      break;
+    case UNLOAD_REQUEST_MSE:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_MSE);
+      break;
+    case SUSPENDED_UNLOAD_REQUEST_PEER:
+      Telemetry::AccumulateCategorical(
+          Telemetry::LABELS_BFCACHE_COMBO::SPD_Unload_Req_Peer);
+      break;
+    default:
+      Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Other);
+      break;
+  }
+};
 
 void nsDocShell::ReattachEditorToWindow(nsISHEntry* aSHEntry) {
   MOZ_ASSERT(!mIsBeingDestroyed);
 
   NS_ASSERTION(!mEditorData,
                "Why reattach an editor when we already have one?");
   NS_ASSERTION(aSHEntry && aSHEntry->HasDetachedEditor(),
                "Reattaching when there's not a detached editor.");
@@ -9476,18 +9525,20 @@ nsresult nsDocShell::InternalLoad(nsDocS
           prevViewer->Destroy();
         }
       }
     }
     nsCOMPtr<nsISHEntry> oldEntry = mOSHE;
     bool restoring;
     rv = RestorePresentation(aLoadState->SHEntry(), &restoring);
     if (restoring) {
+      Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, true);
       return rv;
     }
+    Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, false);
 
     // We failed to restore the presentation, so clean up.
     // Both the old and new history entries could potentially be in
     // an inconsistent state.
     if (NS_FAILED(rv)) {
       if (oldEntry) {
         oldEntry->SyncPresentationState();
       }
@@ -12854,20 +12905,18 @@ nsresult nsDocShell::CharsetChangeStopDo
   if (eCharsetReloadRequested != mCharsetReloadState) {
     Stop(nsIWebNavigation::STOP_ALL);
     return NS_OK;
   }
   // return failer if this request is not accepted due to mCharsetReloadState
   return NS_ERROR_DOCSHELL_REQUEST_REJECTED;
 }
 
-NS_IMETHODIMP
-nsDocShell::SetIsPrinting(bool aIsPrinting) {
+void nsDocShell::SetIsPrinting(bool aIsPrinting) {
   mIsPrintingOrPP = aIsPrinting;
-  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::GetPrintPreview(nsIWebBrowserPrint** aPrintPreview) {
   *aPrintPreview = nullptr;
 #if NS_PRINT_PREVIEW
   nsCOMPtr<nsIDocumentViewerPrint> print = do_QueryInterface(mContentViewer);
   if (!print || !print->IsInitializedForPrintPreview()) {
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -400,16 +400,20 @@ class nsDocShell final : public nsDocLoa
    */
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   nsresult InternalLoad(nsDocShellLoadState* aLoadState,
                         nsIDocShell** aDocShell, nsIRequest** aRequest);
 
   // Clear the document's storage access flag if needed.
   void MaybeClearStorageAccessFlag();
 
+  void SkipBrowsingContextDetach() {
+    mSkipBrowsingContextDetachOnDestroy = true;
+  }
+
  private:  // member functions
   friend class nsDSURIContentListener;
   friend class FramingChecker;
   friend class OnLinkClickEvent;
   friend class nsIDocShell;
   friend class mozilla::dom::BrowsingContext;
 
   // It is necessary to allow adding a timeline marker wherever a docshell
@@ -746,16 +750,47 @@ class nsDocShell final : public nsDocLoa
   // |aLoadType| should be the load type that will replace the current
   // presentation. |aNewRequest| should be the request for the document to
   // be loaded in place of the current document, or null if such a request
   // has not been created yet. |aNewDocument| should be the document that will
   // replace the current document.
   bool CanSavePresentation(uint32_t aLoadType, nsIRequest* aNewRequest,
                            mozilla::dom::Document* aNewDocument);
 
+  // There are 11 possible reasons to make a request fails to use BFCache
+  // (see BFCacheStatus in dom/base/Document.h), and we'd like to record
+  // the common combinations for reasons which make requests fail to use
+  // BFCache. These combinations are generated based on some local browsings,
+  // we need to adjust them when necessary.
+  enum BFCacheStatusCombo : uint16_t {
+    BFCACHE_SUCCESS,
+    UNLOAD = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER,
+    UNLOAD_REQUEST = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+                              mozilla::dom::BFCacheStatus::REQUEST,
+    REQUEST = mozilla::dom::BFCacheStatus::REQUEST,
+    UNLOAD_REQUEST_PEER = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+                          mozilla::dom::BFCacheStatus::REQUEST |
+                          mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION,
+    UNLOAD_REQUEST_PEER_MSE =
+      mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+      mozilla::dom::BFCacheStatus::REQUEST |
+      mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION |
+      mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT,
+    UNLOAD_REQUEST_MSE = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+                         mozilla::dom::BFCacheStatus::REQUEST |
+                         mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT,
+    SUSPENDED_UNLOAD_REQUEST_PEER =
+      mozilla::dom::BFCacheStatus::SUSPENDED |
+      mozilla::dom::BFCacheStatus::UNLOAD_LISTENER |
+      mozilla::dom::BFCacheStatus::REQUEST |
+      mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION,
+  };
+
+  void ReportBFCacheComboTelemetry(uint16_t aCombo);
+
   // Captures the state of the supporting elements of the presentation
   // (the "window" object, docshell tree, meta-refresh loads, and security
   // state) and stores them on |mOSHE|.
   nsresult CaptureState();
 
   // Begin the toplevel restore process for |aSHEntry|.
   // This simulates a channel open, and defers the real work until
   // RestoreFromHistory is called from a PLEvent.
@@ -1200,11 +1235,16 @@ class nsDocShell final : public nsDocLoa
   // We will check the innerWin's timing before creating a new one
   // in MaybeInitTiming()
   bool mBlankTiming : 1;
 
   // This flag indicates when the title is valid for the current URI.
   bool mTitleValidForCurrentURI : 1;
 
   bool mIsFrame : 1;
+
+  // If mSkipBrowsingContextDetachOnDestroy is set to true, then when the
+  // docshell is destroyed, the browsing context will not be detached. This is
+  // for cases where we want to preserve the BC for future use.
+  bool mSkipBrowsingContextDetachOnDestroy : 1;
 };
 
 #endif /* nsDocShell_h__ */
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -541,17 +541,17 @@ interface nsIDocShell : nsIDocShellTreeI
    * and should be treated accordingly.
    **/
   attribute boolean isOffScreenBrowser;
 
   /**
    * Allows nsDocumentViewer to tell the top-level same-type docshell that
    * one of the documents under it is printing.
    */
-  void setIsPrinting(in boolean aIsPrinting);
+  [noscript, notxpcom] void setIsPrinting(in boolean aIsPrinting);
 
   /**
    * If the current content viewer isn't initialized for print preview,
    * it is replaced with one which is and to which an about:blank document
    * is loaded.
    */
   readonly attribute nsIWebBrowserPrint printPreview;
 
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -6883,17 +6883,19 @@ nsViewportInfo Document::GetViewportInfo
     CSSSize viewportSize(viewportWidth, viewportWidth * aspectRatio);
     ScreenIntSize fakeDesktopSize = RoundedToInt(viewportSize * scaleToFit);
     return nsViewportInfo(fakeDesktopSize, scaleToFit,
                           nsViewportInfo::ZoomFlag::AllowZoom);
   }
 
   if (!nsLayoutUtils::ShouldHandleMetaViewport(this)) {
     return nsViewportInfo(aDisplaySize, defaultScale,
-                          nsViewportInfo::ZoomFlag::DisallowZoom);
+                          nsLayoutUtils::AllowZoomingForDocument(this)
+                              ? nsViewportInfo::ZoomFlag::AllowZoom
+                              : nsViewportInfo::ZoomFlag::DisallowZoom);
   }
 
   // In cases where the width of the CSS viewport is less than or equal to the
   // width of the display (i.e. width <= device-width) then we disable
   // double-tap-to-zoom behaviour. See bug 941995 for details.
 
   switch (mViewportType) {
     case DisplayWidthHeight:
@@ -7627,54 +7629,61 @@ void Document::CollectDescendantDocument
       if (aCallback(subdoc)) {
         aDescendants.AppendElement(entry->mSubDocument);
       }
       subdoc->CollectDescendantDocuments(aDescendants, aCallback);
     }
   }
 }
 
-bool Document::CanSavePresentation(nsIRequest* aNewRequest) {
+bool Document::CanSavePresentation(nsIRequest* aNewRequest,
+                                   uint16_t& aBFCacheCombo) {
+  bool ret = true;
+
   if (!IsBFCachingAllowed()) {
-    return false;
+    aBFCacheCombo |= BFCacheStatus::NOT_ALLOWED;
+    ret = false;
   }
 
   nsAutoCString uri;
   if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Verbose))) {
     if (mDocumentURI) {
       mDocumentURI->GetSpec(uri);
     }
   }
 
   if (EventHandlingSuppressed()) {
     MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
             ("Save of %s blocked on event handling suppression", uri.get()));
-    return false;
+    aBFCacheCombo |= BFCacheStatus::EVENT_HANDLING_SUPPRESSED;
+    ret = false;
   }
 
   // Do not allow suspended windows to be placed in the
   // bfcache.  This method is also used to verify a document
   // coming out of the bfcache is ok to restore, though.  So
   // we only want to block suspend windows that aren't also
   // frozen.
   nsPIDOMWindowInner* win = GetInnerWindow();
   if (win && win->IsSuspended() && !win->IsFrozen()) {
     MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
             ("Save of %s blocked on suspended Window", uri.get()));
-    return false;
+    aBFCacheCombo |= BFCacheStatus::SUSPENDED;
+    ret = false;
   }
 
   // Check our event listener manager for unload/beforeunload listeners.
   nsCOMPtr<EventTarget> piTarget = do_QueryInterface(mScriptGlobalObject);
   if (piTarget) {
     EventListenerManager* manager = piTarget->GetExistingListenerManager();
     if (manager && manager->HasUnloadListeners()) {
       MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
               ("Save of %s blocked due to unload handlers", uri.get()));
-      return false;
+      aBFCacheCombo |= BFCacheStatus::UNLOAD_LISTENER;
+      ret = false;
     }
   }
 
   // Check if we have pending network requests
   nsCOMPtr<nsILoadGroup> loadGroup = GetDocumentLoadGroup();
   if (loadGroup) {
     nsCOMPtr<nsISimpleEnumerator> requests;
     loadGroup->GetRequests(getter_AddRefs(requests));
@@ -7708,85 +7717,95 @@ bool Document::CanSavePresentation(nsIRe
 
         if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Verbose))) {
           nsAutoCString requestName;
           request->GetName(requestName);
           MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
                   ("Save of %s blocked because document has request %s",
                    uri.get(), requestName.get()));
         }
-
-        return false;
+        aBFCacheCombo |= BFCacheStatus::REQUEST;
+        ret = false;
       }
     }
   }
 
   // Check if we have active GetUserMedia use
   if (MediaManager::Exists() && win &&
       MediaManager::Get()->IsWindowStillActive(win->WindowID())) {
     MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
             ("Save of %s blocked due to GetUserMedia", uri.get()));
-    return false;
+    aBFCacheCombo |= BFCacheStatus::ACTIVE_GET_USER_MEDIA;
+    ret = false;
   }
 
 #ifdef MOZ_WEBRTC
   // Check if we have active PeerConnections
   if (win && win->HasActivePeerConnections()) {
     MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
             ("Save of %s blocked due to PeerConnection", uri.get()));
-    return false;
+    aBFCacheCombo |= BFCacheStatus::ACTIVE_PEER_CONNECTION;
+    ret = false;
   }
 #endif  // MOZ_WEBRTC
 
   // Don't save presentations for documents containing EME content, so that
   // CDMs reliably shutdown upon user navigation.
   if (ContainsEMEContent()) {
-    return false;
+    aBFCacheCombo |= BFCacheStatus::CONTAINS_EME_CONTENT;
+    ret = false;
   }
 
   // Don't save presentations for documents containing MSE content, to
   // reduce memory usage.
   if (ContainsMSEContent()) {
     MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
             ("Save of %s blocked due to MSE use", uri.get()));
-    return false;
+    aBFCacheCombo |= BFCacheStatus::CONTAINS_MSE_CONTENT;
+    ret = false;
   }
 
   if (mSubDocuments) {
     for (auto iter = mSubDocuments->Iter(); !iter.Done(); iter.Next()) {
       auto entry = static_cast<SubDocMapEntry*>(iter.Get());
       Document* subdoc = entry->mSubDocument;
 
+      uint16_t subDocBFCacheCombo = 0;
       // The aIgnoreRequest we were passed is only for us, so don't pass it on.
-      bool canCache = subdoc ? subdoc->CanSavePresentation(nullptr) : false;
+      bool canCache =
+          subdoc ? subdoc->CanSavePresentation(nullptr, subDocBFCacheCombo)
+                 : false;
       if (!canCache) {
         MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
                 ("Save of %s blocked due to subdocument blocked", uri.get()));
-        return false;
+        aBFCacheCombo |= subDocBFCacheCombo;
+        ret = false;
       }
     }
   }
 
   if (win) {
     auto* globalWindow = nsGlobalWindowInner::Cast(win);
 #ifdef MOZ_WEBSPEECH
     if (globalWindow->HasActiveSpeechSynthesis()) {
       MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
               ("Save of %s blocked due to Speech use", uri.get()));
-      return false;
+      aBFCacheCombo |= BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS;
+      ret = false;
     }
 #endif
     if (globalWindow->HasUsedVR()) {
       MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose,
               ("Save of %s blocked due to having used VR", uri.get()));
-      return false;
-    }
-  }
-
-  return true;
+      aBFCacheCombo |= BFCacheStatus::HAS_USED_VR;
+      ret = false;
+    }
+  }
+
+  return ret;
 }
 
 void Document::Destroy() {
   // The ContentViewer wants to release the document now.  So, tell our content
   // to drop any references to the document so that it can be destroyed.
   if (mIsGoingAway) return;
 
   // Make sure to report before IPC closed.
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -216,16 +216,30 @@ class Sequence;
 class nsDocumentOnStack;
 class nsUnblockOnloadEvent;
 
 template <typename, typename>
 class CallbackObjectHolder;
 
 enum class CallerType : uint32_t;
 
+enum BFCacheStatus {
+  NOT_ALLOWED = 1 << 0,                  // Status 0
+  EVENT_HANDLING_SUPPRESSED = 1 << 1,    // Status 1
+  SUSPENDED = 1 << 2,                    // Status 2
+  UNLOAD_LISTENER = 1 << 3,              // Status 3
+  REQUEST = 1 << 4,                      // Status 4
+  ACTIVE_GET_USER_MEDIA = 1 << 5,        // Status 5
+  ACTIVE_PEER_CONNECTION = 1 << 6,       // Status 6
+  CONTAINS_EME_CONTENT = 1 << 7,         // Status 7
+  CONTAINS_MSE_CONTENT = 1 << 8,         // Status 8
+  HAS_ACTIVE_SPEECH_SYNTHESIS = 1 << 9,  // Status 9
+  HAS_USED_VR = 1 << 10,                 // Status 10
+};
+
 }  // namespace dom
 }  // namespace mozilla
 
 namespace mozilla {
 namespace net {
 class ChannelEventQueue;
 }  // namespace net
 }  // namespace mozilla
@@ -2247,18 +2261,22 @@ class Document : public nsINode,
    *  - If there are any beforeunload or unload listeners, we must fire them
    *    for correctness, but this likely puts the document into a state where
    *    it would not function correctly if restored.
    *
    * |aNewRequest| should be the request for a new document which will
    * replace this document in the docshell.  The new document's request
    * will be ignored when checking for active requests.  If there is no
    * request associated with the new document, this parameter may be null.
-   */
-  virtual bool CanSavePresentation(nsIRequest* aNewRequest);
+   *
+   * |aBFCacheCombo| is used as a bitmask to indicate what the status
+   * combination is when we try to BFCache aNewRequest
+   */
+  virtual bool CanSavePresentation(nsIRequest* aNewRequest,
+                                   uint16_t& aBFCacheCombo);
 
   virtual nsresult Init();
 
   /**
    * Notify the document that its associated ContentViewer is being destroyed.
    * This releases circular references so that the document can go away.
    * Destroy() is only called on documents that have a content viewer.
    */
--- a/dom/base/RemoteOuterWindowProxy.cpp
+++ b/dom/base/RemoteOuterWindowProxy.cpp
@@ -14,22 +14,17 @@
 namespace mozilla {
 namespace dom {
 
 /**
  * RemoteOuterWindowProxy is the proxy handler for the WindowProxy objects for
  * Window objects that live in a different process.
  *
  * RemoteOuterWindowProxy holds a BrowsingContext, which is cycle collected.
- * However, RemoteOuterWindowProxy only holds BrowsingContexts that don't have a
- * reference to a docshell, so there's no need to declare the edge from
- * RemoteOuterWindowProxy to its BrowsingContext to the cycle collector.
- *
- * FIXME Verify that this is correct:
- *       https://bugzilla.mozilla.org/show_bug.cgi?id=1516350.
+ * This reference is declared to the cycle collector via NoteChildren().
  */
 
 class RemoteOuterWindowProxy
     : public RemoteObjectProxy<BrowsingContext,
                                Window_Binding::sCrossOriginAttributes,
                                Window_Binding::sCrossOriginMethods> {
  public:
   typedef RemoteObjectProxy Base;
--- a/dom/base/nsDocumentEncoder.cpp
+++ b/dom/base/nsDocumentEncoder.cpp
@@ -333,18 +333,25 @@ class FixupNodeDeterminer {
   nsINode& mOriginalNode;
 };
 
 nsresult nsDocumentEncoder::SerializeNodeStart(nsINode& aOriginalNode,
                                                int32_t aStartOffset,
                                                int32_t aEndOffset,
                                                nsAString& aStr,
                                                nsINode* aFixupNode) {
-  if (mNeedsPreformatScanning && aOriginalNode.IsElement()) {
-    mSerializer->ScanElementForPreformat(aOriginalNode.AsElement());
+  if (mNeedsPreformatScanning) {
+    if (aOriginalNode.IsElement()) {
+      mSerializer->ScanElementForPreformat(aOriginalNode.AsElement());
+    } else if (aOriginalNode.IsText()) {
+      const nsCOMPtr<nsINode> parent = aOriginalNode.GetParent();
+      if (parent && parent->IsElement()) {
+        mSerializer->ScanElementForPreformat(parent->AsElement());
+      }
+    }
   }
 
   if (!IsVisibleNode(&aOriginalNode)) {
     return NS_OK;
   }
 
   FixupNodeDeterminer fixupNodeDeterminer{mNodeFixup, aFixupNode,
                                           aOriginalNode};
@@ -388,18 +395,25 @@ nsresult nsDocumentEncoder::SerializeNod
       break;
     }
   }
 
   return NS_OK;
 }
 
 nsresult nsDocumentEncoder::SerializeNodeEnd(nsINode& aNode, nsAString& aStr) {
-  if (mNeedsPreformatScanning && aNode.IsElement()) {
-    mSerializer->ForgetElementForPreformat(aNode.AsElement());
+  if (mNeedsPreformatScanning) {
+    if (aNode.IsElement()) {
+      mSerializer->ForgetElementForPreformat(aNode.AsElement());
+    } else if (aNode.IsText()) {
+      const nsCOMPtr<nsINode> parent = aNode.GetParent();
+      if (parent && parent->IsElement()) {
+        mSerializer->ForgetElementForPreformat(parent->AsElement());
+      }
+    }
   }
 
   if (!IsVisibleNode(&aNode)) {
     return NS_OK;
   }
 
   if (aNode.IsElement()) {
     mSerializer->AppendElementEnd(aNode.AsElement(), aStr);
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -359,34 +359,39 @@ nsFrameLoader* nsFrameLoader::Create(Ele
 
   RefPtr<BrowsingContext> context = CreateBrowsingContext(aOwner, aOpener);
   NS_ENSURE_TRUE(context, nullptr);
   return new nsFrameLoader(aOwner, context, aNetworkCreated);
 }
 
 /* static */
 nsFrameLoader* nsFrameLoader::Create(
-    mozilla::dom::Element* aOwner,
+    mozilla::dom::Element* aOwner, BrowsingContext* aPreservedBrowsingContext,
     const mozilla::dom::RemotenessOptions& aOptions) {
   NS_ENSURE_TRUE(aOwner, nullptr);
   // This version of Create is only called for Remoteness updates, so we can
   // assume we need a FrameLoader here and skip the check in the other Create.
 
   bool hasOpener =
       aOptions.mOpener.WasPassed() && !aOptions.mOpener.Value().IsNull();
   MOZ_ASSERT(!aOptions.mRemoteType.WasPassed() ||
                  aOptions.mRemoteType.Value().IsVoid() || !hasOpener,
              "Cannot pass aOpener for a remote frame!");
 
   // This seems slightly unwieldy.
   RefPtr<BrowsingContext> opener;
   if (hasOpener) {
     opener = aOptions.mOpener.Value().Value().get();
   }
-  RefPtr<BrowsingContext> context = CreateBrowsingContext(aOwner, opener);
+  RefPtr<BrowsingContext> context;
+  if (aPreservedBrowsingContext) {
+    context = aPreservedBrowsingContext;
+  } else {
+    context = CreateBrowsingContext(aOwner, opener);
+  }
   NS_ENSURE_TRUE(context, nullptr);
   return new nsFrameLoader(aOwner, context, aOptions);
 }
 
 void nsFrameLoader::LoadFrame(bool aOriginalSrc) {
   if (NS_WARN_IF(!mOwnerContent)) {
     return;
   }
@@ -3485,8 +3490,27 @@ ProcessMessageManager* nsFrameLoader::Ge
 };
 
 JSObject* nsFrameLoader::WrapObject(JSContext* cx,
                                     JS::Handle<JSObject*> aGivenProto) {
   JS::RootedObject result(cx);
   FrameLoader_Binding::Wrap(cx, this, this, aGivenProto, &result);
   return result;
 }
+
+void nsFrameLoader::SkipBrowsingContextDetach() {
+  if (IsRemoteFrame()) {
+    // OOP Browser - Go directly over Browser Parent
+    if (mBrowserParent) {
+      Unused << mBrowserParent->SendSkipBrowsingContextDetach();
+    }
+    // OOP IFrame - Through Browser Bridge Parent, set on browser child
+    else if (mBrowserBridgeChild) {
+      Unused << mBrowserBridgeChild->SendSkipBrowsingContextDetach();
+    }
+    return;
+  }
+
+  // In process
+  RefPtr<nsDocShell> docshell = GetDocShell();
+  MOZ_ASSERT(docshell);
+  docshell->SkipBrowsingContextDetach();
+}
--- a/dom/base/nsFrameLoader.h
+++ b/dom/base/nsFrameLoader.h
@@ -90,27 +90,29 @@ typedef struct _GtkWidget GtkWidget;
 class nsFrameLoader final : public nsStubMutationObserver,
                             public mozilla::dom::ipc::MessageManagerCallback,
                             public nsWrapperCache {
   friend class AutoResetInShow;
   friend class AutoResetInFrameSwap;
   typedef mozilla::dom::PBrowserParent PBrowserParent;
   typedef mozilla::dom::Document Document;
   typedef mozilla::dom::BrowserParent BrowserParent;
+  typedef mozilla::dom::BrowsingContext BrowsingContext;
   typedef mozilla::layout::RenderFrame RenderFrame;
 
  public:
   // Called by Frame Elements to create a new FrameLoader.
   static nsFrameLoader* Create(mozilla::dom::Element* aOwner,
                                mozilla::dom::BrowsingContext* aOpener,
                                bool aNetworkCreated);
 
   // Called by nsFrameLoaderOwner::ChangeRemoteness when switching out
   // FrameLoaders.
   static nsFrameLoader* Create(mozilla::dom::Element* aOwner,
+                               BrowsingContext* aPreservedBrowsingContext,
                                const mozilla::dom::RemotenessOptions& aOptions);
 
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_FRAMELOADER_IID)
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(nsFrameLoader)
 
   NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
@@ -380,16 +382,18 @@ class nsFrameLoader final : public nsStu
   // public because a callback needs these.
   RefPtr<mozilla::dom::ChromeMessageSender> mMessageManager;
   RefPtr<mozilla::dom::InProcessBrowserChildMessageManager>
       mChildMessageManager;
 
   virtual JSObject* WrapObject(JSContext* cx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
+  void SkipBrowsingContextDetach();
+
  private:
   nsFrameLoader(mozilla::dom::Element* aOwner,
                 mozilla::dom::BrowsingContext* aBrowsingContext,
                 bool aNetworkCreated);
   nsFrameLoader(mozilla::dom::Element* aOwner,
                 mozilla::dom::BrowsingContext* aBrowsingContext,
                 const mozilla::dom::RemotenessOptions& aOptions);
   ~nsFrameLoader();
--- a/dom/base/nsFrameLoaderOwner.cpp
+++ b/dom/base/nsFrameLoaderOwner.cpp
@@ -27,28 +27,47 @@ nsFrameLoaderOwner::GetBrowsingContext()
   if (mFrameLoader) {
     return mFrameLoader->GetBrowsingContext();
   }
   return nullptr;
 }
 
 void nsFrameLoaderOwner::ChangeRemoteness(
     const mozilla::dom::RemotenessOptions& aOptions, mozilla::ErrorResult& rv) {
-  // If we already have a Frameloader, destroy it.
+  RefPtr<mozilla::dom::BrowsingContext> bc;
+
+  // If we already have a Frameloader, destroy it, possibly preserving its
+  // browsing context.
   if (mFrameLoader) {
+    // Don't preserve contexts if this is a chrome (parent process) window that
+    // is changing from remote to local.
+    bool isChromeRemoteToLocal =
+        XRE_IsParentProcess() && (!aOptions.mRemoteType.WasPassed() ||
+                                  aOptions.mRemoteType.Value().IsVoid());
+
+    // If this is a process switch due to a difference in Cross Origin Opener
+    // Policy, do not preserve the browsing context. Otherwise, save off the
+    // browsing context and use it when creating our new FrameLoader.
+    if (!aOptions.mReplaceBrowsingContext && !isChromeRemoteToLocal &&
+        mozilla::Preferences::GetBool("fission.preserve_browsing_contexts", false)) {
+      bc = mFrameLoader->GetBrowsingContext();
+      mFrameLoader->SkipBrowsingContextDetach();
+    }
+
     mFrameLoader->Destroy();
     mFrameLoader = nullptr;
   }
 
   // In this case, we're not reparenting a frameloader, we're just destroying
   // our current one and creating a new one, so we can use ourselves as the
   // owner.
   RefPtr<Element> owner = do_QueryObject(this);
   MOZ_ASSERT(owner);
-  mFrameLoader = nsFrameLoader::Create(owner, aOptions);
+  mFrameLoader = nsFrameLoader::Create(owner, bc, aOptions);
+
   if (NS_WARN_IF(!mFrameLoader)) {
     return;
   }
 
   if (aOptions.mPendingSwitchID.WasPassed()) {
     mFrameLoader->ResumeLoad(aOptions.mPendingSwitchID.Value());
   } else {
     mFrameLoader->LoadFrame(false);
@@ -65,14 +84,13 @@ void nsFrameLoaderOwner::ChangeRemotenes
       fm->ActivateRemoteFrameIfNeeded(*owner);
     }
   }
 
   // Assuming this element is a XULFrameElement, once we've reset our
   // FrameLoader, fire an event to act like we've recreated ourselves, similar
   // to what XULFrameElement does after rebinding to the tree.
   // ChromeOnlyDispatch is turns on to make sure this isn't fired into content.
-  (new mozilla::AsyncEventDispatcher(owner,
-                                     NS_LITERAL_STRING("XULFrameLoaderCreated"),
-                                     mozilla::CanBubble::eYes,
-                                     mozilla::ChromeOnlyDispatch::eYes))
+  (new mozilla::AsyncEventDispatcher(
+       owner, NS_LITERAL_STRING("XULFrameLoaderCreated"),
+       mozilla::CanBubble::eYes, mozilla::ChromeOnlyDispatch::eYes))
       ->RunDOMEventWhenSafe();
 }
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -384,18 +384,18 @@ class nsGlobalWindowObserver final : pub
                                   const char16_t* aStorageType,
                                   bool aPrivateBrowsing) override {
     if (mWindow) {
       mWindow->ObserveStorageNotification(aEvent, aStorageType,
                                           aPrivateBrowsing);
     }
   }
 
-  nsIPrincipal* GetPrincipal() const override {
-    return mWindow ? mWindow->GetPrincipal() : nullptr;
+  nsIPrincipal* GetEffectiveStoragePrincipal() const override {
+    return mWindow ? mWindow->GetEffectiveStoragePrincipal() : nullptr;
   }
 
   bool IsPrivateBrowsing() const override {
     return mWindow ? mWindow->IsPrivateBrowsing() : false;
   }
 
   nsIEventTarget* GetEventTarget() const override {
     return mWindow ? mWindow->EventTargetFor(TaskCategory::Other) : nullptr;
@@ -4349,18 +4349,19 @@ Storage* nsGlobalWindowInner::GetSession
     nsCOMPtr<nsIDOMStorageManager> storageManager =
         do_QueryInterface(docShell, &rv);
     if (NS_FAILED(rv)) {
       aError.Throw(rv);
       return nullptr;
     }
 
     RefPtr<Storage> storage;
-    aError = storageManager->CreateStorage(this, principal, documentURI,
-                                           IsPrivateBrowsing(),
+    // No StoragePrincipal for sessions.
+    aError = storageManager->CreateStorage(this, principal, principal,
+                                           documentURI, IsPrivateBrowsing(),
                                            getter_AddRefs(storage));
     if (aError.Failed()) {
       return nullptr;
     }
 
     mSessionStorage = storage;
     MOZ_ASSERT(mSessionStorage);
 
@@ -4401,17 +4402,17 @@ Storage* nsGlobalWindowInner::GetLocalSt
 
   nsContentUtils::StorageAccess access =
       nsContentUtils::StorageAllowedForWindow(this);
 
   // We allow partitioned localStorage only to some hosts.
   if (access == nsContentUtils::StorageAccess::ePartitionedOrDeny) {
     if (!mDoc) {
       access = nsContentUtils::StorageAccess::eDeny;
-    } else {
+    } else if (!StaticPrefs::privacy_storagePrincipal_enabledForTrackers()) {
       nsCOMPtr<nsIURI> uri;
       Unused << mDoc->NodePrincipal()->GetURI(getter_AddRefs(uri));
       static const char* kPrefName =
           "privacy.restrict3rdpartystorage.partitionedHosts";
       if (!uri || !nsContentUtils::IsURIInPrefList(uri, kPrefName)) {
         access = nsContentUtils::StorageAccess::eDeny;
       }
     }
@@ -4427,17 +4428,18 @@ Storage* nsGlobalWindowInner::GetLocalSt
     }
     return nullptr;
   }
 
   // Note that this behavior is observable: if we grant storage permission to a
   // tracker, we pass from the partitioned LocalStorage to the 'normal'
   // LocalStorage. The previous data is lost and the 2 window.localStorage
   // objects, before and after the permission granted, will be different.
-  if (access != nsContentUtils::StorageAccess::ePartitionedOrDeny &&
+  if ((StaticPrefs::privacy_storagePrincipal_enabledForTrackers() ||
+       access != nsContentUtils::StorageAccess::ePartitionedOrDeny) &&
       (!mLocalStorage ||
        mLocalStorage->Type() == Storage::ePartitionedLocalStorage)) {
     RefPtr<Storage> storage;
 
     if (NextGenLocalStorageEnabled()) {
       aError = LSObject::CreateForWindow(this, getter_AddRefs(storage));
     } else {
       nsresult rv;
@@ -4457,18 +4459,24 @@ Storage* nsGlobalWindowInner::GetLocalSt
       }
 
       nsIPrincipal* principal = GetPrincipal();
       if (!principal) {
         aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
         return nullptr;
       }
 
-      aError = storageManager->CreateStorage(this, principal, documentURI,
-                                             IsPrivateBrowsing(),
+      nsIPrincipal* storagePrincipal = GetEffectiveStoragePrincipal();
+      if (!storagePrincipal) {
+        aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+        return nullptr;
+      }
+
+      aError = storageManager->CreateStorage(this, principal, storagePrincipal,
+                                             documentURI, IsPrivateBrowsing(),
                                              getter_AddRefs(storage));
     }
 
     if (aError.Failed()) {
       return nullptr;
     }
 
     mLocalStorage = storage;
@@ -4478,21 +4486,30 @@ Storage* nsGlobalWindowInner::GetLocalSt
   if (access == nsContentUtils::StorageAccess::ePartitionedOrDeny &&
       !mLocalStorage) {
     nsIPrincipal* principal = GetPrincipal();
     if (!principal) {
       aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
       return nullptr;
     }
 
-    mLocalStorage = new PartitionedLocalStorage(this, principal);
-  }
-
-  MOZ_ASSERT((access == nsContentUtils::StorageAccess::ePartitionedOrDeny) ==
-             (mLocalStorage->Type() == Storage::ePartitionedLocalStorage));
+    nsIPrincipal* storagePrincipal = GetEffectiveStoragePrincipal();
+    if (!storagePrincipal) {
+      aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+      return nullptr;
+    }
+
+    mLocalStorage =
+        new PartitionedLocalStorage(this, principal, storagePrincipal);
+  }
+
+  MOZ_ASSERT_IF(
+      !StaticPrefs::privacy_storagePrincipal_enabledForTrackers(),
+      (access == nsContentUtils::StorageAccess::ePartitionedOrDeny) ==
+          (mLocalStorage->Type() == Storage::ePartitionedLocalStorage));
 
   return mLocalStorage;
 }
 
 IDBFactory* nsGlobalWindowInner::GetIndexedDB(ErrorResult& aError) {
   if (!mIndexedDB) {
     // This may keep mIndexedDB null without setting an error.
     aError = IDBFactory::CreateForWindow(this, getter_AddRefs(mIndexedDB));
@@ -4869,16 +4886,21 @@ void nsGlobalWindowInner::ObserveStorage
     return;
   }
 
   nsIPrincipal* principal = GetPrincipal();
   if (!principal) {
     return;
   }
 
+  nsIPrincipal* storagePrincipal = GetEffectiveStoragePrincipal();
+  if (!storagePrincipal) {
+    return;
+  }
+
   bool fireMozStorageChanged = false;
   nsAutoString eventType;
   eventType.AssignLiteral("storage");
 
   if (!NS_strcmp(aStorageType, u"sessionStorage")) {
     RefPtr<Storage> changingStorage = aEvent->GetStorageArea();
     MOZ_ASSERT(changingStorage);
 
@@ -4909,18 +4931,18 @@ void nsGlobalWindowInner::ObserveStorage
     if (fireMozStorageChanged) {
       eventType.AssignLiteral("MozSessionStorageChanged");
     }
   }
 
   else {
     MOZ_ASSERT(!NS_strcmp(aStorageType, u"localStorage"));
 
-    MOZ_DIAGNOSTIC_ASSERT(
-        StorageUtils::PrincipalsEqual(aEvent->GetPrincipal(), principal));
+    MOZ_DIAGNOSTIC_ASSERT(StorageUtils::PrincipalsEqual(aEvent->GetPrincipal(),
+                                                        storagePrincipal));
 
     fireMozStorageChanged =
         mLocalStorage && mLocalStorage == aEvent->GetStorageArea();
 
     if (fireMozStorageChanged) {
       eventType.AssignLiteral("MozLocalStorageChanged");
     }
   }
--- a/dom/base/test/test_bug116083.html
+++ b/dom/base/test/test_bug116083.html
@@ -11,16 +11,20 @@ https://bugzilla.mozilla.org/show_bug.cg
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=116083">Mozilla Bug 116083</a>
 <div id="content">
 <div style="white-space: pre">foo  bar</div>
 <div style="white-space: pre-wrap">foo  bar</div>
 <div style="white-space: pre-line">foo  bar</div>
 <div style="white-space: -moz-pre-space">foo  bar</div>
+<div style="white-space: pre" data-collapse-selection-to-child-and-extend>foo  bar</div>
+<div style="white-space: pre-wrap" data-collapse-selection-to-child-and-extend>foo  bar</div>
+<div style="white-space: pre-line" data-collapse-selection-to-child-and-extend>foo  bar</div>
+<div style="white-space: -moz-pre-space" data-collapse-selection-to-child-and-extend>foo  bar</div>
 <div data-result="bar  baz"><span style="white-space: pre">bar  </span>baz</div>
 <div data-result="bar  baz"><span style="white-space: pre-wrap">bar  </span>baz</div>
 <div data-result="bar  baz"><span style="white-space: pre-line">bar  </span>baz</div>
 <div data-result="bar  baz"><span style="white-space: -moz-pre-space">bar  </span>baz</div>
 <div data-result="foo  &#10;  bar&#10;&#10;!&#10;&#10;&#10;baz" style="white-space: pre"><div>foo  </div><div>  bar</div><div><br></div><div>!</div><div><br><br></div><div>baz</div></div>
 <div data-result="foo &#10; bar&#10;&#10;!&#10;&#10;&#10;baz" style="white-space: pre" contenteditable><div>foo </div><div> bar</div><div><br></div><div>!</div><div><br><br></div><div>baz</div></div>
 <div data-result="foo  &#10;  bar&#10;&#10;!&#10;&#10;&#10;baz" style="white-space: pre-wrap"><div>foo  </div><div>  bar</div><div><br></div><div>!</div><div><br><br></div><div>baz</div></div>
 <div data-result="foo &#10; bar&#10;&#10;!&#10;&#10;&#10;baz" style="white-space: pre-wrap" contenteditable><div>foo </div><div> bar</div><div><br></div><div>!</div><div><br><br></div><div>baz</div></div>
@@ -65,26 +69,45 @@ function hasExpectedFlavors() {
   }
 
   if (navigator.appVersion.includes("Win")) {
     ok(cb.hasDataMatchingFlavors(["application/x-moz-nativehtml"], 1, cb.kGlobalClipboard),
        "The clipboard has application/x-moz-nativehtml");
   }
 }
 
+function collapseSelectionToChildAndExtend(divElement) {
+  is(divElement.childNodes.length, 1, "Expected exactly one child node.");
+  var textChildNode = divElement.childNodes[0];
+  getSelection().collapse(textChildNode);
+  getSelection().extend(textChildNode, divElement.textContent.length);
+}
+
+function selectDependingOnAttributes(divElement) {
+  if (divElement.hasAttribute("data-collapse-selection-to-child-and-extend")) {
+    // Selecting text as follow comes closest to user behaviour.
+    collapseSelectionToChildAndExtend(divElement);
+  } else {
+    getSelection().selectAllChildren(divElement);
+  }
+}
+
 function nextTest() {
   var div = document.querySelector("#content>div");
   if (!div) {
     SimpleTest.finish();
     return;
   }
-  getSelection().selectAllChildren(div);
+
+  selectDependingOnAttributes(div);
+
   var expected = div.hasAttribute("data-result") ?
                  div.getAttribute("data-result") :
                  div.textContent;
+
   SimpleTest.waitForClipboard(expected, function() {
     synthesizeKey("C", {accelKey: true});
   }, function() {
     ok(true, div.getAttribute("style") + " passed");
     hasExpectedFlavors();
     div.remove();
     nextTest();
   }, function() {
--- a/dom/base/test/test_user_select.html
+++ b/dom/base/test/test_user_select.html
@@ -288,27 +288,27 @@ function test()
   checkText("aaaa bbbb", e);
   checkRanges([[0,0,-1,1],[6,0,6,5]], e);
   doneTest(e);
 
   clear();
   e = document.getElementById('testG');
   synthesizeMouse(e, 1, 1, {});
   synthesizeMouse(e, 400, 180, { shiftKey: true });
-  checkText("aaaa bbbb", e); // XXX this doesn't seem right - bug 1247799
+  checkText("aaaa\n\n\n\nbbbb", e);
   checkRanges([[0,0,-1,1],[2,0,-1,3],[4,0,-1,5],[6,0,6,5]], e);
   doneTest(e);
 
   clear();
   e = document.getElementById('testH');
   synthesizeMouse(e, 1, 1, {});
   synthesizeMouse(e, 30, 90, { shiftKey: true });
   synthesizeMouse(e, 50, 90, { shiftKey: true });
   synthesizeMouse(e, 70, 90, { shiftKey: true });
-  checkText("aaaa bbb", e);
+  checkText("aaaa\n\nbbb", e);
   checkRanges([[0,0,-1,1],[-1,2,3,4]], e);
 
   doneTest(e);
   // ======================================================
   // ==================== Script tests ====================
   // ======================================================
 
   clear();
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -4,17 +4,16 @@
  * 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/. */
 
 #include "mozilla/dom/HTMLFormElement.h"
 
 #include "jsapi.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/EventDispatcher.h"
-#include "mozilla/EventStateManager.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/dom/nsCSPUtils.h"
 #include "mozilla/dom/nsCSPContext.h"
 #include "mozilla/dom/nsMixedContentBlocker.h"
 #include "mozilla/dom/CustomEvent.h"
 #include "mozilla/dom/HTMLFormControlsCollection.h"
 #include "mozilla/dom/HTMLFormElementBinding.h"
 #include "mozilla/Move.h"
@@ -110,17 +109,16 @@ HTMLFormElement::HTMLFormElement(
       mSubmitPopupState(PopupBlocker::openAbused),
       mInvalidElementsCount(0),
       mGeneratingSubmit(false),
       mGeneratingReset(false),
       mIsSubmitting(false),
       mDeferSubmission(false),
       mNotifiedObservers(false),
       mNotifiedObserversResult(false),
-      mSubmitInitiatedFromUserInput(false),
       mEverTriedInvalidSubmit(false) {
   // We start out valid.
   AddStatesSilently(NS_EVENT_STATE_VALID);
 }
 
 HTMLFormElement::~HTMLFormElement() {
   if (mControls) {
     mControls->DropFormReference();
@@ -566,18 +564,16 @@ nsresult HTMLFormElement::DoSubmit(Widge
   // be a window...
   nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow();
   if (window) {
     mSubmitPopupState = PopupBlocker::GetPopupControlState();
   } else {
     mSubmitPopupState = PopupBlocker::openAbused;
   }
 
-  mSubmitInitiatedFromUserInput = EventStateManager::IsHandlingUserInput();
-
   if (mDeferSubmission) {
     // we are in an event handler, JS submitted so we have to
     // defer this submission. let's remember it and return
     // without submitting
     mPendingSubmission = submission;
     // ensure reentrancy
     mIsSubmitting = false;
     return NS_OK;
@@ -691,29 +687,29 @@ nsresult HTMLFormElement::SubmitSubmissi
   // Submit
   //
   nsCOMPtr<nsIDocShell> docShell;
 
   {
     nsAutoPopupStatePusher popupStatePusher(mSubmitPopupState);
 
     AutoHandlingUserInputStatePusher userInpStatePusher(
-        mSubmitInitiatedFromUserInput, nullptr, doc);
+        aFormSubmission->IsInitiatedFromUserInput(), nullptr, doc);
 
     nsCOMPtr<nsIInputStream> postDataStream;
     rv = aFormSubmission->GetEncodedSubmission(
         actionURI, getter_AddRefs(postDataStream), actionURI);
     NS_ENSURE_SUBMIT_SUCCESS(rv);
 
     nsAutoString target;
     aFormSubmission->GetTarget(target);
     rv = linkHandler->OnLinkClickSync(
         this, actionURI, target, VoidString(), postDataStream, nullptr, false,
         getter_AddRefs(docShell), getter_AddRefs(mSubmittingRequest),
-        EventStateManager::IsHandlingUserInput());
+        aFormSubmission->IsInitiatedFromUserInput());
     NS_ENSURE_SUBMIT_SUCCESS(rv);
   }
 
   // Even if the submit succeeds, it's possible for there to be no docshell
   // or request; for example, if it's to a named anchor within the same page
   // the submit will not really do anything.
   if (docShell) {
     // If the channel is pending, we have to listen for web progress.
--- a/dom/html/HTMLFormElement.h
+++ b/dom/html/HTMLFormElement.h
@@ -589,18 +589,16 @@ class HTMLFormElement final : public nsG
   /** Whether we are submitting currently */
   bool mIsSubmitting;
   /** Whether the submission is to be deferred in case a script triggers it */
   bool mDeferSubmission;
   /** Whether we notified NS_FORMSUBMIT_SUBJECT listeners already */
   bool mNotifiedObservers;
   /** If we notified the listeners early, what was the result? */
   bool mNotifiedObserversResult;
-  /** Keep track of whether a submission was user-initiated or not */
-  bool mSubmitInitiatedFromUserInput;
   /**
    * Whether the submission of this form has been ever prevented because of
    * being invalid.
    */
   bool mEverTriedInvalidSubmit;
 
  private:
   ~HTMLFormElement();
--- a/dom/html/HTMLFormSubmission.h
+++ b/dom/html/HTMLFormSubmission.h
@@ -4,16 +4,17 @@
  * 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/. */
 
 #ifndef mozilla_dom_HTMLFormSubmission_h
 #define mozilla_dom_HTMLFormSubmission_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/Element.h"
+#include "mozilla/EventStateManager.h"
 #include "nsCOMPtr.h"
 #include "mozilla/Encoding.h"
 #include "nsString.h"
 
 class nsIURI;
 class nsIInputStream;
 class nsGenericHTMLElement;
 class nsIMultiplexInputStream;
@@ -98,44 +99,53 @@ class HTMLFormSubmission {
    */
   nsIURI* GetActionURL() const { return mActionURL; }
 
   /**
    * Get the target that will be used for submission.
    */
   void GetTarget(nsAString& aTarget) { aTarget = mTarget; }
 
+  /**
+   * Return true if this form submission was user-initiated.
+   */
+  bool IsInitiatedFromUserInput() const { return mInitiatedFromUserInput; }
+
  protected:
   /**
    * Can only be constructed by subclasses.
    *
    * @param aEncoding the character encoding of the form
    * @param aOriginatingElement the originating element (can be null)
    */
   HTMLFormSubmission(nsIURI* aActionURL, const nsAString& aTarget,
                      mozilla::NotNull<const mozilla::Encoding*> aEncoding,
                      Element* aOriginatingElement)
       : mActionURL(aActionURL),
         mTarget(aTarget),
         mEncoding(aEncoding),
-        mOriginatingElement(aOriginatingElement) {
+        mOriginatingElement(aOriginatingElement),
+        mInitiatedFromUserInput(EventStateManager::IsHandlingUserInput()) {
     MOZ_COUNT_CTOR(HTMLFormSubmission);
   }
 
   // The action url.
   nsCOMPtr<nsIURI> mActionURL;
 
   // The target.
   nsString mTarget;
 
   // The character encoding of this form submission
   mozilla::NotNull<const mozilla::Encoding*> mEncoding;
 
   // Originating element.
   RefPtr<Element> mOriginatingElement;
+
+  // Keep track of whether this form submission was user-initiated or not
+  bool mInitiatedFromUserInput;
 };
 
 class EncodingFormSubmission : public HTMLFormSubmission {
  public:
   EncodingFormSubmission(nsIURI* aActionURL, const nsAString& aTarget,
                          mozilla::NotNull<const mozilla::Encoding*> aEncoding,
                          Element* aOriginatingElement);
 
--- a/dom/html/MediaDocument.h
+++ b/dom/html/MediaDocument.h
@@ -48,17 +48,17 @@ class MediaDocument : public nsHTMLDocum
   // Check whether initial setup has been done.
   MOZ_MUST_USE bool InitialSetupHasBeenDone() const {
     return mDidInitialDocumentSetup;
   }
 
   virtual nsresult CreateSyntheticDocument();
 
   friend class MediaDocumentStreamListener;
-  nsresult StartLayout();
+  virtual nsresult StartLayout();
 
   void GetFileName(nsAString& aResult, nsIChannel* aChannel);
 
   nsresult LinkStylesheet(const nsAString& aStylesheet);
   nsresult LinkScript(const nsAString& aScript);
 
   // |aFormatNames[]| needs to have four elements in the following order:
   // a format name with neither dimension nor file, a format name with
--- a/dom/html/PluginDocument.cpp
+++ b/dom/html/PluginDocument.cpp
@@ -37,23 +37,22 @@ class PluginDocument final : public Medi
   nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
                              nsILoadGroup* aLoadGroup, nsISupports* aContainer,
                              nsIStreamListener** aDocListener,
                              bool aReset = true,
                              nsIContentSink* aSink = nullptr) override;
 
   void SetScriptGlobalObject(
       nsIScriptGlobalObject* aScriptGlobalObject) override;
-  bool CanSavePresentation(nsIRequest* aNewRequest) override;
+  bool CanSavePresentation(nsIRequest* aNewRequest,
+                           uint16_t& aBFCacheStatus) override;
 
   const nsCString& GetType() const { return mMimeType; }
   Element* GetPluginContent() { return mPluginContent; }
 
-  void StartLayout() { MediaDocument::StartLayout(); }
-
   virtual void Destroy() override {
     if (mStreamListener) {
       mStreamListener->DropDocumentRef();
     }
     MediaDocument::Destroy();
   }
 
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PluginDocument, MediaDocument)
@@ -133,17 +132,18 @@ void PluginDocument::SetScriptGlobalObje
       NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic document");
       InitialSetupDone();
     }
   } else {
     mStreamListener = nullptr;
   }
 }
 
-bool PluginDocument::CanSavePresentation(nsIRequest* aNewRequest) {
+bool PluginDocument::CanSavePresentation(nsIRequest* aNewRequest,
+                                         uint16_t& aBFCacheStatus) {
   // Full-page plugins cannot be cached, currently, because we don't have
   // the stream listener data to feed to the plugin instance.
   return false;
 }
 
 nsresult PluginDocument::StartDocumentLoad(const char* aCommand,
                                            nsIChannel* aChannel,
                                            nsILoadGroup* aLoadGroup,
--- a/dom/html/VideoDocument.cpp
+++ b/dom/html/VideoDocument.cpp
@@ -33,22 +33,23 @@ class VideoDocument final : public Media
 
   virtual void Destroy() override {
     if (mStreamListener) {
       mStreamListener->DropDocumentRef();
     }
     MediaDocument::Destroy();
   }
 
+  nsresult StartLayout() override;
+
  protected:
+  nsresult CreateVideoElement();
   // Sets document <title> to reflect the file name and description.
   void UpdateTitle(nsIChannel* aChannel);
 
-  nsresult CreateSyntheticVideoDocument();
-
   RefPtr<MediaDocumentStreamListener> mStreamListener;
 };
 
 nsresult VideoDocument::StartDocumentLoad(const char* aCommand,
                                           nsIChannel* aChannel,
                                           nsILoadGroup* aLoadGroup,
                                           nsISupports* aContainer,
                                           nsIStreamListener** aDocListener,
@@ -57,47 +58,55 @@ nsresult VideoDocument::StartDocumentLoa
       aCommand, aChannel, aLoadGroup, aContainer, aDocListener, aReset, aSink);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mStreamListener = new MediaDocumentStreamListener(this);
   NS_ADDREF(*aDocListener = mStreamListener);
   return rv;
 }
 
+nsresult VideoDocument::StartLayout() {
+  // Create video element, and begin loading the media resource. Note we
+  // delay creating the video element until now (we're called from
+  // MediaDocumentStreamListener::OnStartRequest) as the PresShell is likely
+  // to have been created by now, so the MediaDecoder will be able to tell
+  // what kind of compositor we have, so the video element knows whether
+  // it can create a hardware accelerated video decoder or not.
+  nsresult rv = CreateVideoElement();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = MediaDocument::StartLayout();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 void VideoDocument::SetScriptGlobalObject(
     nsIScriptGlobalObject* aScriptGlobalObject) {
   // Set the script global object on the superclass before doing
   // anything that might require it....
   MediaDocument::SetScriptGlobalObject(aScriptGlobalObject);
 
   if (aScriptGlobalObject && !InitialSetupHasBeenDone()) {
-    // Create synthetic document
-#ifdef DEBUG
-    nsresult rv =
-#endif
-        CreateSyntheticVideoDocument();
+    DebugOnly<nsresult> rv = CreateSyntheticDocument();
     NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic video document");
 
     if (!nsContentUtils::IsChildOfSameType(this)) {
       LinkStylesheet(NS_LITERAL_STRING(
           "resource://content-accessible/TopLevelVideoDocument.css"));
       LinkStylesheet(NS_LITERAL_STRING(
           "chrome://global/skin/media/TopLevelVideoDocument.css"));
       LinkScript(NS_LITERAL_STRING(
           "chrome://global/content/TopLevelVideoDocument.js"));
     }
     InitialSetupDone();
   }
 }
 
-nsresult VideoDocument::CreateSyntheticVideoDocument() {
-  // make our generic document
-  nsresult rv = MediaDocument::CreateSyntheticDocument();
-  NS_ENSURE_SUCCESS(rv, rv);
-
+nsresult VideoDocument::CreateVideoElement() {
   Element* body = GetBodyElement();
   if (!body) {
     NS_WARNING("no body on video document!");
     return NS_ERROR_FAILURE;
   }
 
   // make content
   RefPtr<mozilla::dom::NodeInfo> nodeInfo;
--- a/dom/interfaces/base/nsIContentPrefService2.idl
+++ b/dom/interfaces/base/nsIContentPrefService2.idl
@@ -186,24 +186,21 @@ interface nsIContentPrefService2 : nsISu
    * The preferences are returned in an array through the out-parameter.  If a
    * preference for a particular subdomain is known not to exist, then an object
    * corresponding to that preference will be present in the array, and, as with
    * getCachedByDomainAndName, its value attribute will be undefined.
    *
    * @param domain   The preferences' domain.
    * @param name     The preferences' name.
    * @param context  The private-browsing context, if any.
-   * @param len      The length of the returned array.
-   * @param prefs    The array of preferences.
+   * @return         The array of preferences.
    */
-  void getCachedBySubdomainAndName(in AString domain,
-                                   in AString name,
-                                   in nsILoadContext context,
-                                   [optional] out unsigned long len,
-                                   [retval,array,size_is(len)] out nsIContentPref prefs);
+  Array<nsIContentPref> getCachedBySubdomainAndName(in AString domain,
+                                                    in AString name,
+                                                    in nsILoadContext context);
 
   /**
    * Synchronously retrieves from the in-memory cache the preference with no
    * domain and the given name.
    *
    * As with getCachedByDomainAndName, if the preference is cached then it is
    * returned; if the preference is known not to exist, then the value attribute
    * of the returned object will be undefined; if the preference is neither
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -167,18 +167,17 @@ interface nsIServiceWorkerManager : nsIS
                                   in AString aBody,
                                   in AString aTag,
                                   in AString aIcon,
                                   in AString aData,
                                   in AString aBehavior);
 
   [optional_argc] void sendPushEvent(in ACString aOriginAttributes,
                                      in ACString aScope,
-                                     [optional] in uint32_t aDataLength,
-                                     [optional, array, size_is(aDataLength)] in uint8_t aDataBytes);
+                                     [optional] in Array<uint8_t> aDataBytes);
   void sendPushSubscriptionChangeEvent(in ACString aOriginAttributes,
                                        in ACString scope);
 
   void addListener(in nsIServiceWorkerManagerListener aListener);
 
   void removeListener(in nsIServiceWorkerManagerListener aListener);
 
   bool isParentInterceptEnabled();
--- a/dom/interfaces/storage/nsIDOMStorageManager.idl
+++ b/dom/interfaces/storage/nsIDOMStorageManager.idl
@@ -34,23 +34,26 @@ interface nsIDOMStorageManager : nsISupp
    * Returns instance of DOM storage object for given principal.
    * A new object is always returned and it is ensured there is
    * a storage for the scope created.
    *
    * @param aWindow
    *    The parent window.
    * @param aPrincipal
    *    Principal to bound storage to.
+   * @param aStoragePrincipal
+   *    StoragePrincipal to bound storage to.
    * @param aDocumentURI
    *    URL of the demanding document, used for DOM storage event only.
    * @param aPrivate
    *    Whether the demanding document is running in Private Browsing mode or not.
    */
   Storage createStorage(in mozIDOMWindow aWindow,
                         in nsIPrincipal aPrincipal,
+                        in nsIPrincipal aStoragePrincipal,
                         in AString aDocumentURI,
                         [optional] in bool aPrivate);
   /**
    * DEPRECATED.  The only good reason to use this was if you were writing a
    * test and wanted to hackily determine if a preload happened.  That's now
    * covered by `nsILocalStorageManager.isPreloaded` and you should use that if
    * that's what you want.  If LSNG is in use, this will throw.
    *
@@ -58,21 +61,24 @@ interface nsIDOMStorageManager : nsISupp
    * If there is no storage managed for the scope, then null is returned and
    * no object is created.  Otherwise, an object (new) for the existing storage
    * scope is returned.
    *
    * @param aWindow
    *    The parent window.
    * @param aPrincipal
    *    Principal to bound storage to.
+   * @param aStoragePrincipal
+   *    StoragePrincipal to bound storage to.
    * @param aPrivate
    *    Whether the demanding document is running in Private Browsing mode or not.
    */
   Storage getStorage(in mozIDOMWindow aWindow,
                      in nsIPrincipal aPrincipal,
+                     in nsIPrincipal aStoragePrincipal,
                      [optional] in bool aPrivate);
 
   /**
    * Clones given storage into this storage manager.
    *
    * @param aStorageToCloneFrom
    *    The storage to copy all items from into this manager.  Manager will then
    *    return a new and independent object that contains snapshot of data from
--- a/dom/ipc/BrowserBridgeParent.cpp
+++ b/dom/ipc/BrowserBridgeParent.cpp
@@ -177,16 +177,21 @@ IPCResult BrowserBridgeParent::RecvDispa
   layers::InputAPZContext context(
       layers::ScrollableLayerGuid(event.mLayersId, 0,
                                   layers::ScrollableLayerGuid::NULL_SCROLL_ID),
       0, nsEventStatus_eIgnore);
   mBrowserParent->SendRealMouseEvent(event);
   return IPC_OK();
 }
 
+IPCResult BrowserBridgeParent::RecvSkipBrowsingContextDetach() {
+  mBrowserParent->SkipBrowsingContextDetach();
+  return IPC_OK();
+}
+
 IPCResult BrowserBridgeParent::RecvActivate() {
   mBrowserParent->Activate();
   return IPC_OK();
 }
 
 IPCResult BrowserBridgeParent::RecvDeactivate() {
   mBrowserParent->Deactivate();
   return IPC_OK();
--- a/dom/ipc/BrowserBridgeParent.h
+++ b/dom/ipc/BrowserBridgeParent.h
@@ -54,16 +54,18 @@ class BrowserBridgeParent : public PBrow
                                            const LayersObserverEpoch& aEpoch);
 
   mozilla::ipc::IPCResult RecvNavigateByKey(const bool& aForward,
                                             const bool& aForDocumentNavigation);
 
   mozilla::ipc::IPCResult RecvDispatchSynthesizedMouseEvent(
       const WidgetMouseEvent& aEvent);
 
+  mozilla::ipc::IPCResult RecvSkipBrowsingContextDetach();
+
   mozilla::ipc::IPCResult RecvActivate();
 
   mozilla::ipc::IPCResult RecvDeactivate();
 
   mozilla::ipc::IPCResult RecvSetIsUnderHiddenEmbedderElement(
       const bool& aIsUnderHiddenEmbedderElement);
 
   void ActorDestroy(ActorDestroyReason aWhy) override;
--- a/dom/ipc/BrowserChild.cpp
+++ b/dom/ipc/BrowserChild.cpp
@@ -1047,16 +1047,27 @@ BrowserChild::~BrowserChild() {
   nsCOMPtr<nsIWebBrowser> webBrowser = do_QueryInterface(WebNavigation());
   if (webBrowser) {
     webBrowser->SetContainerWindow(nullptr);
   }
 
   mozilla::DropJSObjects(this);
 }
 
+mozilla::ipc::IPCResult BrowserChild::RecvSkipBrowsingContextDetach() {
+  nsCOMPtr<nsIDocShell> docShell = do_GetInterface(WebNavigation());
+  if (!docShell) {
+    return IPC_OK();
+  }
+  RefPtr<nsDocShell> docshell = nsDocShell::Cast(docShell);
+  MOZ_ASSERT(docshell);
+  docshell->SkipBrowsingContextDetach();
+  return IPC_OK();
+}
+
 mozilla::ipc::IPCResult BrowserChild::RecvLoadURL(const nsCString& aURI,
                                                   const ShowInfo& aInfo) {
   if (!mDidLoadURLInit) {
     mDidLoadURLInit = true;
     if (!InitBrowserChildMessageManager()) {
       return IPC_FAIL_NO_REASON(this);
     }
 
--- a/dom/ipc/BrowserChild.h
+++ b/dom/ipc/BrowserChild.h
@@ -550,16 +550,17 @@ class BrowserChild final : public Browse
       const mozilla::NativeEventData& aKeyEventData, const bool& aIsConsumed);
 
   mozilla::ipc::IPCResult RecvPrint(const uint64_t& aOuterWindowID,
                                     const PrintData& aPrintData);
 
   mozilla::ipc::IPCResult RecvUpdateNativeWindowHandle(
       const uintptr_t& aNewHandle);
 
+  mozilla::ipc::IPCResult RecvSkipBrowsingContextDetach();
   /**
    * Native widget remoting protocol for use with windowed plugins with e10s.
    */
   PPluginWidgetChild* AllocPPluginWidgetChild();
 
   bool DeallocPPluginWidgetChild(PPluginWidgetChild* aActor);
 
 #ifdef XP_WIN
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -531,17 +531,17 @@ void BrowserParent::SetOwnerElement(Elem
   }
 
   if (mRenderFrame.IsInitialized()) {
     mRenderFrame.OwnerContentChanged();
   }
 
   // Set our BrowsingContext's embedder if we're not embedded within a
   // BrowserBridgeParent.
-  if (!GetBrowserBridgeParent() && mBrowsingContext) {
+  if (!GetBrowserBridgeParent() && mBrowsingContext && mFrameElement) {
     mBrowsingContext->SetEmbedderElement(mFrameElement);
   }
 
   VisitChildren([aElement](BrowserBridgeParent* aBrowser) {
     aBrowser->GetBrowserParent()->SetOwnerElement(aElement);
   });
 }
 
@@ -3739,16 +3739,22 @@ BrowserParent::StopApzAutoscroll(nsViewI
                                gfxUtils::GetContentRenderRoot());
 
       widget->StopAsyncAutoscroll(guid);
     }
   }
   return NS_OK;
 }
 
+void BrowserParent::SkipBrowsingContextDetach() {
+  RefPtr<nsFrameLoader> fl = GetFrameLoader();
+  MOZ_ASSERT(fl);
+  fl->SkipBrowsingContextDetach();
+}
+
 mozilla::ipc::IPCResult BrowserParent::RecvLookUpDictionary(
     const nsString& aText, nsTArray<FontRange>&& aFontRangeArray,
     const bool& aIsVertical, const LayoutDeviceIntPoint& aPoint) {
   nsCOMPtr<nsIWidget> widget = GetWidget();
   if (!widget) {
     return IPC_OK();
   }
 
--- a/dom/ipc/BrowserParent.h
+++ b/dom/ipc/BrowserParent.h
@@ -673,16 +673,18 @@ class BrowserParent final : public PBrow
   void LiveResizeStarted() override;
   void LiveResizeStopped() override;
 
   void SetReadyToHandleInputEvents() { mIsReadyToHandleInputEvents = true; }
   bool IsReadyToHandleInputEvents() { return mIsReadyToHandleInputEvents; }
 
   void NavigateByKey(bool aForward, bool aForDocumentNavigation);
 
+  void SkipBrowsingContextDetach();
+
  protected:
   bool ReceiveMessage(
       const nsString& aMessage, bool aSync, ipc::StructuredCloneData* aData,
       mozilla::jsipc::CpowHolder* aCpows, nsIPrincipal* aPrincipal,
       nsTArray<ipc::StructuredCloneData>* aJSONRetVal = nullptr);
 
   mozilla::ipc::IPCResult RecvAsyncAuthPrompt(const nsCString& aUri,
                                               const nsString& aRealm,
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -2293,16 +2293,18 @@ void ContentChild::ActorDestroy(ActorDes
   BlobURLProtocolHandler::RemoveDataEntries();
 
   mSharedData = nullptr;
 
   mAlertObservers.Clear();
 
   mIdleObservers.Clear();
 
+  mBrowsingContextGroupHolder.Clear();
+
   nsCOMPtr<nsIConsoleService> svc(do_GetService(NS_CONSOLESERVICE_CONTRACTID));
   if (svc) {
     svc->UnregisterListener(mConsoleListener);
     mConsoleListener->mChild = nullptr;
   }
   mIsAlive = false;
 
   CrashReporterClient::DestroySingleton();
@@ -3793,17 +3795,16 @@ mozilla::ipc::IPCResult ContentChild::Re
   aContext->RestoreChildren(std::move(children), /* aFromIPC */ true);
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult ContentChild::RecvRegisterBrowsingContextGroup(
     nsTArray<BrowsingContext::IPCInitializer>&& aInits) {
   RefPtr<BrowsingContextGroup> group = new BrowsingContextGroup();
-
   // Each of the initializers in aInits is sorted in pre-order, so our parent
   // should always be available before the element itself.
   for (auto& init : aInits) {
 #ifdef DEBUG
     RefPtr<BrowsingContext> existing = BrowsingContext::Get(init.mId);
     MOZ_ASSERT(!existing, "BrowsingContext must not exist yet!");
 
     RefPtr<BrowsingContext> parent = init.GetParent();
@@ -3906,16 +3907,21 @@ mozilla::ipc::IPCResult ContentChild::Re
     BrowsingContext* aContext, BrowsingContext::Transaction&& aTransaction,
     BrowsingContext::FieldEpochs&& aEpochs) {
   if (aContext) {
     aTransaction.Apply(aContext, nullptr, &aEpochs);
   }
   return IPC_OK();
 }
 
+void ContentChild::HoldBrowsingContextGroup(BrowsingContextGroup* aBCG) {
+  RefPtr<BrowsingContextGroup> bcgPtr(aBCG);
+  mBrowsingContextGroupHolder.AppendElement(bcgPtr);
+}
+
 }  // namespace dom
 
 #if defined(__OpenBSD__) && defined(MOZ_SANDBOX)
 #  include <unistd.h>
 
 static LazyLogModule sPledgeLog("SandboxPledge");
 
 bool StartOpenBSDSandbox(GeckoProcessType type) {
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -669,16 +669,18 @@ class ContentChild final : public PConte
       const uint32_t& aRegistrarId, nsIURI* aURI, const uint32_t& aNewLoadFlags,
       const Maybe<LoadInfoArgs>& aLoadInfoForwarder, const uint64_t& aChannelId,
       nsIURI* aOriginalURI, const uint64_t& aIdentifier,
       const uint32_t& aRedirectMode);
 
   mozilla::ipc::IPCResult RecvStartDelayedAutoplayMediaComponents(
       BrowsingContext* aContext);
 
+  void HoldBrowsingContextGroup(BrowsingContextGroup* aBCG);
+
 #ifdef NIGHTLY_BUILD
   // Fetch the current number of pending input events.
   //
   // NOTE: This method performs an atomic read, and is safe to call from all
   // threads.
   uint32_t GetPendingInputEvents() { return mPendingInputEvents; }
 #endif
 
@@ -809,16 +811,18 @@ class ContentChild final : public PConte
 #ifdef NIGHTLY_BUILD
   // NOTE: This member is atomic because it can be accessed from
   // off-main-thread.
   mozilla::Atomic<uint32_t> mPendingInputEvents;
 #endif
 
   uint32_t mNetworkLinkType = 0;
 
+  nsTArray<RefPtr<BrowsingContextGroup>> mBrowsingContextGroupHolder;
+
   DISALLOW_EVIL_CONSTRUCTORS(ContentChild);
 };
 
 uint64_t NextWindowID();
 
 }  // namespace dom
 }  // namespace mozilla
 
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -943,16 +943,17 @@ child:
      */
     async SetWidgetNativeData(WindowsHandle aHandle);
 
     /**
      * Requests the content blocking log, which is sent back in response.
      */
     async GetContentBlockingLog() returns(nsCString log, bool success);
 
+    async SkipBrowsingContextDetach();
 parent:
     /** Records a history visit. */
     async VisitURI(URIParams aURI, URIParams? aLastVisitedURI,
                    uint32_t aFlags);
 
     /** Fetches the visited status for an array of URIs (Android-only). */
     async QueryVisitedState(URIParams[] aURIs);
 
--- a/dom/ipc/PBrowserBridge.ipdl
+++ b/dom/ipc/PBrowserBridge.ipdl
@@ -69,12 +69,14 @@ parent:
   /**
    * Sending an activate message moves focus to the iframe.
    */
   async Activate();
 
   async Deactivate();
 
   async SetIsUnderHiddenEmbedderElement(bool aIsUnderHiddenEmbedderElement);
+
+  async SkipBrowsingContextDetach();
 };
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/ipc/WindowGlobalParent.cpp
+++ b/dom/ipc/WindowGlobalParent.cpp
@@ -77,20 +77,16 @@ void WindowGlobalParent::Init(const Wind
   ContentParentId processId(0);
   if (!mInProcess) {
     processId = static_cast<ContentParent*>(Manager()->Manager())->ChildID();
   }
 
   mBrowsingContext = CanonicalBrowsingContext::Cast(aInit.browsingContext());
   MOZ_ASSERT(mBrowsingContext);
 
-  // XXX(nika): This won't be the case soon, but for now this is a good
-  // assertion as we can't switch processes. We should relax this eventually.
-  MOZ_ASSERT(mBrowsingContext->IsOwnedByProcess(processId));
-
   // Attach ourself to the browsing context.
   mBrowsingContext->RegisterWindowGlobal(this);
 
   // If there is no current window global, assume we're about to become it
   // optimistically.
   if (!mBrowsingContext->GetCurrentWindowGlobal()) {
     mBrowsingContext->SetCurrentWindowGlobal(this);
   }
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -34,16 +34,17 @@
 #include "mozilla/dom/quota/UsageInfo.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "mozilla/ipc/PBackgroundParent.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/Logging.h"
 #include "mozilla/storage/Variant.h"
+#include "mozilla/StoragePrincipalHelper.h"
 #include "nsClassHashtable.h"
 #include "nsDataHashtable.h"
 #include "nsExceptionHandler.h"
 #include "nsInterfaceHashtable.h"
 #include "nsIObjectInputStream.h"
 #include "nsIObjectOutputStream.h"
 #include "nsISimpleEnumerator.h"
 #include "nsNetUtil.h"
@@ -3375,16 +3376,17 @@ bool DeallocPBackgroundLSObserverParent(
   // Transfer ownership back from IPDL.
   RefPtr<Observer> actor = dont_AddRef(static_cast<Observer*>(aActor));
 
   return true;
 }
 
 bool VerifyPrincipalInfo(const Maybe<ContentParentId>& aContentParentId,
                          const PrincipalInfo& aPrincipalInfo,
+                         const PrincipalInfo& aStoragePrincipalInfo,
                          const Maybe<nsID>& aClientId) {
   AssertIsOnBackgroundThread();
 
   if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
     ASSERT_UNLESS_FUZZING();
     return false;
   }
 
@@ -3392,17 +3394,19 @@ bool VerifyPrincipalInfo(const Maybe<Con
     RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance();
     if (svc &&
         !svc->HasWindow(aContentParentId, aPrincipalInfo, aClientId.ref())) {
       ASSERT_UNLESS_FUZZING();
       return false;
     }
   }
 
-  return true;
+  return StoragePrincipalHelper::
+      VerifyValidStoragePrincipalInfoForPrincipalInfo(aStoragePrincipalInfo,
+                                                      aPrincipalInfo);
 }
 
 bool VerifyOriginKey(const nsACString& aOriginKey,
                      const PrincipalInfo& aPrincipalInfo) {
   AssertIsOnBackgroundThread();
 
   nsCString originAttrSuffix;
   nsCString originKey;
@@ -3427,18 +3431,19 @@ bool VerifyRequestParams(const Maybe<Con
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
 
   switch (aParams.type()) {
     case LSRequestParams::TLSRequestPreloadDatastoreParams: {
       const LSRequestCommonParams& params =
           aParams.get_LSRequestPreloadDatastoreParams().commonParams();
 
-      if (NS_WARN_IF(!VerifyPrincipalInfo(aContentParentId,
-                                          params.principalInfo(), Nothing()))) {
+      if (NS_WARN_IF(
+              !VerifyPrincipalInfo(aContentParentId, params.principalInfo(),
+                                   params.storagePrincipalInfo(), Nothing()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
       }
 
       if (NS_WARN_IF(
               !VerifyOriginKey(params.originKey(), params.principalInfo()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
@@ -3447,19 +3452,19 @@ bool VerifyRequestParams(const Maybe<Con
     }
 
     case LSRequestParams::TLSRequestPrepareDatastoreParams: {
       const LSRequestPrepareDatastoreParams& params =
           aParams.get_LSRequestPrepareDatastoreParams();
 
       const LSRequestCommonParams& commonParams = params.commonParams();
 
-      if (NS_WARN_IF(!VerifyPrincipalInfo(aContentParentId,
-                                          commonParams.principalInfo(),
-                                          params.clientId()))) {
+      if (NS_WARN_IF(!VerifyPrincipalInfo(
+              aContentParentId, commonParams.principalInfo(),
+              commonParams.storagePrincipalInfo(), params.clientId()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
       }
 
       if (NS_WARN_IF(!VerifyOriginKey(commonParams.originKey(),
                                       commonParams.principalInfo()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
@@ -3467,17 +3472,18 @@ bool VerifyRequestParams(const Maybe<Con
       break;
     }
 
     case LSRequestParams::TLSRequestPrepareObserverParams: {
       const LSRequestPrepareObserverParams& params =
           aParams.get_LSRequestPrepareObserverParams();
 
       if (NS_WARN_IF(!VerifyPrincipalInfo(
-              aContentParentId, params.principalInfo(), params.clientId()))) {
+              aContentParentId, params.principalInfo(),
+              params.storagePrincipalInfo(), params.clientId()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
       }
       break;
     }
 
     default:
       MOZ_CRASH("Should never get here!");
@@ -3588,18 +3594,19 @@ bool VerifyRequestParams(const Maybe<Con
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
 
   switch (aParams.type()) {
     case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
       const LSSimpleRequestPreloadedParams& params =
           aParams.get_LSSimpleRequestPreloadedParams();
 
-      if (NS_WARN_IF(!VerifyPrincipalInfo(aContentParentId,
-                                          params.principalInfo(), Nothing()))) {
+      if (NS_WARN_IF(
+              !VerifyPrincipalInfo(aContentParentId, params.principalInfo(),
+                                   params.storagePrincipalInfo(), Nothing()))) {
         ASSERT_UNLESS_FUZZING();
         return false;
       }
       break;
     }
 
     default:
       MOZ_CRASH("Should never get here!");
@@ -6599,25 +6606,26 @@ nsresult PrepareDatastoreOp::Open() {
   MOZ_ASSERT(mState == State::Opening);
   MOZ_ASSERT(mNestedState == NestedState::BeforeNesting);
 
   if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
       !MayProceedOnNonOwningThread()) {
     return NS_ERROR_FAILURE;
   }
 
-  const PrincipalInfo& principalInfo = mParams.principalInfo();
-
-  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+  const PrincipalInfo& storagePrincipalInfo = mParams.storagePrincipalInfo();
+
+  if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
     QuotaManager::GetInfoForChrome(&mSuffix, &mGroup, &mOrigin);
   } else {
-    MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+    MOZ_ASSERT(storagePrincipalInfo.type() ==
+               PrincipalInfo::TContentPrincipalInfo);
 
     QuotaManager::GetInfoFromValidatedPrincipalInfo(
-        principalInfo, &mSuffix, &mGroup, &mMainThreadOrigin);
+        storagePrincipalInfo, &mSuffix, &mGroup, &mMainThreadOrigin);
   }
 
   mState = State::Nesting;
   mNestedState = NestedState::CheckExistingOperations;
 
   MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
 
   return NS_OK;
@@ -6629,27 +6637,29 @@ nsresult PrepareDatastoreOp::CheckExisti
   MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations);
   MOZ_ASSERT(gPrepareDatastoreOps);
 
   if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
       !MayProceed()) {
     return NS_ERROR_FAILURE;
   }
 
-  const PrincipalInfo& principalInfo = mParams.principalInfo();
+  const PrincipalInfo& storagePrincipalInfo = mParams.storagePrincipalInfo();
 
   nsCString originAttrSuffix;
   uint32_t privateBrowsingId;
 
-  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+  if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
     privateBrowsingId = 0;
   } else {
-    MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
-
-    const ContentPrincipalInfo& info = principalInfo.get_ContentPrincipalInfo();
+    MOZ_ASSERT(storagePrincipalInfo.type() ==
+               PrincipalInfo::TContentPrincipalInfo);
+
+    const ContentPrincipalInfo& info =
+        storagePrincipalInfo.get_ContentPrincipalInfo();
     const OriginAttributes& attrs = info.attrs();
     attrs.CreateSuffix(originAttrSuffix);
 
     privateBrowsingId = attrs.mPrivateBrowsingId;
   }
 
   mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(
       originAttrSuffix, mParams.originKey());
@@ -7905,25 +7915,26 @@ nsresult PrepareObserverOp::Open() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mState == State::Opening);
 
   if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
       !MayProceedOnNonOwningThread()) {
     return NS_ERROR_FAILURE;
   }
 
-  const PrincipalInfo& principalInfo = mParams.principalInfo();
-
-  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+  const PrincipalInfo& storagePrincipalInfo = mParams.storagePrincipalInfo();
+
+  if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
     QuotaManager::GetInfoForChrome(nullptr, nullptr, &mOrigin);
   } else {
-    MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
-
-    QuotaManager::GetInfoFromValidatedPrincipalInfo(principalInfo, nullptr,
-                                                    nullptr, &mOrigin);
+    MOZ_ASSERT(storagePrincipalInfo.type() ==
+               PrincipalInfo::TContentPrincipalInfo);
+
+    QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo,
+                                                    nullptr, nullptr, &mOrigin);
   }
 
   mState = State::SendingReadyMessage;
   MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
 
   return NS_OK;
 }
 
@@ -8046,25 +8057,26 @@ nsresult PreloadedOp::Open() {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mState == State::Opening);
 
   if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
       !MayProceedOnNonOwningThread()) {
     return NS_ERROR_FAILURE;
   }
 
-  const PrincipalInfo& principalInfo = mParams.principalInfo();
-
-  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+  const PrincipalInfo& storagePrincipalInfo = mParams.storagePrincipalInfo();
+
+  if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
     QuotaManager::GetInfoForChrome(nullptr, nullptr, &mOrigin);
   } else {
-    MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
-
-    QuotaManager::GetInfoFromValidatedPrincipalInfo(principalInfo, nullptr,
-                                                    nullptr, &mOrigin);
+    MOZ_ASSERT(storagePrincipalInfo.type() ==
+               PrincipalInfo::TContentPrincipalInfo);
+
+    QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo,
+                                                    nullptr, nullptr, &mOrigin);
   }
 
   mState = State::SendingResults;
   MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
 
   return NS_OK;
 }
 
@@ -8965,21 +8977,21 @@ nsresult QuotaClient::CreateArchivedOrig
                                               &attrs))) {
       return NS_ERROR_FAILURE;
     }
 
     ContentPrincipalInfo contentPrincipalInfo;
     contentPrincipalInfo.attrs() = attrs;
     contentPrincipalInfo.spec() = spec;
 
-    PrincipalInfo principalInfo(contentPrincipalInfo);
+    PrincipalInfo storagePrincipalInfo(contentPrincipalInfo);
 
     nsCString originAttrSuffix;
     nsCString originKey;
-    rv = GenerateOriginKey2(principalInfo, originAttrSuffix, originKey);
+    rv = GenerateOriginKey2(storagePrincipalInfo, originAttrSuffix, originKey);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     archivedOriginScope =
         ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey);
   } else if (aOriginScope.IsPrefix()) {
     nsCString spec;
@@ -8988,21 +9000,21 @@ nsresult QuotaClient::CreateArchivedOrig
                                               spec, &attrs))) {
       return NS_ERROR_FAILURE;
     }
 
     ContentPrincipalInfo contentPrincipalInfo;
     contentPrincipalInfo.attrs() = attrs;
     contentPrincipalInfo.spec() = spec;
 
-    PrincipalInfo principalInfo(contentPrincipalInfo);
+    PrincipalInfo storagePrincipalInfo(contentPrincipalInfo);
 
     nsCString originAttrSuffix;
     nsCString originKey;
-    rv = GenerateOriginKey2(principalInfo, originAttrSuffix, originKey);
+    rv = GenerateOriginKey2(storagePrincipalInfo, originAttrSuffix, originKey);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     archivedOriginScope = ArchivedOriginScope::CreateFromPrefix(originKey);
   } else if (aOriginScope.IsPattern()) {
     archivedOriginScope =
         ArchivedOriginScope::CreateFromPattern(aOriginScope.GetPattern());
--- a/dom/localstorage/LSObject.cpp
+++ b/dom/localstorage/LSObject.cpp
@@ -190,18 +190,19 @@ class RequestHelper final : public Runna
   NS_DECL_NSIRUNNABLE
 
   // LSRequestChildCallback
   void OnResponse(const LSRequestResponse& aResponse) override;
 };
 
 }  // namespace
 
-LSObject::LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal)
-    : Storage(aWindow, aPrincipal),
+LSObject::LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+                   nsIPrincipal* aStoragePrincipal)
+    : Storage(aWindow, aPrincipal, aStoragePrincipal),
       mPrivateBrowsingId(0),
       mInExplicitSnapshot(false) {
   AssertIsOnOwningThread();
   MOZ_ASSERT(NextGenLocalStorageEnabled());
 }
 
 LSObject::~LSObject() {
   AssertIsOnOwningThread();
@@ -239,64 +240,80 @@ void LSObject::Initialize() {
 
 // static
 nsresult LSObject::CreateForWindow(nsPIDOMWindowInner* aWindow,
                                    Storage** aStorage) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aWindow);
   MOZ_ASSERT(aStorage);
   MOZ_ASSERT(NextGenLocalStorageEnabled());
-  MOZ_ASSERT(nsContentUtils::StorageAllowedForWindow(aWindow) >
+  MOZ_ASSERT(nsContentUtils::StorageAllowedForWindow(aWindow) !=
              nsContentUtils::StorageAccess::eDeny);
 
   nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
   MOZ_ASSERT(sop);
 
   nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
   if (NS_WARN_IF(!principal)) {
     return NS_ERROR_FAILURE;
   }
 
+  nsCOMPtr<nsIPrincipal> storagePrincipal = sop->GetEffectiveStoragePrincipal();
+  if (NS_WARN_IF(!storagePrincipal)) {
+    return NS_ERROR_FAILURE;
+  }
+
   if (nsContentUtils::IsSystemPrincipal(principal)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // localStorage is not available on some pages on purpose, for example
   // about:home. Match the old implementation by using GenerateOriginKey
   // for the check.
   nsCString originAttrSuffix;
   nsCString originKey;
-  nsresult rv = GenerateOriginKey(principal, originAttrSuffix, originKey);
+  nsresult rv =
+      GenerateOriginKey(storagePrincipal, originAttrSuffix, originKey);
   if (NS_FAILED(rv)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   nsAutoPtr<PrincipalInfo> principalInfo(new PrincipalInfo());
   rv = PrincipalToPrincipalInfo(principal, principalInfo);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo);
 
-  if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*principalInfo))) {
+  nsAutoPtr<PrincipalInfo> storagePrincipalInfo(new PrincipalInfo());
+  rv = PrincipalToPrincipalInfo(storagePrincipal, storagePrincipalInfo);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(storagePrincipalInfo->type() ==
+             PrincipalInfo::TContentPrincipalInfo);
+
+  if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) {
     return NS_ERROR_FAILURE;
   }
 
   nsCString suffix;
   nsCString origin;
-  rv = QuotaManager::GetInfoFromPrincipal(principal, &suffix, nullptr, &origin);
+  rv = QuotaManager::GetInfoFromPrincipal(storagePrincipal, &suffix, nullptr,
+                                          &origin);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   MOZ_ASSERT(originAttrSuffix == suffix);
 
   uint32_t privateBrowsingId;
-  rv = principal->GetPrivateBrowsingId(&privateBrowsingId);
+  rv = storagePrincipal->GetPrivateBrowsingId(&privateBrowsingId);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo();
   if (clientInfo.isNothing()) {
     return NS_ERROR_FAILURE;
   }
@@ -306,61 +323,75 @@ nsresult LSObject::CreateForWindow(nsPID
   nsString documentURI;
   if (nsCOMPtr<Document> doc = aWindow->GetExtantDoc()) {
     rv = doc->GetDocumentURI(documentURI);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
-  RefPtr<LSObject> object = new LSObject(aWindow, principal);
+  RefPtr<LSObject> object = new LSObject(aWindow, principal, storagePrincipal);
   object->mPrincipalInfo = std::move(principalInfo);
+  object->mStoragePrincipalInfo = std::move(storagePrincipalInfo);
   object->mPrivateBrowsingId = privateBrowsingId;
   object->mClientId = clientId;
   object->mOrigin = origin;
   object->mOriginKey = originKey;
   object->mDocumentURI = documentURI;
 
   object.forget(aStorage);
   return NS_OK;
 }
 
 // static
 nsresult LSObject::CreateForPrincipal(nsPIDOMWindowInner* aWindow,
                                       nsIPrincipal* aPrincipal,
+                                      nsIPrincipal* aStoragePrincipal,
                                       const nsAString& aDocumentURI,
                                       bool aPrivate, LSObject** aObject) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aPrincipal);
+  MOZ_ASSERT(aStoragePrincipal);
   MOZ_ASSERT(aObject);
 
   nsCString originAttrSuffix;
   nsCString originKey;
-  nsresult rv = GenerateOriginKey(aPrincipal, originAttrSuffix, originKey);
+  nsresult rv =
+      GenerateOriginKey(aStoragePrincipal, originAttrSuffix, originKey);
   if (NS_FAILED(rv)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   nsAutoPtr<PrincipalInfo> principalInfo(new PrincipalInfo());
   rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo ||
              principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo);
 
-  if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*principalInfo))) {
+  nsAutoPtr<PrincipalInfo> storagePrincipalInfo(new PrincipalInfo());
+  rv = PrincipalToPrincipalInfo(aStoragePrincipal, storagePrincipalInfo);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(
+      storagePrincipalInfo->type() == PrincipalInfo::TContentPrincipalInfo ||
+      storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo);
+
+  if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) {
     return NS_ERROR_FAILURE;
   }
 
   nsCString suffix;
   nsCString origin;
 
-  if (principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo) {
+  if (storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo) {
     QuotaManager::GetInfoForChrome(&suffix, nullptr, &origin);
   } else {
     rv = QuotaManager::GetInfoFromPrincipal(aPrincipal, &suffix, nullptr,
                                             &origin);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
@@ -372,18 +403,20 @@ nsresult LSObject::CreateForPrincipal(ns
     Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo();
     if (clientInfo.isNothing()) {
       return NS_ERROR_FAILURE;
     }
 
     clientId = Some(clientInfo.ref().Id());
   }
 
-  RefPtr<LSObject> object = new LSObject(aWindow, aPrincipal);
+  RefPtr<LSObject> object =
+      new LSObject(aWindow, aPrincipal, aStoragePrincipal);
   object->mPrincipalInfo = std::move(principalInfo);
+  object->mStoragePrincipalInfo = std::move(storagePrincipalInfo);
   object->mPrivateBrowsingId = aPrivate ? 1 : 0;
   object->mClientId = clientId;
   object->mOrigin = origin;
   object->mOriginKey = originKey;
   object->mDocumentURI = aDocumentURI;
 
   object.forget(aObject);
   return NS_OK;
@@ -806,16 +839,17 @@ nsresult LSObject::EnsureDatabase() {
   PBackgroundChild* backgroundActor =
       BackgroundChild::GetOrCreateForCurrentThread();
   if (NS_WARN_IF(!backgroundActor)) {
     return NS_ERROR_FAILURE;
   }
 
   LSRequestCommonParams commonParams;
   commonParams.principalInfo() = *mPrincipalInfo;