Merge mozilla-central to autoland. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Thu, 21 Feb 2019 23:56:21 +0200
changeset 460447 af3314b6bcb1f9bb31700ad9a456661018e37686
parent 460446 d8b49bdcea45427d3a811c5d1f65104631653b3c (current diff)
parent 460252 e92ff56d2be21676b447c6fbb87b4c4479539bc9 (diff)
child 460448 fae2ad1c3dc6823fd9684d7f46968446adac66fa
push id112088
push userdluca@mozilla.com
push dateFri, 22 Feb 2019 07:28:34 +0000
treeherdermozilla-inbound@02aebe526755 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. a=merge CLOSED TREE
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -773,17 +773,17 @@ window._gBrowser = {
 
     // Specially handle about:blank as local
     if (aURI.pathQueryRef === "blank") {
       return true;
     }
 
     try {
       // Use the passed in resolvedURI if we have one
-      const resolvedURI = aResolvedURI || Services.io.newChannelFromURI2(
+      const resolvedURI = aResolvedURI || Services.io.newChannelFromURI(
         aURI,
         null, // loadingNode
         Services.scriptSecurityManager.getSystemPrincipal(), // loadingPrincipal
         null, // triggeringPrincipal
         Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, // securityFlags
         Ci.nsIContentPolicy.TYPE_OTHER // contentPolicyType
       ).URI;
       return resolvedURI.schemeIs("jar") || resolvedURI.schemeIs("file");
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -1586,17 +1586,17 @@ var SessionStoreInternal = {
   },
 
   /**
    * On quit application granted
    */
   onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown = false) {
     // Collect an initial snapshot of window data before we do the flush.
     let index = 0;
-    for (let window of this._browserWindows) {
+    for (let window of this._orderedBrowserWindows) {
       this._collectWindowData(window);
       this._windows[window.__SSi].zIndex = ++index;
     }
 
     // Now add an AsyncShutdown blocker that'll spin the event loop
     // until the windows have all been flushed.
 
     // This progress object will track the state of async window flushing
@@ -3411,17 +3411,17 @@ var SessionStoreInternal = {
     });
 
     var activeWindow = this._getTopWindow();
 
     TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
     if (RunState.isRunning) {
       // update the data for all windows with activities since the last save operation.
       let index = 0;
-      for (let window of this._browserWindows) {
+      for (let window of this._orderedBrowserWindows) {
         if (!this._isWindowLoaded(window)) // window data is still in _statesToRestore
           continue;
         if (aUpdateAll || DirtyWindows.has(window) || window == activeWindow) {
           this._collectWindowData(window);
         } else { // always update the window features (whose change alone never triggers a save operation)
           this._updateWindowFeatures(window);
         }
         this._windows[window.__SSi].zIndex = ++index;
@@ -4522,29 +4522,55 @@ var SessionStoreInternal = {
   _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
     // Attempt to load the session start time from the session state
     if (state.session && state.session.startTime) {
       this._sessionStartTime = state.session.startTime;
     }
   },
 
   /**
-   * Iterator that yields all currently opened browser windows, in order.
+   * Iterator that yields all currently opened browser windows.
    * (Might miss the most recent one.)
+   * This list is in focus order, but may include minimized windows
+   * before non-minimized windows.
    */
   _browserWindows: {
     * [Symbol.iterator]() {
       for (let window of BrowserWindowTracker.orderedWindows) {
         if (window.__SSi && !window.closed)
           yield window;
       }
     },
   },
 
   /**
+   * Iterator that yields all currently opened browser windows,
+   * with minimized windows last.
+   * (Might miss the most recent window.)
+   */
+  _orderedBrowserWindows: {
+    * [Symbol.iterator]() {
+      let windows = BrowserWindowTracker.orderedWindows;
+      windows.sort((a, b) => {
+        if (a.windowState == a.STATE_MINIMIZED && b.windowState != b.STATE_MINIMIZED) {
+          return 1;
+        }
+        if (a.windowState != a.STATE_MINIMIZED && b.windowState == b.STATE_MINIMIZED) {
+          return -1;
+        }
+        return 0;
+      });
+      for (let window of windows) {
+        if (window.__SSi && !window.closed)
+          yield window;
+      }
+    },
+  },
+
+  /**
    * Returns most recent window
    * @returns Window reference
    */
   _getTopWindow: function ssi_getTopWindow() {
     return BrowserWindowTracker.getTopWindow({ allowPopups: true });
   },
 
   /**
--- a/browser/modules/BrowserWindowTracker.jsm
+++ b/browser/modules/BrowserWindowTracker.jsm
@@ -14,17 +14,17 @@ const {Services} = ChromeUtils.import("r
 
 // Lazy getters
 XPCOMUtils.defineLazyModuleGetters(this, {
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
 });
 
 // Constants
 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
-const WINDOW_EVENTS = ["activate", "sizemodechange", "unload"];
+const WINDOW_EVENTS = ["activate", "unload"];
 const DEBUG = false;
 
 // Variables
 var _lastTopLevelWindowID = 0;
 var _trackedWindows = [];
 
 // Global methods
 function debug(s) {
@@ -59,19 +59,16 @@ function _handleEvent(event) {
       }
       break;
     case "TabSelect":
       _updateCurrentContentOuterWindowID(event.target.linkedBrowser);
       break;
     case "activate":
       WindowHelper.onActivate(event.target);
       break;
-    case "sizemodechange":
-      WindowHelper.onSizemodeChange(event.target);
-      break;
     case "unload":
       WindowHelper.removeWindow(event.currentTarget);
       break;
   }
 }
 
 function _handleMessage(message) {
   let browser = message.target;
@@ -139,24 +136,16 @@ var WindowHelper = {
     if (window == _trackedWindows[0])
       return;
 
     _untrackWindowOrder(window);
     _trackWindowOrder(window);
 
     _updateCurrentContentOuterWindowID(window.gBrowser.selectedBrowser);
   },
-
-  onSizemodeChange(window) {
-    if (window.windowState == window.STATE_MINIMIZED) {
-      // Make sure to have the minimized window behind unminimized windows.
-      _untrackWindowOrder(window);
-      _trackWindowOrder(window);
-    }
-  },
 };
 
 this.BrowserWindowTracker = {
   /**
    * Get the most recent browser window.
    *
    * @param options an object accepting the arguments for the search.
    *        * private: true to restrict the search to private windows
--- a/browser/modules/FaviconLoader.jsm
+++ b/browser/modules/FaviconLoader.jsm
@@ -85,17 +85,17 @@ function promiseImage(stream, type) {
     }, Services.tm.currentThread);
   });
 }
 
 class FaviconLoad {
   constructor(iconInfo) {
     this.icon = iconInfo;
 
-    this.channel = Services.io.newChannelFromURI2(
+    this.channel = Services.io.newChannelFromURI(
       iconInfo.iconUri,
       iconInfo.node,
       iconInfo.node.nodePrincipal,
       iconInfo.node.nodePrincipal,
       (Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
        Ci.nsILoadInfo.SEC_ALLOW_CHROME |
        Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT),
       Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON);
--- a/browser/modules/test/browser/browser_BrowserWindowTracker.js
+++ b/browser/modules/test/browser/browser_BrowserWindowTracker.js
@@ -105,21 +105,10 @@ add_task(async function test_orderedWind
     let ordered2 = BrowserWindowTracker.orderedWindows.filter(w => w != TEST_WINDOW);
     // After the shuffle, we expect window '1' to be the top-most window, because
     // it was the last one we called focus on. Then '6', the window we focused
     // before-last, followed by '4'. The order of the other windows remains
     // unchanged.
     let expected = [1, 6, 4, 9, 8, 7, 5, 3, 2, 0];
     Assert.deepEqual(expected, ordered2.map(w => windows.indexOf(w)),
       "After shuffle of focused windows, the order should've changed.");
-
-    // Minimizing a window should put it at the end of the ordered list of windows.
-    let promise = BrowserTestUtils.waitForEvent(windows[9], "sizemodechange");
-    windows[9].minimize();
-    await promise;
-
-    let ordered3 = BrowserWindowTracker.orderedWindows.filter(w => w != TEST_WINDOW);
-    // Test the end of the array of window indices, because Windows Debug builds
-    // mysteriously swap the order of the first two windows.
-    Assert.deepEqual([8, 7, 5, 3, 2, 0, 9], ordered3.map(w => windows.indexOf(w)).slice(3),
-      "When a window is minimized, the order should've changed.");
   });
 });
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -23,32 +23,24 @@ const Promise = require("Promise");
 
 loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
 loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
 loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
-loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
-loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
-loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
 loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
-loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 loader.lazyRequireGetter(this, "saveScreenshot", "devtools/shared/screenshot/save");
 
 loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 
 const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
-loader.lazyGetter(this, "TOOLBOX_L10N", function() {
-  return new LocalizationHelper("devtools/client/locales/toolbox.properties");
-});
 
 // Sidebar dimensions
 const INITIAL_SIDEBAR_SIZE = 350;
 
 // How long we wait to debounce resize events
 const LAZY_RESIZE_INTERVAL_MS = 200;
 
 // If the toolbox's width is smaller than the given amount of pixels, the sidebar
@@ -120,22 +112,19 @@ function Inspector(toolbox) {
 
   this.reflowTracker = new ReflowTracker(this._target);
   this.styleChangeTracker = new InspectorStyleChangeTracker(this);
 
   // Store the URL of the target page prior to navigation in order to ensure
   // telemetry counts in the Grid Inspector are not double counted on reload.
   this.previousURL = this.target.url;
 
-  this.nodeMenuTriggerInfo = null;
-
   this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
-  this._onContextMenu = this._onContextMenu.bind(this);
   this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
   this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
 
   this.onDetached = this.onDetached.bind(this);
   this.onHostChanged = this.onHostChanged.bind(this);
   this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
   this.onNewSelection = this.onNewSelection.bind(this);
   this.onNewRoot = this.onNewRoot.bind(this);
@@ -1397,18 +1386,16 @@ Inspector.prototype = {
 
     if (this._highlighters) {
       this._highlighters.destroy();
       this._highlighters = null;
     }
 
     if (this._markupFrame) {
       this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.contentWindow.removeEventListener("contextmenu",
-                                                          this._onContextMenu);
     }
 
     if (this._search) {
       this._search.destroy();
       this._search = null;
     }
 
     const sidebarDestroyer = this.sidebar.destroy();
@@ -1444,459 +1431,16 @@ Inspector.prototype = {
       markupDestroyer,
       sidebarDestroyer,
       ruleViewSideBarDestroyer,
     ]);
 
     return this._panelDestroyer;
   },
 
-  /**
-   * Returns the clipboard content if it is appropriate for pasting
-   * into the current node's outer HTML, otherwise returns null.
-   */
-  _getClipboardContentForPaste: function() {
-    const content = clipboardHelper.getText();
-    if (content && content.trim().length > 0) {
-      return content;
-    }
-    return null;
-  },
-
-  _onContextMenu: function(e) {
-    if (!(e.originalTarget instanceof Element) ||
-        e.originalTarget.closest("input[type=text]") ||
-        e.originalTarget.closest("input:not([type])") ||
-        e.originalTarget.closest("textarea")) {
-      return;
-    }
-
-    e.stopPropagation();
-    e.preventDefault();
-
-    this._openMenu({
-      screenX: e.screenX,
-      screenY: e.screenY,
-      target: e.target,
-    });
-  },
-
-  _openMenu: function({ target, screenX = 0, screenY = 0 } = { }) {
-    if (this.selection.isSlotted()) {
-      // Slotted elements should not show any context menu.
-      return null;
-    }
-
-    const markupContainer = this.markup.getContainer(this.selection.nodeFront);
-
-    this.contextMenuTarget = target;
-    this.nodeMenuTriggerInfo = markupContainer &&
-      markupContainer.editor.getInfoAtNode(target);
-
-    const isSelectionElement = this.selection.isElementNode() &&
-                             !this.selection.isPseudoElementNode();
-    const isEditableElement = isSelectionElement &&
-                            !this.selection.isAnonymousNode();
-    const isDuplicatableElement = isSelectionElement &&
-                                !this.selection.isAnonymousNode() &&
-                                !this.selection.isRoot();
-    const isScreenshotable = isSelectionElement &&
-                           this.selection.nodeFront.isTreeDisplayed;
-
-    const menu = new Menu();
-    menu.append(new MenuItem({
-      id: "node-menu-edithtml",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.editHTML(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-add",
-      label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
-      disabled: !this.canAddHTMLChild(),
-      click: () => this.addNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-duplicatenode",
-      label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
-      disabled: !isDuplicatableElement,
-      click: () => this.duplicateNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-delete",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
-      disabled: !this.isDeletable(this.selection.nodeFront),
-      click: () => this.deleteNode(),
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
-      submenu: this._getAttributesSubmenu(isEditableElement),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    // Set the pseudo classes
-    for (const name of ["hover", "active", "focus", "focus-within"]) {
-      const menuitem = new MenuItem({
-        id: "node-menu-pseudo-" + name,
-        label: name,
-        type: "checkbox",
-        click: this.togglePseudoClass.bind(this, ":" + name),
-      });
-
-      if (isSelectionElement) {
-        const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
-        menuitem.checked = checked;
-      } else {
-        menuitem.disabled = true;
-      }
-
-      menu.append(menuitem);
-    }
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
-      submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
-    }));
-
-    menu.append(new MenuItem({
-      label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
-      submenu: this._getPasteSubmenu(isEditableElement),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    const isNodeWithChildren = this.selection.isNode() &&
-                             markupContainer.hasChildren;
-    menu.append(new MenuItem({
-      id: "node-menu-expand",
-      label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
-      disabled: !isNodeWithChildren,
-      click: () => this.expandNode(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-collapse",
-      label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
-      disabled: !isNodeWithChildren || !markupContainer.expanded,
-      click: () => this.collapseAll(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-    }));
-
-    menu.append(new MenuItem({
-      id: "node-menu-scrollnodeintoview",
-      label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.scrollNodeIntoView(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-screenshotnode",
-      label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
-      disabled: !isScreenshotable,
-      click: () => this.screenshotNode().catch(console.error),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-useinconsole",
-      label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
-      click: () => this.useInConsole(),
-    }));
-    menu.append(new MenuItem({
-      id: "node-menu-showdomproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
-      click: () => this.showDOMProperties(),
-    }));
-
-    if (this.selection.nodeFront.customElementLocation) {
-      menu.append(new MenuItem({
-        type: "separator",
-      }));
-
-      menu.append(new MenuItem({
-        id: "node-menu-jumptodefinition",
-        label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
-        click: () => this.jumpToCustomElementDefinition(),
-      }));
-    }
-
-    this.buildA11YMenuItem(menu);
-
-    const nodeLinkMenuItems = this._getNodeLinkMenuItems();
-    if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
-      menu.append(new MenuItem({
-        id: "node-menu-link-separator",
-        type: "separator",
-      }));
-    }
-
-    for (const menuitem of nodeLinkMenuItems) {
-      menu.append(menuitem);
-    }
-
-    menu.popup(screenX, screenY, this._toolbox);
-    return menu;
-  },
-
-  buildA11YMenuItem: function(menu) {
-    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
-        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
-      return;
-    }
-
-    const showA11YPropsItem = new MenuItem({
-      id: "node-menu-showaccessibilityproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
-      click: () => this.showAccessibilityProperties(),
-      disabled: true,
-    });
-    // Only attempt to determine if a11y props menu item needs to be enabled if
-    // AccessibilityFront is enabled.
-    if (this.accessibilityFront.enabled) {
-      this._updateA11YMenuItem(showA11YPropsItem);
-    }
-
-    menu.append(showA11YPropsItem);
-  },
-
-  _updateA11YMenuItem: async function(menuItem) {
-    const hasMethod = await this.target.actorHasMethod("domwalker",
-                                                       "hasAccessibilityProperties");
-    if (!hasMethod) {
-      return;
-    }
-
-    const hasA11YProps = await this.walker.hasAccessibilityProperties(
-      this.selection.nodeFront);
-    if (hasA11YProps) {
-      this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
-    }
-
-    this.emit("node-menu-updated");
-  },
-
-  _getCopySubmenu: function(markupContainer, isSelectionElement) {
-    const copySubmenu = new Menu();
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyinner",
-      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyInnerHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyouter",
-      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyOuterHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyuniqueselector",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyUniqueSelector(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copycsspath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyCssPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyxpath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyXPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyimagedatauri",
-      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
-      disabled: !isSelectionElement || !markupContainer ||
-                !markupContainer.isPreviewable(),
-      click: () => this.copyImageDataUri(),
-    }));
-
-    return copySubmenu;
-  },
-
-  _getPasteSubmenu: function(isEditableElement) {
-    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
-    const disableAdjacentPaste = !isPasteable || this.selection.isRoot() ||
-          this.selection.isBodyNode() || this.selection.isHeadNode();
-    const disableFirstLastPaste = !isPasteable ||
-          (this.selection.isHTMLNode() && this.selection.isRoot());
-
-    const pasteSubmenu = new Menu();
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteinnerhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteInnerHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteouterhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteOuterHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastebefore",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("beforeBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteafter",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("afterEnd"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastefirstchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("afterBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastelastchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("beforeEnd"),
-    }));
-
-    return pasteSubmenu;
-  },
-
-  _getAttributesSubmenu: function(isEditableElement) {
-    const attributesSubmenu = new Menu();
-    const nodeInfo = this.nodeMenuTriggerInfo;
-    const isAttributeClicked = isEditableElement && nodeInfo &&
-                              nodeInfo.type === "attribute";
-
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-add-attribute",
-      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.onAddAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-copy-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
-                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onCopyAttributeValue(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-edit-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onEditAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-remove-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onRemoveAttribute(),
-    }));
-
-    return attributesSubmenu;
-  },
-
-  /**
-   * Link menu items can be shown or hidden depending on the context and
-   * selected node, and their labels can vary.
-   *
-   * @return {Array} list of visible menu items related to links.
-   */
-  _getNodeLinkMenuItems: function() {
-    const linkFollow = new MenuItem({
-      id: "node-menu-link-follow",
-      visible: false,
-      click: () => this.onFollowLink(),
-    });
-    const linkCopy = new MenuItem({
-      id: "node-menu-link-copy",
-      visible: false,
-      click: () => this.onCopyLink(),
-    });
-
-    // Get information about the right-clicked node.
-    const popupNode = this.contextMenuTarget;
-    if (!popupNode || !popupNode.classList.contains("link")) {
-      return [linkFollow, linkCopy];
-    }
-
-    const type = popupNode.dataset.type;
-    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
-      // Links can't be opened in new tabs in the browser toolbox.
-      if (type === "uri" && !this.target.chrome) {
-        linkFollow.visible = true;
-        linkFollow.label = INSPECTOR_L10N.getStr(
-          "inspector.menu.openUrlInNewTab.label");
-      } else if (type === "cssresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewCssSourceInStyleEditor.label");
-      } else if (type === "jsresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewJsSourceInDebugger.label");
-      }
-
-      linkCopy.visible = true;
-      linkCopy.label = INSPECTOR_L10N.getStr(
-        "inspector.menu.copyUrlToClipboard.label");
-    } else if (type === "idref") {
-      linkFollow.visible = true;
-      linkFollow.label = INSPECTOR_L10N.getFormatStr(
-        "inspector.menu.selectElement.label", popupNode.dataset.link);
-    }
-
-    return [linkFollow, linkCopy];
-  },
-
   _initMarkup: function() {
     if (!this._markupFrame) {
       this._markupFrame = this.panelDoc.createElement("iframe");
       this._markupFrame.setAttribute("aria-label",
         INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
       this._markupFrame.setAttribute("flex", "1");
       // This is needed to enable tooltips inside the iframe document.
       this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
@@ -1908,17 +1452,16 @@ Inspector.prototype = {
       this._markupFrame.setAttribute("src", "markup/markup.xhtml");
     } else {
       this._onMarkupFrameLoad();
     }
   },
 
   _onMarkupFrameLoad: function() {
     this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-    this._markupFrame.contentWindow.addEventListener("contextmenu", this._onContextMenu);
     this._markupFrame.contentWindow.focus();
     this._markupBox.style.visibility = "visible";
     this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
     this.emit("markuploaded");
   },
 
   _destroyMarkup: function() {
     let destroyPromise;
@@ -2024,251 +1567,16 @@ Inspector.prototype = {
 
       const hierarchical = pseudo == ":hover" || pseudo == ":active";
       return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
     }
     return promise.resolve();
   },
 
   /**
-   * Show DOM properties
-   */
-  showDOMProperties: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      jsterm.execute("inspect($0)");
-      jsterm.focus();
-    });
-  },
-
-  jumpToCustomElementDefinition: function() {
-    const node = this.selection.nodeFront;
-    const { url, line } = node.customElementLocation;
-    this._toolbox.viewSourceInDebugger(url, line, "show_custom_element");
-  },
-
-  /**
-   * Show Accessibility properties for currently selected node
-   */
-  async showAccessibilityProperties() {
-    const a11yPanel = await this._toolbox.selectTool("accessibility");
-    // Select the accessible object in the panel and wait for the event that
-    // tells us it has been done.
-    const onSelected = a11yPanel.once("new-accessible-front-selected");
-    a11yPanel.selectAccessibleForNode(this.selection.nodeFront,
-                                      "inspector-context-menu");
-    await onSelected;
-  },
-
-  /**
-   * Use in Console.
-   *
-   * Takes the currently selected node in the inspector and assigns it to a
-   * temp variable on the content window.  Also opens the split console and
-   * autofills it with the temp variable.
-   */
-  useInConsole: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      const evalString = `{ let i = 0;
-        while (window.hasOwnProperty("temp" + i) && i < 1000) {
-          i++;
-        }
-        window["temp" + i] = $0;
-        "temp" + i;
-      }`;
-
-      const options = {
-        selectedNodeActor: this.selection.nodeFront.actorID,
-      };
-      jsterm.requestEvaluation(evalString, options).then((res) => {
-        jsterm.setInputValue(res.result);
-        this.emit("console-var-ready");
-      });
-    });
-  },
-
-  /**
-   * Edit the outerHTML of the selected Node.
-   */
-  editHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    if (this.markup) {
-      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's outer HTML.
-   */
-  pasteOuterHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeOuterHTML(node).then(oldContent => {
-      this.markup.updateNodeOuterHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's inner HTML.
-   */
-  pasteInnerHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeInnerHTML(node).then(oldContent => {
-      this.markup.updateNodeInnerHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
-   * @param position
-   *        The position as specified for Element.insertAdjacentHTML
-   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
-   */
-  pasteAdjacentHTML: function(position) {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.insertAdjacentHTMLToNode(node, position, content);
-  },
-
-  /**
-   * Copy the innerHTML of the selected Node to the clipboard.
-   */
-  copyInnerHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
-  },
-
-  /**
-   * Copy the outerHTML of the selected Node to the clipboard.
-   */
-  copyOuterHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    const node = this.selection.nodeFront;
-
-    switch (node.nodeType) {
-      case nodeConstants.ELEMENT_NODE :
-        this._copyLongString(this.walker.outerHTML(node));
-        break;
-      case nodeConstants.COMMENT_NODE :
-        this._getLongString(node.getNodeValue()).then(comment => {
-          clipboardHelper.copyString("<!--" + comment + "-->");
-        });
-        break;
-      case nodeConstants.DOCUMENT_TYPE_NODE :
-        clipboardHelper.copyString(node.doctypeString);
-        break;
-    }
-  },
-
-  /**
-   * Copy the data-uri for the currently selected image in the clipboard.
-   */
-  copyImageDataUri: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    if (container && container.isPreviewable()) {
-      container.copyImageDataUri();
-    }
-  },
-
-  /**
-   * Copy the content of a longString (via a promise resolving a
-   * LongStringActor) to the clipboard
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving (with no argument) when the
-   *         string is sent to the clipboard
-   */
-  _copyLongString: function(longStringActorPromise) {
-    return this._getLongString(longStringActorPromise).then(string => {
-      clipboardHelper.copyString(string);
-    }).catch(console.error);
-  },
-
-  /**
-   * Retrieve the content of a longString (via a promise resolving a LongStringActor)
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving with the retrieved string as argument
-   */
-  _getLongString: function(longStringActorPromise) {
-    return longStringActorPromise.then(longStringActor => {
-      return longStringActor.string().then(string => {
-        longStringActor.release().catch(console.error);
-        return string;
-      });
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy a unique selector of the selected Node to the clipboard.
-   */
-  copyUniqueSelector: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
-    this.selection.nodeFront.getUniqueSelector().then(selector => {
-      clipboardHelper.copyString(selector);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the full CSS Path of the selected Node to the clipboard.
-   */
-  copyCssPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
-    this.selection.nodeFront.getCssPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the XPath of the selected Node to the clipboard.
-   */
-  copyXPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
-    this.selection.nodeFront.getXPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
    * Initiate screenshot command on selected node.
    */
   async screenshotNode() {
     // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
     // is still visible, therefore showing it in the picture.
     // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
     // will do that, calling `hide` for the highlighter only if previously shown.
     await this.highlighter.hideBoxModel();
@@ -2281,170 +1589,16 @@ Inspector.prototype = {
       clipboard: clipboardEnabled,
     };
     const screenshotFront = await this.target.getFront("screenshot");
     const screenshot = await screenshotFront.capture(args);
     await saveScreenshot(this.panelWin, args, screenshot);
   },
 
   /**
-   * Scroll the node into view.
-   */
-  scrollNodeIntoView: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.selection.nodeFront.scrollIntoView();
-  },
-
-  /**
-   * Duplicate the selected node
-   */
-  duplicateNode: function() {
-    const selection = this.selection;
-    if (!selection.isElementNode() ||
-        selection.isRoot() ||
-        selection.isAnonymousNode() ||
-        selection.isPseudoElementNode()) {
-      return;
-    }
-    this.walker.duplicateNode(selection.nodeFront).catch(console.error);
-  },
-
-  /**
-   * Delete the selected node.
-   */
-  deleteNode: function() {
-    if (!this.selection.isNode() ||
-         this.selection.isRoot()) {
-      return;
-    }
-
-    // If the markup panel is active, use the markup panel to delete
-    // the node, making this an undoable action.
-    if (this.markup) {
-      this.markup.deleteNode(this.selection.nodeFront);
-    } else {
-      // remove the node from content
-      this.walker.removeNode(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Add attribute to node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onAddAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.addAttribute();
-  },
-
-  /**
-   * Copy attribute value for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onCopyAttributeValue: function() {
-    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
-  },
-
-  /**
-   * Edit attribute for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onEditAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.editAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  /**
-   * Remove attribute from node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onRemoveAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.removeAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  expandNode: function() {
-    this.markup.expandAll(this.selection.nodeFront);
-  },
-
-  collapseAll: function() {
-    this.markup.collapseAll(this.selection.nodeFront);
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-follow menu item
-   * in the inspector contextual-menu.
-   */
-  onFollowLink: function() {
-    const type = this.contextMenuTarget.dataset.type;
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.followAttributeLink(type, link);
-  },
-
-  /**
-   * Given a type and link found in a node's attribute in the markup-view,
-   * attempt to follow that link (which may result in opening a new tab, the
-   * style editor or debugger).
-   */
-  followAttributeLink: function(type, link) {
-    if (!type || !link) {
-      return;
-    }
-
-    if (type === "uri" || type === "cssresource" || type === "jsresource") {
-      // Open link in a new tab.
-      this.inspector.resolveRelativeURL(
-        link, this.selection.nodeFront).then(url => {
-          if (type === "uri") {
-            openContentLink(url);
-          } else if (type === "cssresource") {
-            return this.toolbox.viewSourceInStyleEditor(url);
-          } else if (type === "jsresource") {
-            return this.toolbox.viewSourceInDebugger(url);
-          }
-          return null;
-        }).catch(console.error);
-    } else if (type == "idref") {
-      // Select the node in the same document.
-      this.walker.document(this.selection.nodeFront).then(doc => {
-        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
-          if (!node) {
-            this.emit("idref-attribute-link-failed");
-            return;
-          }
-          this.selection.setNodeFront(node);
-        });
-      }).catch(console.error);
-    }
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-copy menu item
-   * in the inspector contextual-menu.
-   */
-  onCopyLink: function() {
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.copyAttributeLink(link);
-  },
-
-  /**
-   * This method is here for the benefit of copying links.
-   */
-  copyAttributeLink: function(link) {
-    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
-      clipboardHelper.copyString(url);
-    }, console.error);
-  },
-
-  /**
    * Returns an object containing the shared handler functions used in the box
    * model and grid React components.
    */
   getCommonComponentProps() {
     return {
       setSelectedNode: this.selection.setNodeFront,
       onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
     };
@@ -2459,28 +1613,16 @@ Inspector.prototype = {
    * @param  {Object} options
    *         Options passed to the highlighter actor.
    */
   onShowBoxModelHighlighterForNode(nodeFront, options) {
     const toolbox = this.toolbox;
     toolbox.highlighter.highlight(nodeFront, options);
   },
 
-  /**
-   * Returns a value indicating whether a node can be deleted.
-   *
-   * @param {NodeFront} nodeFront
-   *        The node to test for deletion
-   */
-  isDeletable(nodeFront) {
-    return !(nodeFront.isDocumentElement ||
-           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
-           nodeFront.isAnonymous);
-  },
-
   async inspectNodeActor(nodeActor, inspectFromAnnotation) {
     const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
     if (!nodeFront) {
       console.error("The object cannot be linked to the inspector, the " +
                     "corresponding nodeFront could not be found.");
       return false;
     }
 
copy from devtools/client/inspector/inspector.js
copy to devtools/client/inspector/markup/markup-context-menu.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/markup/markup-context-menu.js
@@ -1,1490 +1,594 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 window, BrowserLoader */
-
 "use strict";
 
 const Services = require("Services");
 const promise = require("promise");
-const EventEmitter = require("devtools/shared/event-emitter");
-const {executeSoon} = require("devtools/shared/DevToolsUtils");
-const {Toolbox} = require("devtools/client/framework/toolbox");
-const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
-const Store = require("devtools/client/inspector/store");
-const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
+const { LocalizationHelper } = require("devtools/shared/l10n");
 
-// Use privileged promise in panel documents to prevent having them to freeze
-// during toolbox destruction. See bug 1402779.
-const Promise = require("Promise");
-
-loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true);
-loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
-loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
-loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
-loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
-loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
-loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
-loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
 loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
-loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
+loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
-loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
-loader.lazyRequireGetter(this, "saveScreenshot", "devtools/shared/screenshot/save");
 
-loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
-
-const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
-const INSPECTOR_L10N =
-  new LocalizationHelper("devtools/client/locales/inspector.properties");
 loader.lazyGetter(this, "TOOLBOX_L10N", function() {
   return new LocalizationHelper("devtools/client/locales/toolbox.properties");
 });
 
-// Sidebar dimensions
-const INITIAL_SIDEBAR_SIZE = 350;
-
-// How long we wait to debounce resize events
-const LAZY_RESIZE_INTERVAL_MS = 200;
-
-// If the toolbox's width is smaller than the given amount of pixels, the sidebar
-// automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
-const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
-// If the toolbox's width docked to the side is smaller than the given amount of pixels,
-// the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
-// mode.
-const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
-
-const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
-const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
-const THREE_PANE_CHROME_ENABLED_PREF = "devtools.inspector.chrome.three-pane-enabled";
-const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
-const TRACK_CHANGES_PREF = "devtools.inspector.changes.enabled";
+const INSPECTOR_L10N =
+  new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 /**
- * Represents an open instance of the Inspector for a tab.
- * The inspector controls the breadcrumbs, the markup view, and the sidebar
- * (computed view, rule view, font view and animation inspector).
- *
- * Events:
- * - ready
- *      Fired when the inspector panel is opened for the first time and ready to
- *      use
- * - new-root
- *      Fired after a new root (navigation to a new page) event was fired by
- *      the walker, and taken into account by the inspector (after the markup
- *      view has been reloaded)
- * - markuploaded
- *      Fired when the markup-view frame has loaded
- * - breadcrumbs-updated
- *      Fired when the breadcrumb widget updates to a new node
- * - boxmodel-view-updated
- *      Fired when the box model updates to a new node
- * - markupmutation
- *      Fired after markup mutations have been processed by the markup-view
- * - computed-view-refreshed
- *      Fired when the computed rules view updates to a new node
- * - computed-view-property-expanded
- *      Fired when a property is expanded in the computed rules view
- * - computed-view-property-collapsed
- *      Fired when a property is collapsed in the computed rules view
- * - computed-view-sourcelinks-updated
- *      Fired when the stylesheet source links have been updated (when switching
- *      to source-mapped files)
- * - rule-view-refreshed
- *      Fired when the rule view updates to a new node
- * - rule-view-sourcelinks-updated
- *      Fired when the stylesheet source links have been updated (when switching
- *      to source-mapped files)
+ * Context menu for the Markup view.
  */
-function Inspector(toolbox) {
-  EventEmitter.decorate(this);
-
-  this._toolbox = toolbox;
-  this._target = toolbox.target;
-  this.panelDoc = window.document;
-  this.panelWin = window;
-  this.panelWin.inspector = this;
-  this.telemetry = toolbox.telemetry;
-  this.store = Store();
-
-  this._markupBox = this.panelDoc.getElementById("markup-box");
-
-  // Map [panel id => panel instance]
-  // Stores all the instances of sidebar panels like rule view, computed view, ...
-  this._panels = new Map();
-
-  this.reflowTracker = new ReflowTracker(this._target);
-  this.styleChangeTracker = new InspectorStyleChangeTracker(this);
-
-  // Store the URL of the target page prior to navigation in order to ensure
-  // telemetry counts in the Grid Inspector are not double counted on reload.
-  this.previousURL = this.target.url;
-
-  this.nodeMenuTriggerInfo = null;
-
-  this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
-  this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
-  this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
-  this._onContextMenu = this._onContextMenu.bind(this);
-  this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
-  this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
-
-  this.onDetached = this.onDetached.bind(this);
-  this.onHostChanged = this.onHostChanged.bind(this);
-  this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
-  this.onNewSelection = this.onNewSelection.bind(this);
-  this.onNewRoot = this.onNewRoot.bind(this);
-  this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
-  this.onShowBoxModelHighlighterForNode =
-    this.onShowBoxModelHighlighterForNode.bind(this);
-  this.onSidebarHidden = this.onSidebarHidden.bind(this);
-  this.onSidebarResized = this.onSidebarResized.bind(this);
-  this.onSidebarSelect = this.onSidebarSelect.bind(this);
-  this.onSidebarShown = this.onSidebarShown.bind(this);
-  this.onSidebarToggle = this.onSidebarToggle.bind(this);
-
-  this._target.on("will-navigate", this._onBeforeNavigate);
-}
-
-Inspector.prototype = {
-  /**
-   * open is effectively an asynchronous constructor
-   */
-  async init() {
-    // Localize all the nodes containing a data-localization attribute.
-    localizeMarkup(this.panelDoc);
-
-    await Promise.all([
-      this._getCssProperties(),
-      this._getPageStyle(),
-      this._getDefaultSelection(),
-      this._getAccessibilityFront(),
-    ]);
-
-    return this._deferredOpen();
-  },
-
-  get toolbox() {
-    return this._toolbox;
-  },
-
-  get inspector() {
-    return this.toolbox.inspector;
-  },
+class MarkupContextMenu {
+  constructor(markup) {
+    this.markup = markup;
+    this.inspector = markup.inspector;
+    this.selection = this.inspector.selection;
+    this.target = this.inspector.target;
+    this.telemetry = this.inspector.telemetry;
+    this.toolbox = this.inspector.toolbox;
+    this.walker = this.inspector.walker;
+  }
 
-  get walker() {
-    return this.toolbox.walker;
-  },
-
-  get selection() {
-    return this.toolbox.selection;
-  },
-
-  get highlighter() {
-    return this.toolbox.highlighter;
-  },
-
-  get highlighters() {
-    if (!this._highlighters) {
-      this._highlighters = new HighlightersOverlay(this);
-    }
-
-    return this._highlighters;
-  },
-
-  get isHighlighterReady() {
-    return !!this._highlighters;
-  },
-
-  get is3PaneModeEnabled() {
-    if (this.target.chrome) {
-      if (!this._is3PaneModeChromeEnabled) {
-        this._is3PaneModeChromeEnabled = Services.prefs.getBoolPref(
-          THREE_PANE_CHROME_ENABLED_PREF);
-      }
-
-      return this._is3PaneModeChromeEnabled;
-    }
-
-    if (!this._is3PaneModeEnabled) {
-      this._is3PaneModeEnabled = Services.prefs.getBoolPref(THREE_PANE_ENABLED_PREF);
-    }
-
-    return this._is3PaneModeEnabled;
-  },
-
-  set is3PaneModeEnabled(value) {
-    if (this.target.chrome) {
-      this._is3PaneModeChromeEnabled = value;
-      Services.prefs.setBoolPref(THREE_PANE_CHROME_ENABLED_PREF,
-        this._is3PaneModeChromeEnabled);
-    } else {
-      this._is3PaneModeEnabled = value;
-      Services.prefs.setBoolPref(THREE_PANE_ENABLED_PREF, this._is3PaneModeEnabled);
-    }
-  },
-
-  get notificationBox() {
-    if (!this._notificationBox) {
-      this._notificationBox = this.toolbox.getNotificationBox();
-    }
-
-    return this._notificationBox;
-  },
-
-  get search() {
-    if (!this._search) {
-      this._search = new InspectorSearch(this, this.searchBox, this.searchClearButton);
-    }
-
-    return this._search;
-  },
-
-  get cssProperties() {
-    return this._cssProperties.cssProperties;
-  },
-
-  /**
-   * Check if the changes panel is enabled and supported by the server.
-   */
-  _supportsChangesPanel() {
-    // The changes actor was introduced in Fx65, we are checking this for backward
-    // compatibility when connecting to an older server. Can be removed once Fx65 hit the
-    // release channel.
-    return this._target.hasActor("changes") &&
-      Services.prefs.getBoolPref(TRACK_CHANGES_PREF);
-  },
-
-  /**
-   * Handle promise rejections for various asynchronous actions, and only log errors if
-   * the inspector panel still exists.
-   * This is useful to silence useless errors that happen when the inspector is closed
-   * while still initializing (and making protocol requests).
-   */
-  _handleRejectionIfNotDestroyed: function(e) {
-    if (!this._panelDestroyer) {
-      console.error(e);
-    }
-  },
-
-  _deferredOpen: async function() {
-    this._initMarkup();
-    this.isReady = false;
-
-    // Set the node front so that the markup and sidebar panels will have the selected
-    // nodeFront ready when they're initialized.
-    if (this._defaultNode) {
-      this.selection.setNodeFront(this._defaultNode, { reason: "inspector-open" });
-    }
-
-    if (this._supportsChangesPanel()) {
-      // Get the Changes front, then call a method on it, which will instantiate
-      // the ChangesActor. We want the ChangesActor to be guaranteed available before
-      // the user makes any changes.
-      this.changesFront = await this.toolbox.target.getFront("changes");
-      this.changesFront.start();
+  show(event) {
+    if (!(event.originalTarget instanceof Element) ||
+        event.originalTarget.closest("input[type=text]") ||
+        event.originalTarget.closest("input:not([type])") ||
+        event.originalTarget.closest("textarea")) {
+      return;
     }
 
-    // Setup the splitter before the sidebar is displayed so, we don't miss any events.
-    this.setupSplitter();
-
-    // We can display right panel with: tab bar, markup view and breadbrumb. Right after
-    // the splitter set the right and left panel sizes, in order to avoid resizing it
-    // during load of the inspector.
-    this.panelDoc.getElementById("inspector-main-content").style.visibility = "visible";
-
-    // Setup the sidebar panels.
-    this.setupSidebar();
-
-    await this.once("markuploaded");
-    this.isReady = true;
-
-    // All the components are initialized. Take care of the remaining initialization
-    // and setup.
-    this.breadcrumbs = new HTMLBreadcrumbs(this);
-    this.setupExtensionSidebars();
-    this.setupSearchBox();
-    await this.setupToolbar();
-
-    this.onNewSelection();
-
-    this.walker.on("new-root", this.onNewRoot);
-    this.toolbox.on("host-changed", this.onHostChanged);
-    this.selection.on("new-node-front", this.onNewSelection);
-    this.selection.on("detached-front", this.onDetached);
-
-    // Log the 3 pane inspector setting on inspector open. The question we want to answer
-    // is:
-    // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
-    this.telemetry.keyedScalarAdd(THREE_PANE_ENABLED_SCALAR, this.is3PaneModeEnabled, 1);
+    event.stopPropagation();
+    event.preventDefault();
 
-    this.emit("ready");
-    return this;
-  },
-
-  _onBeforeNavigate: function() {
-    this._defaultNode = null;
-    this.selection.setNodeFront(null);
-    this._destroyMarkup();
-    this._pendingSelection = null;
-  },
-
-  _getCssProperties: function() {
-    return initCssProperties(this.toolbox).then(cssProperties => {
-      this._cssProperties = cssProperties;
-    }, this._handleRejectionIfNotDestroyed);
-  },
-
-  _getAccessibilityFront: async function() {
-    this.accessibilityFront = await this.target.getFront("accessibility");
-    return this.accessibilityFront;
-  },
-
-  _getDefaultSelection: function() {
-    // This may throw if the document is still loading and we are
-    // refering to a dead about:blank document
-    return this._getDefaultNodeForSelection().catch(this._handleRejectionIfNotDestroyed);
-  },
-
-  _getPageStyle: function() {
-    return this.inspector.getPageStyle().then(pageStyle => {
-      this.pageStyle = pageStyle;
-    }, this._handleRejectionIfNotDestroyed);
-  },
+    this._openMenu({
+      screenX: event.screenX,
+      screenY: event.screenY,
+      target: event.target,
+    });
+  }
 
   /**
-   * Return a promise that will resolve to the default node for selection.
+   * This method is here for the benefit of copying links.
    */
-  _getDefaultNodeForSelection: function() {
-    if (this._defaultNode) {
-      return this._defaultNode;
-    }
-    const walker = this.walker;
-    let rootNode = null;
-    const pendingSelection = this._pendingSelection;
-
-    // A helper to tell if the target has or is about to navigate.
-    // this._pendingSelection changes on "will-navigate" and "new-root" events.
-    const hasNavigated = () => pendingSelection !== this._pendingSelection;
-
-    // If available, set either the previously selected node or the body
-    // as default selected, else set documentElement
-    return walker.getRootNode().then(node => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      rootNode = node;
-      if (this.selectionCssSelector) {
-        return walker.querySelector(rootNode, this.selectionCssSelector);
-      }
-      return null;
-    }).then(front => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      if (front) {
-        return front;
-      }
-      return walker.querySelector(rootNode, "body");
-    }).then(front => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-
-      if (front) {
-        return front;
-      }
-      return this.walker.documentElement();
-    }).then(node => {
-      if (hasNavigated()) {
-        return promise.reject("navigated; resolution of _defaultNode aborted");
-      }
-      this._defaultNode = node;
-      return node;
-    });
-  },
-
-  /**
-   * Target getter.
-   */
-  get target() {
-    return this._target;
-  },
-
-  /**
-   * Target setter.
-   */
-  set target(value) {
-    this._target = value;
-  },
+  _copyAttributeLink(link) {
+    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
+      clipboardHelper.copyString(url);
+    }, console.error);
+  }
 
   /**
-   * Hooks the searchbar to show result and auto completion suggestions.
+   * Copy the full CSS Path of the selected Node to the clipboard.
    */
-  setupSearchBox: function() {
-    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear");
-    this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
-
-    this.searchBox.addEventListener("focus", () => {
-      this.search.on("search-cleared", this._clearSearchResultsLabel);
-      this.search.on("search-result", this._updateSearchResultsLabel);
-    }, { once: true });
-
-    const shortcuts = new KeyShortcuts({
-      window: this.panelDoc.defaultView,
-    });
-    const key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
-    shortcuts.on(key, event => {
-      // Prevent overriding same shortcut from the computed/rule views
-      if (event.target.closest("#sidebar-panel-ruleview") ||
-          event.target.closest("#sidebar-panel-computedview")) {
-        return;
-      }
-      event.preventDefault();
-      this.searchBox.focus();
-    });
-  },
-
-  get searchSuggestions() {
-    return this.search.autocompleter;
-  },
-
-  _clearSearchResultsLabel: function(result) {
-    return this._updateSearchResultsLabel(result, true);
-  },
-
-  _updateSearchResultsLabel: function(result, clear = false) {
-    let str = "";
-    if (!clear) {
-      if (result) {
-        str = INSPECTOR_L10N.getFormatStr(
-          "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength);
-      } else {
-        str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
-      }
+  _copyCssPath() {
+    if (!this.selection.isNode()) {
+      return;
     }
 
-    this.searchResultsLabel.textContent = str;
-  },
-
-  get React() {
-    return this._toolbox.React;
-  },
-
-  get ReactDOM() {
-    return this._toolbox.ReactDOM;
-  },
-
-  get ReactRedux() {
-    return this._toolbox.ReactRedux;
-  },
-
-  get browserRequire() {
-    return this._toolbox.browserRequire;
-  },
-
-  get InspectorTabPanel() {
-    if (!this._InspectorTabPanel) {
-      this._InspectorTabPanel =
-        this.React.createFactory(this.browserRequire(
-        "devtools/client/inspector/components/InspectorTabPanel"));
-    }
-    return this._InspectorTabPanel;
-  },
-
-  get InspectorSplitBox() {
-    if (!this._InspectorSplitBox) {
-      this._InspectorSplitBox = this.React.createFactory(this.browserRequire(
-        "devtools/client/shared/components/splitter/SplitBox"));
-    }
-    return this._InspectorSplitBox;
-  },
-
-  get TabBar() {
-    if (!this._TabBar) {
-      this._TabBar = this.React.createFactory(this.browserRequire(
-        "devtools/client/shared/components/tabs/TabBar"));
-    }
-    return this._TabBar;
-  },
+    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
+    this.selection.nodeFront.getCssPath().then(path => {
+      clipboardHelper.copyString(path);
+    }).catch(console.error);
+  }
 
   /**
-   * Check if the inspector should use the landscape mode.
-   *
-   * @return {Boolean} true if the inspector should be in landscape mode.
+   * Copy the data-uri for the currently selected image in the clipboard.
    */
-  useLandscapeMode: function() {
-    if (!this.panelDoc) {
-      return true;
+  _copyImageDataUri() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    if (container && container.isPreviewable()) {
+      container.copyImageDataUri();
     }
-
-    const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
-    const { width } = window.windowUtils.getBoundsWithoutFlushing(splitterBox);
-
-    return this.is3PaneModeEnabled &&
-           (this.toolbox.hostType == Toolbox.HostType.LEFT ||
-            this.toolbox.hostType == Toolbox.HostType.RIGHT) ?
-      width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD :
-      width > PORTRAIT_MODE_WIDTH_THRESHOLD;
-  },
+  }
 
   /**
-   * Build Splitter located between the main and side area of
-   * the Inspector panel.
+   * Copy the innerHTML of the selected Node to the clipboard.
    */
-  setupSplitter: function() {
-    const { width, height, splitSidebarWidth } = this.getSidebarSize();
-
-    const splitter = this.InspectorSplitBox({
-      className: "inspector-sidebar-splitter",
-      initialWidth: width,
-      initialHeight: height,
-      minSize: "10%",
-      maxSize: "80%",
-      splitterSize: 1,
-      endPanelControl: true,
-      startPanel: this.InspectorTabPanel({
-        id: "inspector-main-content",
-      }),
-      endPanel: this.InspectorSplitBox({
-        initialWidth: splitSidebarWidth,
-        minSize: 10,
-        maxSize: "80%",
-        splitterSize: this.is3PaneModeEnabled ? 1 : 0,
-        endPanelControl: this.is3PaneModeEnabled,
-        startPanel: this.InspectorTabPanel({
-          id: "inspector-rules-container",
-        }),
-        endPanel: this.InspectorTabPanel({
-          id: "inspector-sidebar-container",
-        }),
-        ref: splitbox => {
-          this.sidebarSplitBox = splitbox;
-        },
-      }),
-      vert: this.useLandscapeMode(),
-      onControlledPanelResized: this.onSidebarResized,
-    });
-
-    this.splitBox = this.ReactDOM.render(splitter,
-      this.panelDoc.getElementById("inspector-splitter-box"));
-
-    this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
-  },
-
-  _onLazyPanelResize: async function() {
-    // We can be called on a closed window because of the deferred task.
-    if (window.closed) {
+  _copyInnerHTML() {
+    if (!this.selection.isNode()) {
       return;
     }
 
-    this.splitBox.setState({ vert: this.useLandscapeMode() });
-    this.emit("inspector-resize");
-  },
+    copyLongString(this.walker.innerHTML(this.selection.nodeFront));
+  }
+
+  /**
+   * Copy the outerHTML of the selected Node to the clipboard.
+   */
+  _copyOuterHTML() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.markup.copyOuterHTML();
+  }
+
+  /**
+   * Copy a unique selector of the selected Node to the clipboard.
+   */
+  _copyUniqueSelector() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
+    this.selection.nodeFront.getUniqueSelector().then(selector => {
+      clipboardHelper.copyString(selector);
+    }).catch(console.error);
+  }
 
   /**
-   * If Toolbox width is less than 600 px, the splitter changes its mode
-   * to `horizontal` to support portrait view.
+   * Copy the XPath of the selected Node to the clipboard.
+   */
+  _copyXPath() {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
+    this.selection.nodeFront.getXPath().then(path => {
+      clipboardHelper.copyString(path);
+    }).catch(console.error);
+  }
+
+  /**
+   * Delete the selected node.
    */
-  onPanelWindowResize: function() {
-    if (this.toolbox.currentToolId !== "inspector") {
+  _deleteNode() {
+    if (!this.selection.isNode() ||
+         this.selection.isRoot()) {
+      return;
+    }
+
+    // If the markup panel is active, use the markup panel to delete
+    // the node, making this an undoable action.
+    if (this.markup) {
+      this.markup.deleteNode(this.selection.nodeFront);
+    } else {
+      // remove the node from content
+      this.walker.removeNode(this.selection.nodeFront);
+    }
+  }
+
+  /**
+   * Duplicate the selected node
+   */
+  _duplicateNode() {
+    if (!this.selection.isElementNode() ||
+        this.selection.isRoot() ||
+        this.selection.isAnonymousNode() ||
+        this.selection.isPseudoElementNode()) {
+      return;
+    }
+
+    this.walker.duplicateNode(this.selection.nodeFront).catch(console.error);
+  }
+
+  /**
+   * Edit the outerHTML of the selected Node.
+   */
+  _editHTML() {
+    if (!this.selection.isNode()) {
       return;
     }
 
-    if (!this._lazyResizeHandler) {
-      this._lazyResizeHandler = new DeferredTask(this._onLazyPanelResize.bind(this),
-                                                 LAZY_RESIZE_INTERVAL_MS, 0);
-    }
-    this._lazyResizeHandler.arm();
-  },
-
-  getSidebarSize: function() {
-    let width;
-    let height;
-    let splitSidebarWidth;
+    this.markup.beginEditingOuterHTML(this.selection.nodeFront);
+  }
 
-    // Initialize splitter size from preferences.
-    try {
-      width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
-      height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
-      splitSidebarWidth = Services.prefs.getIntPref(
-        "devtools.toolsidebar-width.inspector.splitsidebar");
-    } catch (e) {
-      // Set width and height of the splitter. Only one
-      // value is really useful at a time depending on the current
-      // orientation (vertical/horizontal).
-      // Having both is supported by the splitter component.
-      width = this.is3PaneModeEnabled ?
-        INITIAL_SIDEBAR_SIZE * 2 : INITIAL_SIDEBAR_SIZE;
-      height = INITIAL_SIDEBAR_SIZE;
-      splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
-    }
-
-    return { width, height, splitSidebarWidth };
-  },
+  /**
+   * Jumps to the custom element definition in the debugger.
+   */
+  _jumpToCustomElementDefinition() {
+    const { url, line } = this.selection.nodeFront.customElementLocation;
+    this.toolbox.viewSourceInDebugger(url, line, "show_custom_element");
+  }
 
-  onSidebarHidden: function() {
-    // Store the current splitter size to preferences.
-    const state = this.splitBox.state;
-    Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width);
-    Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
-    Services.prefs.setIntPref("devtools.toolsidebar-width.inspector.splitsidebar",
-      this.sidebarSplitBox.state.width);
-  },
-
-  onSidebarResized: function(width, height) {
-    this.toolbox.emit("inspector-sidebar-resized", { width, height });
-  },
-
-  onSidebarSelect: function(toolId) {
-    // Save the currently selected sidebar panel
-    Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
+  /**
+   * Add attribute to node.
+   * Used for node context menu and shouldn't be called directly.
+   */
+  _onAddAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.addAttribute();
+  }
 
-    // Then forces the panel creation by calling getPanel
-    // (This allows lazy loading the panels only once we select them)
-    this.getPanel(toolId);
-
-    this.toolbox.emit("inspector-sidebar-select", toolId);
-  },
+  /**
+   * Copy attribute value for node.
+   * Used for node context menu and shouldn't be called directly.
+   */
+  _onCopyAttributeValue() {
+    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
+  }
 
-  onSidebarShown: function() {
-    const { width, height, splitSidebarWidth } = this.getSidebarSize();
-    this.splitBox.setState({ width, height });
-    this.sidebarSplitBox.setState({ width: splitSidebarWidth });
-  },
-
-  async onSidebarToggle() {
-    this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
-    await this.setupToolbar();
-    await this.addRuleView({ skipQueue: true });
-  },
+  /**
+   * This method is here for the benefit of the node-menu-link-copy menu item
+   * in the inspector contextual-menu.
+   */
+  _onCopyLink() {
+    this.copyAttributeLink(this.contextMenuTarget.dataset.link);
+  }
 
   /**
-   * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
-   * split box, specifies the end panel control and resizes the split box width depending
-   * on the width of the toolbox.
+   * Edit attribute for node.
+   * Used for node context menu and shouldn't be called directly.
    */
-  setSidebarSplitBoxState() {
-    const toolboxWidth =
-      this.panelDoc.getElementById("inspector-splitter-box").clientWidth;
-
-    // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
-    // vertical mode) width.
-    const sidebarWidth = this.splitBox.state.width;
-    // This variable represents the width of the right panel in horizontal mode or
-    // bottom-right panel in vertical mode width in 3 pane mode.
-    let sidebarSplitboxWidth;
-
-    if (this.useLandscapeMode()) {
-      // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
-      // or bottom panel in vertical mode) width will be bigger than half of the
-      // toolbox's width.
-      const canDoubleSidebarWidth = (sidebarWidth * 2) < (toolboxWidth / 2);
+  _onEditAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.editAttribute(this.nodeMenuTriggerInfo.name);
+  }
 
-      // Resize the main split box's end panel that contains the middle and right panel.
-      // Attempts to resize the main split box's end panel to be double the size of the
-      // existing sidebar's width when switching to 3 pane mode. However, if the middle
-      // and right panel's width together is greater than half of the toolbox's width,
-      // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
-      // the current toolbox's width.
-      this.splitBox.setState({
-        width: canDoubleSidebarWidth ? sidebarWidth * 2 : toolboxWidth * 2 / 3,
-      });
-
-      // In landscape/horizontal mode, set the right panel back to its original
-      // inspector sidebar width if we can double the sidebar width. Otherwise, set
-      // the width of the right panel to be 1/3 of the toolbox's width since all 3
-      // panels will be equally sized.
-      sidebarSplitboxWidth = canDoubleSidebarWidth ? sidebarWidth : toolboxWidth / 3;
-    } else {
-      // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
-      // toolbox's width.
-      sidebarSplitboxWidth = toolboxWidth / 2;
-    }
-
-    // Show the splitter inside the sidebar split box. Sets the width of the inspector
-    // sidebar and specify that the end (right in horizontal or bottom-right in
-    // vertical) panel of the sidebar split box should be controlled when resizing.
-    this.sidebarSplitBox.setState({
-      endPanelControl: true,
-      splitterSize: 1,
-      width: sidebarSplitboxWidth,
-    });
-  },
+  /**
+   * This method is here for the benefit of the node-menu-link-follow menu item
+   * in the inspector contextual-menu.
+   */
+  _onFollowLink() {
+    const type = this.contextMenuTarget.dataset.type;
+    const link = this.contextMenuTarget.dataset.link;
+    this.markup.followAttributeLink(type, link);
+  }
 
   /**
-   * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
-   * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
-   * pane mode. The default tab specifies whether or not the rule view should be selected.
-   * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
-   * rule view is being merged back into the inspector sidebar from middle/bottom-left
-   * panel. Otherwise, we specify the default tab when handling the sidebar setup.
-   *
-   * @params {String} defaultTab
-   *         Thie id of the default tab for the sidebar.
+   * Remove attribute from node.
+   * Used for node context menu and shouldn't be called directly.
    */
-  async addRuleView({ defaultTab = "ruleview", skipQueue = false } = {}) {
-    const ruleViewSidebar = this.sidebarSplitBox.startPanelContainer;
-
-    if (this.is3PaneModeEnabled) {
-      // Convert to 3 pane mode by removing the rule view from the inspector sidebar
-      // and adding the rule view to the middle (in landscape/horizontal mode) or
-      // bottom-left (in portrait/vertical mode) panel.
-      ruleViewSidebar.style.display = "block";
-
-      this.setSidebarSplitBoxState();
-
-      // Force the rule view panel creation by calling getPanel
-      this.getPanel("ruleview");
-
-      await this.sidebar.removeTab("ruleview");
-
-      this.ruleViewSideBar.addExistingTab(
-        "ruleview",
-        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-        true);
-
-      this.ruleViewSideBar.show();
-    } else {
-      // Removes the rule view from the 3 pane mode and adds the rule view to the main
-      // inspector sidebar.
-      ruleViewSidebar.style.display = "none";
-
-      // Set the width of the split box (right panel in horziontal mode and bottom panel
-      // in vertical mode) to be the width of the inspector sidebar.
-      const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
-      this.splitBox.setState({
-        width: this.useLandscapeMode() ?
-          this.sidebarSplitBox.state.width : splitterBox.clientWidth,
-      });
-
-      // Hide the splitter to prevent any drag events in the sidebar split box and
-      // specify that the end (right panel in horziontal mode or bottom panel in vertical
-      // mode) panel should be uncontrolled when resizing.
-      this.sidebarSplitBox.setState({
-        endPanelControl: false,
-        splitterSize: 0,
-      });
-
-      this.ruleViewSideBar.hide();
-      await this.ruleViewSideBar.removeTab("ruleview");
-
-      if (skipQueue) {
-        this.sidebar.addExistingTab(
-        "ruleview",
-        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-        defaultTab == "ruleview",
-        0);
-      } else {
-        this.sidebar.queueExistingTab(
-          "ruleview",
-          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-          defaultTab == "ruleview",
-          0);
-      }
-    }
-
-    this.emit("ruleview-added");
-  },
+  _onRemoveAttribute() {
+    const container = this.markup.getContainer(this.selection.nodeFront);
+    container.removeAttribute(this.nodeMenuTriggerInfo.name);
+  }
 
   /**
-   * Lazily get and create panel instances displayed in the sidebar
+   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
+   *
+   * @param  {String} position
+   *         The position as specified for Element.insertAdjacentHTML
+   *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
    */
-  getPanel: function(id) {
-    if (this._panels.has(id)) {
-      return this._panels.get(id);
+  _pasteAdjacentHTML(position) {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    let panel;
-    switch (id) {
-      case "animationinspector":
-        const AnimationInspector =
-          this.browserRequire("devtools/client/inspector/animation/animation");
-        panel = new AnimationInspector(this, this.panelWin);
-        break;
-      case "boxmodel":
-        // box-model isn't a panel on its own, it used to, now it is being used by
-        // the layout view which retrieves an instance via getPanel.
-        const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
-        panel = new BoxModel(this, this.panelWin);
-        break;
-      case "changesview":
-        const ChangesView =
-          this.browserRequire("devtools/client/inspector/changes/ChangesView");
-        panel = new ChangesView(this, this.panelWin);
-        break;
-      case "computedview":
-        const {ComputedViewTool} =
-          this.browserRequire("devtools/client/inspector/computed/computed");
-        panel = new ComputedViewTool(this, this.panelWin);
-        break;
-      case "fontinspector":
-        const FontInspector =
-          this.browserRequire("devtools/client/inspector/fonts/fonts");
-        panel = new FontInspector(this, this.panelWin);
-        break;
-      case "layoutview":
-        const LayoutView =
-          this.browserRequire("devtools/client/inspector/layout/layout");
-        panel = new LayoutView(this, this.panelWin);
-        break;
-      case "newruleview":
-        const RulesView =
-          this.browserRequire("devtools/client/inspector/rules/new-rules");
-        panel = new RulesView(this, this.panelWin);
-        break;
-      case "ruleview":
-        const {RuleViewTool} = require("devtools/client/inspector/rules/rules");
-        panel = new RuleViewTool(this, this.panelWin);
-        break;
-      default:
-        // This is a custom panel or a non lazy-loaded one.
-        return null;
-    }
-
-    if (panel) {
-      this._panels.set(id, panel);
-    }
-
-    return panel;
-  },
+    const node = this.selection.nodeFront;
+    return this.markup.insertAdjacentHTMLToNode(node, position, content);
+  }
 
   /**
-   * Build the sidebar.
+   * Paste the contents of the clipboard into the selected Node's inner HTML.
    */
-  async setupSidebar() {
-    const sidebar = this.panelDoc.getElementById("inspector-sidebar");
-    const options = {
-      showAllTabsMenu: true,
-      sidebarToggleButton: {
-        collapsed: !this.is3PaneModeEnabled,
-        collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
-        expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
-        onClick: this.onSidebarToggle,
-      },
-    };
-
-    this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
-    this.sidebar.on("select", this.onSidebarSelect);
-
-    const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
-    this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
-      hideTabstripe: true,
-    });
-
-    // defaultTab may also be an empty string or a tab id that doesn't exist anymore
-    // (e.g. it was a tab registered by an addon that has been uninstalled).
-    let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
-
-    if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
-      defaultTab = "layoutview";
+  _pasteInnerHTML() {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    // Append all side panels
-
-    await this.addRuleView({ defaultTab });
+    const node = this.selection.nodeFront;
+    return this.markup.getNodeInnerHTML(node).then(oldContent => {
+      this.markup.updateNodeInnerHTML(node, content, oldContent);
+    });
+  }
 
-    // Inspector sidebar panels in order of appearance.
-    const sidebarPanels = [
-      {
-        id: "layoutview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
-      },
-      {
-        id: "computedview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
-      },
-      {
-        id: "fontinspector",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
-      },
-      {
-        id: "animationinspector",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
-      },
-    ];
-
-    if (this._supportsChangesPanel()) {
-      // Insert Changes as third tab, right after Computed.
-      // TODO: move this inline to `sidebarPanels` above when addressing Bug 1511877.
-      sidebarPanels.splice(2, 0, {
-        id: "changesview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
-      });
-    }
-
-    if (Services.prefs.getBoolPref("devtools.inspector.new-rulesview.enabled")) {
-      sidebarPanels.push({
-        id: "newruleview",
-        title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
-      });
+  /**
+   * Paste the contents of the clipboard into the selected Node's outer HTML.
+   */
+  _pasteOuterHTML() {
+    const content = this._getClipboardContentForPaste();
+    if (!content) {
+      return promise.reject("No clipboard content for paste");
     }
 
-    for (const { id, title } of sidebarPanels) {
-      // The Computed panel is not a React-based panel. We pick its element container from
-      // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
-      // other panels when using the Inspector's tool sidebar.
-      if (id === "computedview") {
-        this.sidebar.queueExistingTab(id, title, defaultTab === id);
-      } else {
-        // When `panel` is a function, it is called when the tab should render. It is
-        // expected to return a React component to populate the tab's content area.
-        // Calling this method on-demand allows us to lazy-load the requested panel.
-        this.sidebar.queueTab(id, title, {
-          props: {
-            id,
-            title,
-          },
-          panel: () => {
-            return this.getPanel(id).provider;
-          },
-        }, defaultTab === id);
-      }
-    }
-
-    this.sidebar.addAllQueuedTabs();
-
-    // Persist splitter state in preferences.
-    this.sidebar.on("show", this.onSidebarShown);
-    this.sidebar.on("hide", this.onSidebarHidden);
-    this.sidebar.on("destroy", this.onSidebarHidden);
-
-    this.sidebar.show();
-  },
+    const node = this.selection.nodeFront;
+    return this.markup.getNodeOuterHTML(node).then(oldContent => {
+      this.markup.updateNodeOuterHTML(node, content, oldContent);
+    });
+  }
 
   /**
-   * Setup any extension sidebar already registered to the toolbox when the inspector.
-   * has been created for the first time.
+   * Show Accessibility properties for currently selected node
    */
-  setupExtensionSidebars() {
-    for (const [sidebarId, {title}] of this.toolbox.inspectorExtensionSidebars) {
-      this.addExtensionSidebar(sidebarId, {title});
-    }
-  },
+  async _showAccessibilityProperties() {
+    const a11yPanel = await this.toolbox.selectTool("accessibility");
+    // Select the accessible object in the panel and wait for the event that
+    // tells us it has been done.
+    const onSelected = a11yPanel.once("new-accessible-front-selected");
+    a11yPanel.selectAccessibleForNode(this.selection.nodeFront, "inspector-context-menu");
+    await onSelected;
+  }
 
   /**
-   * Create a side-panel tab controlled by an extension
-   * using the devtools.panels.elements.createSidebarPane and sidebar object API
-   *
-   * @param {String} id
-   *        An unique id for the sidebar tab.
-   * @param {Object} options
-   * @param {String} options.title
-   *        The tab title
+   * Show DOM properties
    */
-  addExtensionSidebar: function(id, {title}) {
-    if (this._panels.has(id)) {
-      throw new Error(`Cannot create an extension sidebar for the existent id: ${id}`);
-    }
+  _showDOMProperties() {
+    this.toolbox.openSplitConsole().then(() => {
+      const panel = this.toolbox.getPanel("webconsole");
+      const jsterm = panel.hud.jsterm;
 
-    const extensionSidebar = new ExtensionSidebar(this, {id, title});
-
-    // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
-    // the render of the extension title (e.g. use the icon in the sidebar and show the
-    // extension name in a tooltip).
-    this.addSidebarTab(id, title, extensionSidebar.provider, false);
-
-    this._panels.set(id, extensionSidebar);
-
-    // Emit the created ExtensionSidebar instance to the listeners registered
-    // on the toolbox by the "devtools.panels.elements" WebExtensions API.
-    this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
-  },
+      jsterm.execute("inspect($0)");
+      jsterm.focus();
+    });
+  }
 
   /**
-   * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
-   * extension has been disable/uninstalled while the toolbox and inspector were
-   * still open).
+   * Use in Console.
    *
-   * @param {String} id
-   *        The id of the sidebar tab to destroy.
+   * Takes the currently selected node in the inspector and assigns it to a
+   * temp variable on the content window.  Also opens the split console and
+   * autofills it with the temp variable.
    */
-  removeExtensionSidebar: function(id) {
-    if (!this._panels.has(id)) {
-      throw new Error(`Unable to find a sidebar panel with id "${id}"`);
-    }
-
-    const panel = this._panels.get(id);
-
-    if (!(panel instanceof ExtensionSidebar)) {
-      throw new Error(`The sidebar panel with id "${id}" is not an ExtensionSidebar`);
-    }
-
-    this._panels.delete(id);
-    this.sidebar.removeTab(id);
-    panel.destroy();
-  },
-
-  /**
-   * Register a side-panel tab. This API can be used outside of
-   * DevTools (e.g. from an extension) as well as by DevTools
-   * code base.
-   *
-   * @param {string} tab uniq id
-   * @param {string} title tab title
-   * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
-   * @param {boolean} selected true if the panel should be selected
-   */
-  addSidebarTab: function(id, title, panel, selected) {
-    this.sidebar.addTab(id, title, panel, selected);
-  },
+  _useInConsole() {
+    this.toolbox.openSplitConsole().then(() => {
+      const panel = this.toolbox.getPanel("webconsole");
+      const jsterm = panel.hud.jsterm;
 
-  /**
-   * Method to check whether the document is a HTML document and
-   * pickColorFromPage method is available or not.
-   *
-   * @return {Boolean} true if the eyedropper highlighter is supported by the current
-   *         document.
-   */
-  async supportsEyeDropper() {
-    try {
-      const hasSupportsHighlighters =
-        await this.target.actorHasMethod("inspector", "supportsHighlighters");
-
-      let supportsHighlighters;
-      if (hasSupportsHighlighters) {
-        supportsHighlighters = await this.inspector.supportsHighlighters();
-      } else {
-        // If the actor does not provide the supportsHighlighter method, fallback to
-        // check if the selected node's document is a HTML document.
-        const { nodeFront } = this.selection;
-        supportsHighlighters = nodeFront && nodeFront.isInHTMLDocument;
-      }
+      const evalString = `{ let i = 0;
+        while (window.hasOwnProperty("temp" + i) && i < 1000) {
+          i++;
+        }
+        window["temp" + i] = $0;
+        "temp" + i;
+      }`;
 
-      return supportsHighlighters;
-    } catch (e) {
-      console.error(e);
-      return false;
-    }
-  },
-
-  async setupToolbar() {
-    this.teardownToolbar();
+      const options = {
+        selectedNodeActor: this.selection.nodeFront.actorID,
+      };
+      jsterm.requestEvaluation(evalString, options).then((res) => {
+        jsterm.setInputValue(res.result);
+        this.inspector.emit("console-var-ready");
+      });
+    });
+  }
 
-    // Setup the add-node button.
-    this.addNode = this.addNode.bind(this);
-    this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
-    this.addNodeButton.addEventListener("click", this.addNode);
-
-    // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
-    const canShowEyeDropper = await this.supportsEyeDropper();
-
-    // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
-    // available.
-    if (!this.panelDoc) {
+  _buildA11YMenuItem(menu) {
+    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
+        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
       return;
     }
 
-    if (canShowEyeDropper) {
-      this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
-      this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
-      this.eyeDropperButton = this.panelDoc
-                                    .getElementById("inspector-eyedropper-toggle");
-      this.eyeDropperButton.disabled = false;
-      this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label");
-      this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
-    } else {
-      const eyeDropperButton =
-        this.panelDoc.getElementById("inspector-eyedropper-toggle");
-      eyeDropperButton.disabled = true;
-      eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title");
-    }
-
-    this.emit("inspector-toolbar-updated");
-  },
-
-  teardownToolbar: function() {
-    if (this.addNodeButton) {
-      this.addNodeButton.removeEventListener("click", this.addNode);
-      this.addNodeButton = null;
-    }
-
-    if (this.eyeDropperButton) {
-      this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
-      this.eyeDropperButton = null;
-    }
-  },
-
-  /**
-   * Reset the inspector on new root mutation.
-   */
-  onNewRoot: function() {
-    // Record new-root timing for telemetry
-    this._newRootStart = this.panelWin.performance.now();
-
-    this._defaultNode = null;
-    this.selection.setNodeFront(null);
-    this._destroyMarkup();
-
-    const onNodeSelected = defaultNode => {
-      // Cancel this promise resolution as a new one had
-      // been queued up.
-      if (this._pendingSelection != onNodeSelected) {
-        return;
-      }
-      this._pendingSelection = null;
-      this.selection.setNodeFront(defaultNode, { reason: "navigateaway" });
-
-      this.once("markuploaded", this.onMarkupLoaded);
-      this._initMarkup();
+    const showA11YPropsItem = new MenuItem({
+      id: "node-menu-showaccessibilityproperties",
+      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
+      click: () => this._showAccessibilityProperties(),
+      disabled: true,
+    });
 
-      // Setup the toolbar again, since its content may depend on the current document.
-      this.setupToolbar();
-    };
-    this._pendingSelection = onNodeSelected;
-    this._getDefaultNodeForSelection()
-        .then(onNodeSelected, this._handleRejectionIfNotDestroyed);
-  },
-
-  /**
-   * Handler for "markuploaded" event fired on a new root mutation and after the markup
-   * view is initialized. Expands the current selected node and restores the saved
-   * highlighter state.
-   */
-  async onMarkupLoaded() {
-    if (!this.markup) {
-      return;
-    }
-
-    const onExpand = this.markup.expandNode(this.selection.nodeFront);
-
-    // Restore the highlighter states prior to emitting "new-root".
-    if (this._highlighters) {
-      await Promise.all([
-        this.highlighters.restoreFlexboxState(),
-        this.highlighters.restoreGridState(),
-      ]);
-    }
-
-    this.emit("new-root");
-
-    // Wait for full expand of the selected node in order to ensure
-    // the markup view is fully emitted before firing 'reloaded'.
-    // 'reloaded' is used to know when the panel is fully updated
-    // after a page reload.
-    await onExpand;
-
-    this.emit("reloaded");
-
-    // Record the time between new-root event and inspector fully loaded.
-    if (this._newRootStart) {
-      // Only log the timing when inspector is not destroyed and is in foreground.
-      if (this.toolbox && this.toolbox.currentToolId == "inspector") {
-        const delay = this.panelWin.performance.now() - this._newRootStart;
-        const telemetryKey = "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS";
-        const histogram = this.telemetry.getHistogramById(telemetryKey);
-        histogram.add(delay);
-      }
-      delete this._newRootStart;
-    }
-  },
-
-  _selectionCssSelector: null,
-
-  /**
-   * Set the currently selected node unique css selector.
-   * Will store the current target url along with it to allow pre-selection at
-   * reload
-   */
-  set selectionCssSelector(cssSelector = null) {
-    if (this._panelDestroyer) {
-      return;
+    // Only attempt to determine if a11y props menu item needs to be enabled if
+    // AccessibilityFront is enabled.
+    if (this.inspector.accessibilityFront.enabled) {
+      this._updateA11YMenuItem(showA11YPropsItem);
     }
 
-    this._selectionCssSelector = {
-      selector: cssSelector,
-      url: this._target.url,
-    };
-  },
-
-  /**
-   * Get the current selection unique css selector if any, that is, if a node
-   * is actually selected and that node has been selected while on the same url
-   */
-  get selectionCssSelector() {
-    if (this._selectionCssSelector &&
-        this._selectionCssSelector.url === this._target.url) {
-      return this._selectionCssSelector.selector;
-    }
-    return null;
-  },
-
-  /**
-   * On any new selection made by the user, store the unique css selector
-   * of the selected node so it can be restored after reload of the same page
-   */
-  updateSelectionCssSelector() {
-    if (this.selection.isElementNode()) {
-      this.selection.nodeFront.getUniqueSelector().then(selector => {
-        this.selectionCssSelector = selector;
-      }, this._handleRejectionIfNotDestroyed);
-    }
-  },
-
-  /**
-   * Can a new HTML element be inserted into the currently selected element?
-   * @return {Boolean}
-   */
-  canAddHTMLChild: function() {
-    const selection = this.selection;
-
-    // Don't allow to insert an element into these elements. This should only
-    // contain elements where walker.insertAdjacentHTML has no effect.
-    const invalidTagNames = ["html", "iframe"];
-
-    return selection.isHTMLNode() &&
-           selection.isElementNode() &&
-           !selection.isPseudoElementNode() &&
-           !selection.isAnonymousNode() &&
-           !invalidTagNames.includes(
-            selection.nodeFront.nodeName.toLowerCase());
-  },
+    menu.append(showA11YPropsItem);
+  }
 
-  /**
-   * Update the state of the add button in the toolbar depending on the current selection.
-   */
-  updateAddElementButton() {
-    const btn = this.panelDoc.getElementById("inspector-element-add-button");
-    if (this.canAddHTMLChild()) {
-      btn.removeAttribute("disabled");
-    } else {
-      btn.setAttribute("disabled", "true");
-    }
-  },
-
-  /**
-   * Handler for the "host-changed" event from the toolbox. Resets the inspector
-   * sidebar sizes when the toolbox host type changes.
-   */
-  async onHostChanged() {
-    // Eagerly call our resize handling code to process the fact that we
-    // switched hosts. If we don't do this, we'll wait for resize events + 200ms
-    // to have passed, which causes the old layout to noticeably show up in the
-    // new host, followed by the updated one.
-    await this._onLazyPanelResize();
-    // Note that we may have been destroyed by now, especially in tests, so we
-    // need to check if that's happened before touching anything else.
-    if (!this.target || !this.is3PaneModeEnabled) {
-      return;
-    }
-
-    this.setSidebarSplitBoxState();
-  },
-
-  /**
-   * When a new node is selected.
-   */
-  onNewSelection: function(value, reason) {
-    if (reason === "selection-destroy") {
-      return;
-    }
-
-    this.updateAddElementButton();
-    this.updateSelectionCssSelector();
-
-    const selfUpdate = this.updating("inspector-panel");
-    executeSoon(() => {
-      try {
-        selfUpdate(this.selection.nodeFront);
-      } catch (ex) {
-        console.error(ex);
-      }
-    });
-  },
-
-  /**
-   * Delay the "inspector-updated" notification while a tool
-   * is updating itself.  Returns a function that must be
-   * invoked when the tool is done updating with the node
-   * that the tool is viewing.
-   */
-  updating: function(name) {
-    if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
-      this.cancelUpdate();
-    }
+  _getAttributesSubmenu(isEditableElement) {
+    const attributesSubmenu = new Menu();
+    const nodeInfo = this.nodeMenuTriggerInfo;
+    const isAttributeClicked = isEditableElement && nodeInfo &&
+                              nodeInfo.type === "attribute";
 
-    if (!this._updateProgress) {
-      // Start an update in progress.
-      const self = this;
-      this._updateProgress = {
-        node: this.selection.nodeFront,
-        outstanding: new Set(),
-        checkDone: function() {
-          if (this !== self._updateProgress) {
-            return;
-          }
-          // Cancel update if there is no `selection` anymore.
-          // It can happen if the inspector panel is already destroyed.
-          if (!self.selection || (this.node !== self.selection.nodeFront)) {
-            self.cancelUpdate();
-            return;
-          }
-          if (this.outstanding.size !== 0) {
-            return;
-          }
-
-          self._updateProgress = null;
-          self.emit("inspector-updated", name);
-        },
-      };
-    }
-
-    const progress = this._updateProgress;
-    const done = function() {
-      progress.outstanding.delete(done);
-      progress.checkDone();
-    };
-    progress.outstanding.add(done);
-    return done;
-  },
-
-  /**
-   * Cancel notification of inspector updates.
-   */
-  cancelUpdate: function() {
-    this._updateProgress = null;
-  },
-
-  /**
-   * When a node is deleted, select its parent node or the defaultNode if no
-   * parent is found (may happen when deleting an iframe inside which the
-   * node was selected).
-   */
-  onDetached: function(parentNode) {
-    this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
-    const nodeFront = parentNode ? parentNode : this._defaultNode;
-    this.selection.setNodeFront(nodeFront, { reason: "detached" });
-  },
-
-  /**
-   * Destroy the inspector.
-   */
-  destroy: function() {
-    if (this._panelDestroyer) {
-      return this._panelDestroyer;
-    }
-
-    if (this.walker) {
-      this.walker.off("new-root", this.onNewRoot);
-      this.pageStyle = null;
-    }
-
-    this.cancelUpdate();
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-add-attribute",
+      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
+      disabled: !isEditableElement,
+      click: () => this._onAddAttribute(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-copy-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
+                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onCopyAttributeValue(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-edit-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
+                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onEditAttribute(),
+    }));
+    attributesSubmenu.append(new MenuItem({
+      id: "node-menu-remove-attribute",
+      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
+                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
+      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
+      disabled: !isAttributeClicked,
+      click: () => this._onRemoveAttribute(),
+    }));
 
-    this.sidebar.destroy();
-
-    this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
-    this.selection.off("new-node-front", this.onNewSelection);
-    this.selection.off("detached-front", this.onDetached);
-    this.sidebar.off("select", this.onSidebarSelect);
-    this.sidebar.off("show", this.onSidebarShown);
-    this.sidebar.off("hide", this.onSidebarHidden);
-    this.sidebar.off("destroy", this.onSidebarHidden);
-    this.target.off("will-navigate", this._onBeforeNavigate);
-
-    for (const [, panel] of this._panels) {
-      panel.destroy();
-    }
-    this._panels.clear();
-
-    if (this._highlighters) {
-      this._highlighters.destroy();
-      this._highlighters = null;
-    }
-
-    if (this._markupFrame) {
-      this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.contentWindow.removeEventListener("contextmenu",
-                                                          this._onContextMenu);
-    }
-
-    if (this._search) {
-      this._search.destroy();
-      this._search = null;
-    }
-
-    const sidebarDestroyer = this.sidebar.destroy();
-    const ruleViewSideBarDestroyer = this.ruleViewSideBar ?
-      this.ruleViewSideBar.destroy() : null;
-    const markupDestroyer = this._destroyMarkup();
-
-    this.teardownToolbar();
-
-    this.breadcrumbs.destroy();
-    this.reflowTracker.destroy();
-    this.styleChangeTracker.destroy();
-
-    this._is3PaneModeChromeEnabled = null;
-    this._is3PaneModeEnabled = null;
-    this._markupBox = null;
-    this._markupFrame = null;
-    this._notificationBox = null;
-    this._target = null;
-    this._toolbox = null;
-    this.breadcrumbs = null;
-    this.panelDoc = null;
-    this.panelWin.inspector = null;
-    this.panelWin = null;
-    this.resultsLength = null;
-    this.searchBox = null;
-    this.show3PaneTooltip = null;
-    this.sidebar = null;
-    this.store = null;
-    this.telemetry = null;
-
-    this._panelDestroyer = promise.all([
-      markupDestroyer,
-      sidebarDestroyer,
-      ruleViewSideBarDestroyer,
-    ]);
-
-    return this._panelDestroyer;
-  },
+    return attributesSubmenu;
+  }
 
   /**
    * Returns the clipboard content if it is appropriate for pasting
    * into the current node's outer HTML, otherwise returns null.
    */
-  _getClipboardContentForPaste: function() {
+  _getClipboardContentForPaste() {
     const content = clipboardHelper.getText();
     if (content && content.trim().length > 0) {
       return content;
     }
     return null;
-  },
+  }
 
-  _onContextMenu: function(e) {
-    if (!(e.originalTarget instanceof Element) ||
-        e.originalTarget.closest("input[type=text]") ||
-        e.originalTarget.closest("input:not([type])") ||
-        e.originalTarget.closest("textarea")) {
-      return;
+  _getCopySubmenu(markupContainer, isSelectionElement) {
+    const copySubmenu = new Menu();
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyinner",
+      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyInnerHTML(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyouter",
+      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyOuterHTML(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyuniqueselector",
+      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyUniqueSelector(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copycsspath",
+      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyCssPath(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyxpath",
+      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
+      disabled: !isSelectionElement,
+      click: () => this._copyXPath(),
+    }));
+    copySubmenu.append(new MenuItem({
+      id: "node-menu-copyimagedatauri",
+      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
+      disabled: !isSelectionElement || !markupContainer ||
+                !markupContainer.isPreviewable(),
+      click: () => this._copyImageDataUri(),
+    }));
+
+    return copySubmenu;
+  }
+
+  /**
+   * Link menu items can be shown or hidden depending on the context and
+   * selected node, and their labels can vary.
+   *
+   * @return {Array} list of visible menu items related to links.
+   */
+  _getNodeLinkMenuItems() {
+    const linkFollow = new MenuItem({
+      id: "node-menu-link-follow",
+      visible: false,
+      click: () => this._onFollowLink(),
+    });
+    const linkCopy = new MenuItem({
+      id: "node-menu-link-copy",
+      visible: false,
+      click: () => this._onCopyLink(),
+    });
+
+    // Get information about the right-clicked node.
+    const popupNode = this.contextMenuTarget;
+    if (!popupNode || !popupNode.classList.contains("link")) {
+      return [linkFollow, linkCopy];
     }
 
-    e.stopPropagation();
-    e.preventDefault();
+    const type = popupNode.dataset.type;
+    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
+      // Links can't be opened in new tabs in the browser toolbox.
+      if (type === "uri" && !this.target.chrome) {
+        linkFollow.visible = true;
+        linkFollow.label = INSPECTOR_L10N.getStr(
+          "inspector.menu.openUrlInNewTab.label");
+      } else if (type === "cssresource") {
+        linkFollow.visible = true;
+        linkFollow.label = TOOLBOX_L10N.getStr(
+          "toolbox.viewCssSourceInStyleEditor.label");
+      } else if (type === "jsresource") {
+        linkFollow.visible = true;
+        linkFollow.label = TOOLBOX_L10N.getStr(
+          "toolbox.viewJsSourceInDebugger.label");
+      }
+
+      linkCopy.visible = true;
+      linkCopy.label = INSPECTOR_L10N.getStr(
+        "inspector.menu.copyUrlToClipboard.label");
+    } else if (type === "idref") {
+      linkFollow.visible = true;
+      linkFollow.label = INSPECTOR_L10N.getFormatStr(
+        "inspector.menu.selectElement.label", popupNode.dataset.link);
+    }
+
+    return [linkFollow, linkCopy];
+  }
+
+  _getPasteSubmenu(isEditableElement) {
+    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
+    const disableAdjacentPaste = !isPasteable ||
+                                 this.selection.isRoot() ||
+                                 this.selection.isBodyNode() ||
+                                 this.selection.isHeadNode();
+    const disableFirstLastPaste = !isPasteable ||
+      (this.selection.isHTMLNode() && this.selection.isRoot());
 
-    this._openMenu({
-      screenX: e.screenX,
-      screenY: e.screenY,
-      target: e.target,
-    });
-  },
+    const pasteSubmenu = new Menu();
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteinnerhtml",
+      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
+      disabled: !isPasteable,
+      click: () => this._pasteInnerHTML(),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteouterhtml",
+      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
+      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
+      disabled: !isPasteable,
+      click: () => this._pasteOuterHTML(),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastebefore",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
+      disabled: disableAdjacentPaste,
+      click: () => this._pasteAdjacentHTML("beforeBegin"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pasteafter",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
+      disabled: disableAdjacentPaste,
+      click: () => this._pasteAdjacentHTML("afterEnd"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastefirstchild",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
+      disabled: disableFirstLastPaste,
+      click: () => this._pasteAdjacentHTML("afterBegin"),
+    }));
+    pasteSubmenu.append(new MenuItem({
+      id: "node-menu-pastelastchild",
+      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
+      disabled: disableFirstLastPaste,
+      click: () => this._pasteAdjacentHTML("beforeEnd"),
+    }));
 
-  _openMenu: function({ target, screenX = 0, screenY = 0 } = { }) {
+    return pasteSubmenu;
+  }
+
+  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
     if (this.selection.isSlotted()) {
       // Slotted elements should not show any context menu.
       return null;
     }
 
     const markupContainer = this.markup.getContainer(this.selection.nodeFront);
 
     this.contextMenuTarget = target;
@@ -1502,37 +606,37 @@ Inspector.prototype = {
                            this.selection.nodeFront.isTreeDisplayed;
 
     const menu = new Menu();
     menu.append(new MenuItem({
       id: "node-menu-edithtml",
       label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
       disabled: !isEditableElement,
-      click: () => this.editHTML(),
+      click: () => this._editHTML(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-add",
       label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
-      disabled: !this.canAddHTMLChild(),
-      click: () => this.addNode(),
+      disabled: !this.inspector.canAddHTMLChild(),
+      click: () => this.inspector.addNode(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-duplicatenode",
       label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
       disabled: !isDuplicatableElement,
-      click: () => this.duplicateNode(),
+      click: () => this._duplicateNode(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-delete",
       label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
-      disabled: !this.isDeletable(this.selection.nodeFront),
-      click: () => this.deleteNode(),
+      disabled: !this.markup.isDeletable(this.selection.nodeFront),
+      click: () => this._deleteNode(),
     }));
 
     menu.append(new MenuItem({
       label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
       accesskey:
         INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
       submenu: this._getAttributesSubmenu(isEditableElement),
     }));
@@ -1542,17 +646,17 @@ Inspector.prototype = {
     }));
 
     // Set the pseudo classes
     for (const name of ["hover", "active", "focus", "focus-within"]) {
       const menuitem = new MenuItem({
         id: "node-menu-pseudo-" + name,
         label: name,
         type: "checkbox",
-        click: this.togglePseudoClass.bind(this, ":" + name),
+        click: () => this.inspector.togglePseudoClass(":" + name),
       });
 
       if (isSelectionElement) {
         const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
         menuitem.checked = checked;
       } else {
         menuitem.disabled = true;
       }
@@ -1579,920 +683,94 @@ Inspector.prototype = {
     }));
 
     const isNodeWithChildren = this.selection.isNode() &&
                              markupContainer.hasChildren;
     menu.append(new MenuItem({
       id: "node-menu-expand",
       label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
       disabled: !isNodeWithChildren,
-      click: () => this.expandNode(),
+      click: () => this.markup.expandAll(this.selection.nodeFront),
     }));
     menu.append(new MenuItem({
       id: "node-menu-collapse",
       label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
       disabled: !isNodeWithChildren || !markupContainer.expanded,
-      click: () => this.collapseAll(),
+      click: () => this.markup.collapseAll(this.selection.nodeFront),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
     }));
 
     menu.append(new MenuItem({
       id: "node-menu-scrollnodeintoview",
       label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
       accesskey:
         INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
       disabled: !isSelectionElement,
-      click: () => this.scrollNodeIntoView(),
+      click: () => this.markup.scrollNodeIntoView(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-screenshotnode",
       label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
       disabled: !isScreenshotable,
-      click: () => this.screenshotNode().catch(console.error),
+      click: () => this.inspector.screenshotNode().catch(console.error),
     }));
     menu.append(new MenuItem({
       id: "node-menu-useinconsole",
       label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
-      click: () => this.useInConsole(),
+      click: () => this._useInConsole(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-showdomproperties",
       label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
-      click: () => this.showDOMProperties(),
+      click: () => this._showDOMProperties(),
     }));
 
     if (this.selection.nodeFront.customElementLocation) {
       menu.append(new MenuItem({
         type: "separator",
       }));
 
       menu.append(new MenuItem({
         id: "node-menu-jumptodefinition",
         label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
-        click: () => this.jumpToCustomElementDefinition(),
+        click: () => this._jumpToCustomElementDefinition(),
       }));
     }
 
-    this.buildA11YMenuItem(menu);
+    this._buildA11YMenuItem(menu);
 
     const nodeLinkMenuItems = this._getNodeLinkMenuItems();
     if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
       menu.append(new MenuItem({
         id: "node-menu-link-separator",
         type: "separator",
       }));
     }
 
     for (const menuitem of nodeLinkMenuItems) {
       menu.append(menuitem);
     }
 
-    menu.popup(screenX, screenY, this._toolbox);
+    menu.popup(screenX, screenY, this.toolbox);
     return menu;
-  },
-
-  buildA11YMenuItem: function(menu) {
-    if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
-        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
-      return;
-    }
+  }
 
-    const showA11YPropsItem = new MenuItem({
-      id: "node-menu-showaccessibilityproperties",
-      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
-      click: () => this.showAccessibilityProperties(),
-      disabled: true,
-    });
-    // Only attempt to determine if a11y props menu item needs to be enabled if
-    // AccessibilityFront is enabled.
-    if (this.accessibilityFront.enabled) {
-      this._updateA11YMenuItem(showA11YPropsItem);
-    }
-
-    menu.append(showA11YPropsItem);
-  },
-
-  _updateA11YMenuItem: async function(menuItem) {
+  async _updateA11YMenuItem(menuItem) {
     const hasMethod = await this.target.actorHasMethod("domwalker",
                                                        "hasAccessibilityProperties");
     if (!hasMethod) {
       return;
     }
 
     const hasA11YProps = await this.walker.hasAccessibilityProperties(
       this.selection.nodeFront);
     if (hasA11YProps) {
-      this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
-    }
-
-    this.emit("node-menu-updated");
-  },
-
-  _getCopySubmenu: function(markupContainer, isSelectionElement) {
-    const copySubmenu = new Menu();
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyinner",
-      label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyInnerHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyouter",
-      label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyOuterHTML(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyuniqueselector",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyUniqueSelector(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copycsspath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyCssPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyxpath",
-      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
-      disabled: !isSelectionElement,
-      click: () => this.copyXPath(),
-    }));
-    copySubmenu.append(new MenuItem({
-      id: "node-menu-copyimagedatauri",
-      label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
-      disabled: !isSelectionElement || !markupContainer ||
-                !markupContainer.isPreviewable(),
-      click: () => this.copyImageDataUri(),
-    }));
-
-    return copySubmenu;
-  },
-
-  _getPasteSubmenu: function(isEditableElement) {
-    const isPasteable = isEditableElement && this._getClipboardContentForPaste();
-    const disableAdjacentPaste = !isPasteable || this.selection.isRoot() ||
-          this.selection.isBodyNode() || this.selection.isHeadNode();
-    const disableFirstLastPaste = !isPasteable ||
-          (this.selection.isHTMLNode() && this.selection.isRoot());
-
-    const pasteSubmenu = new Menu();
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteinnerhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteInnerHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteouterhtml",
-      label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
-      disabled: !isPasteable,
-      click: () => this.pasteOuterHTML(),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastebefore",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("beforeBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pasteafter",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
-      disabled: disableAdjacentPaste,
-      click: () => this.pasteAdjacentHTML("afterEnd"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastefirstchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("afterBegin"),
-    }));
-    pasteSubmenu.append(new MenuItem({
-      id: "node-menu-pastelastchild",
-      label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
-      accesskey:
-        INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
-      disabled: disableFirstLastPaste,
-      click: () => this.pasteAdjacentHTML("beforeEnd"),
-    }));
-
-    return pasteSubmenu;
-  },
-
-  _getAttributesSubmenu: function(isEditableElement) {
-    const attributesSubmenu = new Menu();
-    const nodeInfo = this.nodeMenuTriggerInfo;
-    const isAttributeClicked = isEditableElement && nodeInfo &&
-                              nodeInfo.type === "attribute";
-
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-add-attribute",
-      label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
-      accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
-      disabled: !isEditableElement,
-      click: () => this.onAddAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-copy-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
-                                        isAttributeClicked ? `${nodeInfo.value}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onCopyAttributeValue(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-edit-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onEditAttribute(),
-    }));
-    attributesSubmenu.append(new MenuItem({
-      id: "node-menu-remove-attribute",
-      label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
-                                        isAttributeClicked ? `${nodeInfo.name}` : ""),
-      accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
-      disabled: !isAttributeClicked,
-      click: () => this.onRemoveAttribute(),
-    }));
-
-    return attributesSubmenu;
-  },
-
-  /**
-   * Link menu items can be shown or hidden depending on the context and
-   * selected node, and their labels can vary.
-   *
-   * @return {Array} list of visible menu items related to links.
-   */
-  _getNodeLinkMenuItems: function() {
-    const linkFollow = new MenuItem({
-      id: "node-menu-link-follow",
-      visible: false,
-      click: () => this.onFollowLink(),
-    });
-    const linkCopy = new MenuItem({
-      id: "node-menu-link-copy",
-      visible: false,
-      click: () => this.onCopyLink(),
-    });
-
-    // Get information about the right-clicked node.
-    const popupNode = this.contextMenuTarget;
-    if (!popupNode || !popupNode.classList.contains("link")) {
-      return [linkFollow, linkCopy];
-    }
-
-    const type = popupNode.dataset.type;
-    if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
-      // Links can't be opened in new tabs in the browser toolbox.
-      if (type === "uri" && !this.target.chrome) {
-        linkFollow.visible = true;
-        linkFollow.label = INSPECTOR_L10N.getStr(
-          "inspector.menu.openUrlInNewTab.label");
-      } else if (type === "cssresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewCssSourceInStyleEditor.label");
-      } else if (type === "jsresource") {
-        linkFollow.visible = true;
-        linkFollow.label = TOOLBOX_L10N.getStr(
-          "toolbox.viewJsSourceInDebugger.label");
-      }
-
-      linkCopy.visible = true;
-      linkCopy.label = INSPECTOR_L10N.getStr(
-        "inspector.menu.copyUrlToClipboard.label");
-    } else if (type === "idref") {
-      linkFollow.visible = true;
-      linkFollow.label = INSPECTOR_L10N.getFormatStr(
-        "inspector.menu.selectElement.label", popupNode.dataset.link);
-    }
-
-    return [linkFollow, linkCopy];
-  },
-
-  _initMarkup: function() {
-    if (!this._markupFrame) {
-      this._markupFrame = this.panelDoc.createElement("iframe");
-      this._markupFrame.setAttribute("aria-label",
-        INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
-      this._markupFrame.setAttribute("flex", "1");
-      // This is needed to enable tooltips inside the iframe document.
-      this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
-
-      this._markupBox.style.visibility = "hidden";
-      this._markupBox.appendChild(this._markupFrame);
-
-      this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
-      this._markupFrame.setAttribute("src", "markup/markup.xhtml");
-    } else {
-      this._onMarkupFrameLoad();
-    }
-  },
-
-  _onMarkupFrameLoad: function() {
-    this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
-    this._markupFrame.contentWindow.addEventListener("contextmenu", this._onContextMenu);
-    this._markupFrame.contentWindow.focus();
-    this._markupBox.style.visibility = "visible";
-    this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
-    this.emit("markuploaded");
-  },
-
-  _destroyMarkup: function() {
-    let destroyPromise;
-
-    if (this.markup) {
-      destroyPromise = this.markup.destroy();
-      this.markup = null;
-    } else {
-      destroyPromise = promise.resolve();
-    }
-
-    this._markupBox.style.visibility = "hidden";
-
-    return destroyPromise;
-  },
-
-  onEyeDropperButtonClicked: function() {
-    this.eyeDropperButton.classList.contains("checked")
-      ? this.hideEyeDropper()
-      : this.showEyeDropper();
-  },
-
-  startEyeDropperListeners: function() {
-    this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
-    this.inspector.once("color-picked", this.onEyeDropperDone);
-    this.walker.once("new-root", this.onEyeDropperDone);
-  },
-
-  stopEyeDropperListeners: function() {
-    this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
-    this.inspector.off("color-picked", this.onEyeDropperDone);
-    this.walker.off("new-root", this.onEyeDropperDone);
-  },
-
-  onEyeDropperDone: function() {
-    this.eyeDropperButton.classList.remove("checked");
-    this.stopEyeDropperListeners();
-  },
-
-  /**
-   * Show the eyedropper on the page.
-   * @return {Promise} resolves when the eyedropper is visible.
-   */
-  showEyeDropper: function() {
-    // The eyedropper button doesn't exist, most probably because the actor doesn't
-    // support the pickColorFromPage, or because the page isn't HTML.
-    if (!this.eyeDropperButton) {
-      return null;
-    }
-
-    this.telemetry.scalarSet(TELEMETRY_EYEDROPPER_OPENED, 1);
-    this.eyeDropperButton.classList.add("checked");
-    this.startEyeDropperListeners();
-    return this.inspector.pickColorFromPage({copyOnSelect: true})
-                         .catch(console.error);
-  },
-
-  /**
-   * Hide the eyedropper.
-   * @return {Promise} resolves when the eyedropper is hidden.
-   */
-  hideEyeDropper: function() {
-    // The eyedropper button doesn't exist, most probably  because the page isn't HTML.
-    if (!this.eyeDropperButton) {
-      return null;
-    }
-
-    this.eyeDropperButton.classList.remove("checked");
-    this.stopEyeDropperListeners();
-    return this.inspector.cancelPickColorFromPage()
-                         .catch(console.error);
-  },
-
-  /**
-   * Create a new node as the last child of the current selection, expand the
-   * parent and select the new node.
-   */
-  async addNode() {
-    if (!this.canAddHTMLChild()) {
-      return;
+      this.toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
     }
 
-    const html = "<div></div>";
-
-    // Insert the html and expect a childList markup mutation.
-    const onMutations = this.once("markupmutation");
-    await this.walker.insertAdjacentHTML(this.selection.nodeFront, "beforeEnd", html);
-    await onMutations;
-
-    // Expand the parent node.
-    this.markup.expandNode(this.selection.nodeFront);
-  },
-
-  /**
-   * Toggle a pseudo class.
-   */
-  togglePseudoClass: function(pseudo) {
-    if (this.selection.isElementNode()) {
-      const node = this.selection.nodeFront;
-      if (node.hasPseudoClassLock(pseudo)) {
-        return this.walker.removePseudoClassLock(node, pseudo, {parents: true});
-      }
-
-      const hierarchical = pseudo == ":hover" || pseudo == ":active";
-      return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
-    }
-    return promise.resolve();
-  },
-
-  /**
-   * Show DOM properties
-   */
-  showDOMProperties: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      jsterm.execute("inspect($0)");
-      jsterm.focus();
-    });
-  },
-
-  jumpToCustomElementDefinition: function() {
-    const node = this.selection.nodeFront;
-    const { url, line } = node.customElementLocation;
-    this._toolbox.viewSourceInDebugger(url, line, "show_custom_element");
-  },
-
-  /**
-   * Show Accessibility properties for currently selected node
-   */
-  async showAccessibilityProperties() {
-    const a11yPanel = await this._toolbox.selectTool("accessibility");
-    // Select the accessible object in the panel and wait for the event that
-    // tells us it has been done.
-    const onSelected = a11yPanel.once("new-accessible-front-selected");
-    a11yPanel.selectAccessibleForNode(this.selection.nodeFront,
-                                      "inspector-context-menu");
-    await onSelected;
-  },
-
-  /**
-   * Use in Console.
-   *
-   * Takes the currently selected node in the inspector and assigns it to a
-   * temp variable on the content window.  Also opens the split console and
-   * autofills it with the temp variable.
-   */
-  useInConsole: function() {
-    this._toolbox.openSplitConsole().then(() => {
-      const panel = this._toolbox.getPanel("webconsole");
-      const jsterm = panel.hud.jsterm;
-
-      const evalString = `{ let i = 0;
-        while (window.hasOwnProperty("temp" + i) && i < 1000) {
-          i++;
-        }
-        window["temp" + i] = $0;
-        "temp" + i;
-      }`;
-
-      const options = {
-        selectedNodeActor: this.selection.nodeFront.actorID,
-      };
-      jsterm.requestEvaluation(evalString, options).then((res) => {
-        jsterm.setInputValue(res.result);
-        this.emit("console-var-ready");
-      });
-    });
-  },
-
-  /**
-   * Edit the outerHTML of the selected Node.
-   */
-  editHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    if (this.markup) {
-      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's outer HTML.
-   */
-  pasteOuterHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeOuterHTML(node).then(oldContent => {
-      this.markup.updateNodeOuterHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard into the selected Node's inner HTML.
-   */
-  pasteInnerHTML: function() {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.getNodeInnerHTML(node).then(oldContent => {
-      this.markup.updateNodeInnerHTML(node, content, oldContent);
-    });
-  },
-
-  /**
-   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
-   * @param position
-   *        The position as specified for Element.insertAdjacentHTML
-   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
-   */
-  pasteAdjacentHTML: function(position) {
-    const content = this._getClipboardContentForPaste();
-    if (!content) {
-      return promise.reject("No clipboard content for paste");
-    }
-
-    const node = this.selection.nodeFront;
-    return this.markup.insertAdjacentHTMLToNode(node, position, content);
-  },
-
-  /**
-   * Copy the innerHTML of the selected Node to the clipboard.
-   */
-  copyInnerHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
-  },
-
-  /**
-   * Copy the outerHTML of the selected Node to the clipboard.
-   */
-  copyOuterHTML: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-    const node = this.selection.nodeFront;
-
-    switch (node.nodeType) {
-      case nodeConstants.ELEMENT_NODE :
-        this._copyLongString(this.walker.outerHTML(node));
-        break;
-      case nodeConstants.COMMENT_NODE :
-        this._getLongString(node.getNodeValue()).then(comment => {
-          clipboardHelper.copyString("<!--" + comment + "-->");
-        });
-        break;
-      case nodeConstants.DOCUMENT_TYPE_NODE :
-        clipboardHelper.copyString(node.doctypeString);
-        break;
-    }
-  },
-
-  /**
-   * Copy the data-uri for the currently selected image in the clipboard.
-   */
-  copyImageDataUri: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    if (container && container.isPreviewable()) {
-      container.copyImageDataUri();
-    }
-  },
-
-  /**
-   * Copy the content of a longString (via a promise resolving a
-   * LongStringActor) to the clipboard
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving (with no argument) when the
-   *         string is sent to the clipboard
-   */
-  _copyLongString: function(longStringActorPromise) {
-    return this._getLongString(longStringActorPromise).then(string => {
-      clipboardHelper.copyString(string);
-    }).catch(console.error);
-  },
-
-  /**
-   * Retrieve the content of a longString (via a promise resolving a LongStringActor)
-   * @param  {Promise} longStringActorPromise
-   *         promise expected to resolve a LongStringActor instance
-   * @return {Promise} promise resolving with the retrieved string as argument
-   */
-  _getLongString: function(longStringActorPromise) {
-    return longStringActorPromise.then(longStringActor => {
-      return longStringActor.string().then(string => {
-        longStringActor.release().catch(console.error);
-        return string;
-      });
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy a unique selector of the selected Node to the clipboard.
-   */
-  copyUniqueSelector: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
-    this.selection.nodeFront.getUniqueSelector().then(selector => {
-      clipboardHelper.copyString(selector);
-    }).catch(console.error);
-  },
-
-  /**
-   * Copy the full CSS Path of the selected Node to the clipboard.
-   */
-  copyCssPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
-    this.selection.nodeFront.getCssPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
+    this.inspector.emit("node-menu-updated");
+  }
+}
 
-  /**
-   * Copy the XPath of the selected Node to the clipboard.
-   */
-  copyXPath: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
-    this.selection.nodeFront.getXPath().then(path => {
-      clipboardHelper.copyString(path);
-    }).catch(console.error);
-  },
-
-  /**
-   * Initiate screenshot command on selected node.
-   */
-  async screenshotNode() {
-    // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
-    // is still visible, therefore showing it in the picture.
-    // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
-    // will do that, calling `hide` for the highlighter only if previously shown.
-    await this.highlighter.hideBoxModel();
-
-    const clipboardEnabled = Services.prefs
-      .getBoolPref("devtools.screenshot.clipboard.enabled");
-    const args = {
-      file: true,
-      nodeActorID: this.selection.nodeFront.actorID,
-      clipboard: clipboardEnabled,
-    };
-    const screenshotFront = await this.target.getFront("screenshot");
-    const screenshot = await screenshotFront.capture(args);
-    await saveScreenshot(this.panelWin, args, screenshot);
-  },
-
-  /**
-   * Scroll the node into view.
-   */
-  scrollNodeIntoView: function() {
-    if (!this.selection.isNode()) {
-      return;
-    }
-
-    this.selection.nodeFront.scrollIntoView();
-  },
-
-  /**
-   * Duplicate the selected node
-   */
-  duplicateNode: function() {
-    const selection = this.selection;
-    if (!selection.isElementNode() ||
-        selection.isRoot() ||
-        selection.isAnonymousNode() ||
-        selection.isPseudoElementNode()) {
-      return;
-    }
-    this.walker.duplicateNode(selection.nodeFront).catch(console.error);
-  },
-
-  /**
-   * Delete the selected node.
-   */
-  deleteNode: function() {
-    if (!this.selection.isNode() ||
-         this.selection.isRoot()) {
-      return;
-    }
-
-    // If the markup panel is active, use the markup panel to delete
-    // the node, making this an undoable action.
-    if (this.markup) {
-      this.markup.deleteNode(this.selection.nodeFront);
-    } else {
-      // remove the node from content
-      this.walker.removeNode(this.selection.nodeFront);
-    }
-  },
-
-  /**
-   * Add attribute to node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onAddAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.addAttribute();
-  },
-
-  /**
-   * Copy attribute value for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onCopyAttributeValue: function() {
-    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
-  },
-
-  /**
-   * Edit attribute for node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onEditAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.editAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  /**
-   * Remove attribute from node.
-   * Used for node context menu and shouldn't be called directly.
-   */
-  onRemoveAttribute: function() {
-    const container = this.markup.getContainer(this.selection.nodeFront);
-    container.removeAttribute(this.nodeMenuTriggerInfo.name);
-  },
-
-  expandNode: function() {
-    this.markup.expandAll(this.selection.nodeFront);
-  },
-
-  collapseAll: function() {
-    this.markup.collapseAll(this.selection.nodeFront);
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-follow menu item
-   * in the inspector contextual-menu.
-   */
-  onFollowLink: function() {
-    const type = this.contextMenuTarget.dataset.type;
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.followAttributeLink(type, link);
-  },
-
-  /**
-   * Given a type and link found in a node's attribute in the markup-view,
-   * attempt to follow that link (which may result in opening a new tab, the
-   * style editor or debugger).
-   */
-  followAttributeLink: function(type, link) {
-    if (!type || !link) {
-      return;
-    }
-
-    if (type === "uri" || type === "cssresource" || type === "jsresource") {
-      // Open link in a new tab.
-      this.inspector.resolveRelativeURL(
-        link, this.selection.nodeFront).then(url => {
-          if (type === "uri") {
-            openContentLink(url);
-          } else if (type === "cssresource") {
-            return this.toolbox.viewSourceInStyleEditor(url);
-          } else if (type === "jsresource") {
-            return this.toolbox.viewSourceInDebugger(url);
-          }
-          return null;
-        }).catch(console.error);
-    } else if (type == "idref") {
-      // Select the node in the same document.
-      this.walker.document(this.selection.nodeFront).then(doc => {
-        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
-          if (!node) {
-            this.emit("idref-attribute-link-failed");
-            return;
-          }
-          this.selection.setNodeFront(node);
-        });
-      }).catch(console.error);
-    }
-  },
-
-  /**
-   * This method is here for the benefit of the node-menu-link-copy menu item
-   * in the inspector contextual-menu.
-   */
-  onCopyLink: function() {
-    const link = this.contextMenuTarget.dataset.link;
-
-    this.copyAttributeLink(link);
-  },
-
-  /**
-   * This method is here for the benefit of copying links.
-   */
-  copyAttributeLink: function(link) {
-    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
-      clipboardHelper.copyString(url);
-    }, console.error);
-  },
-
-  /**
-   * Returns an object containing the shared handler functions used in the box
-   * model and grid React components.
-   */
-  getCommonComponentProps() {
-    return {
-      setSelectedNode: this.selection.setNodeFront,
-      onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
-    };
-  },
-
-  /**
-   * Shows the box-model highlighter on the element corresponding to the provided
-   * NodeFront.
-   *
-   * @param  {NodeFront} nodeFront
-   *         The node to highlight.
-   * @param  {Object} options
-   *         Options passed to the highlighter actor.
-   */
-  onShowBoxModelHighlighterForNode(nodeFront, options) {
-    const toolbox = this.toolbox;
-    toolbox.highlighter.highlight(nodeFront, options);
-  },
-
-  /**
-   * Returns a value indicating whether a node can be deleted.
-   *
-   * @param {NodeFront} nodeFront
-   *        The node to test for deletion
-   */
-  isDeletable(nodeFront) {
-    return !(nodeFront.isDocumentElement ||
-           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
-           nodeFront.isAnonymous);
-  },
-
-  async inspectNodeActor(nodeActor, inspectFromAnnotation) {
-    const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
-    if (!nodeFront) {
-      console.error("The object cannot be linked to the inspector, the " +
-                    "corresponding nodeFront could not be found.");
-      return false;
-    }
-
-    const isAttached = await this.walker.isInDOMTree(nodeFront);
-    if (!isAttached) {
-      console.error("Selected DOMNode is not attached to the document tree.");
-      return false;
-    }
-
-    await this.selection.setNodeFront(nodeFront, { reason: inspectFromAnnotation });
-    return true;
-  },
-};
-
-exports.Inspector = Inspector;
+module.exports = MarkupContextMenu;
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -16,19 +16,24 @@ const AutocompletePopup = require("devto
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
 const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
 const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
 const RootContainer = require("devtools/client/inspector/markup/views/root-container");
 
+loader.lazyRequireGetter(this, "MarkupContextMenu", "devtools/client/inspector/markup/markup-context-menu");
 loader.lazyRequireGetter(this, "SlottedNodeContainer", "devtools/client/inspector/markup/views/slotted-node-container");
+loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
+loader.lazyRequireGetter(this, "getLongString", "devtools/client/inspector/shared/utils", true);
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 loader.lazyRequireGetter(this, "HTMLTooltip", "devtools/client/shared/widgets/tooltip/HTMLTooltip", true);
 loader.lazyRequireGetter(this, "UndoStack", "devtools/client/shared/undo", true);
+loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 // Page size for pageup/pagedown
 const PAGE_SIZE = 10;
 const DEFAULT_MAX_CHILDREN = 100;
 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
@@ -93,31 +98,33 @@ function MarkupView(inspector, frame, co
   // slotted container for a given node front.
   this._slottedContainerKeys = new WeakMap();
 
   // Binding functions that need to be called in scope.
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
   this._mutationObserver = this._mutationObserver.bind(this);
   this._onBlur = this._onBlur.bind(this);
+  this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCollapseAttributesPrefChange = this._onCollapseAttributesPrefChange.bind(this);
   this._onWalkerNodeStatesChanged = this._onWalkerNodeStatesChanged.bind(this);
   this._onFocus = this._onFocus.bind(this);
   this._onMouseClick = this._onMouseClick.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseOut = this._onMouseOut.bind(this);
   this._onMouseUp = this._onMouseUp.bind(this);
   this._onNewSelection = this._onNewSelection.bind(this);
   this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
   this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
 
   // Listening to various events.
   this._elt.addEventListener("blur", this._onBlur, true);
   this._elt.addEventListener("click", this._onMouseClick);
+  this._elt.addEventListener("contextmenu", this._onContextMenu);
   this._elt.addEventListener("mousemove", this._onMouseMove);
   this._elt.addEventListener("mouseout", this._onMouseOut);
   this._frame.addEventListener("focus", this._onFocus);
   this.inspector.selection.on("new-node-front", this._onNewSelection);
   this.walker.on("display-change", this._onWalkerNodeStatesChanged);
   this.walker.on("scrollable-change", this._onWalkerNodeStatesChanged);
   this.walker.on("mutations", this._mutationObserver);
   this.win.addEventListener("copy", this._onCopy);
@@ -151,16 +158,24 @@ function MarkupView(inspector, frame, co
 MarkupView.prototype = {
   /**
    * How long does a node flash when it mutates (in ms).
    */
   CONTAINER_FLASHING_DURATION: 500,
 
   _selectedContainer: null,
 
+  get contextMenu() {
+    if (!this._contextMenu) {
+      this._contextMenu = new MarkupContextMenu(this);
+    }
+
+    return this._contextMenu;
+  },
+
   get eventDetailsTooltip() {
     if (!this._eventDetailsTooltip) {
       // This tooltip will be attached to the toolbox document.
       this._eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, {
         type: "arrow",
         consumeOutsideClicks: false,
       });
     }
@@ -274,16 +289,20 @@ MarkupView.prototype = {
       return;
     }
 
     if (this._selectedContainer) {
       this._selectedContainer.clearFocus();
     }
   },
 
+  _onContextMenu: function(event) {
+    this.contextMenu.show(event);
+  },
+
   /**
    * Executed on each mouse-move while a node is being dragged in the view.
    * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
    * node in.
    */
   _autoScroll: function(event) {
     const docEl = this.doc.documentElement;
 
@@ -761,23 +780,84 @@ MarkupView.prototype = {
   _onCopy: function(evt) {
     // Ignore copy events from editors
     if (this._isInputOrTextarea(evt.target)) {
       return;
     }
 
     const selection = this.inspector.selection;
     if (selection.isNode()) {
-      this.inspector.copyOuterHTML();
+      this.copyOuterHTML();
     }
     evt.stopPropagation();
     evt.preventDefault();
   },
 
   /**
+   * Copy the outerHTML of the selected Node to the clipboard.
+   */
+  copyOuterHTML: function() {
+    if (!this.inspector.selection.isNode()) {
+      return;
+    }
+    const node = this.inspector.selection.nodeFront;
+
+    switch (node.nodeType) {
+      case nodeConstants.ELEMENT_NODE :
+        copyLongString(this.walker.outerHTML(node));
+        break;
+      case nodeConstants.COMMENT_NODE :
+        getLongString(node.getNodeValue()).then(comment => {
+          clipboardHelper.copyString("<!--" + comment + "-->");
+        });
+        break;
+      case nodeConstants.DOCUMENT_TYPE_NODE :
+        clipboardHelper.copyString(node.doctypeString);
+        break;
+    }
+  },
+
+  /**
+   * Given a type and link found in a node's attribute in the markup-view,
+   * attempt to follow that link (which may result in opening a new tab, the
+   * style editor or debugger).
+   */
+  followAttributeLink: function(type, link) {
+    if (!type || !link) {
+      return;
+    }
+
+    if (type === "uri" || type === "cssresource" || type === "jsresource") {
+      // Open link in a new tab.
+      this.inspector.inspector.resolveRelativeURL(
+        link, this.inspector.selection.nodeFront).then(url => {
+          if (type === "uri") {
+            openContentLink(url);
+          } else if (type === "cssresource") {
+            return this.toolbox.viewSourceInStyleEditor(url);
+          } else if (type === "jsresource") {
+            return this.toolbox.viewSourceInDebugger(url);
+          }
+          return null;
+        }).catch(console.error);
+    } else if (type == "idref") {
+      // Select the node in the same document.
+      this.walker.document(this.inspector.selection.nodeFront).then(doc => {
+        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
+          if (!node) {
+            this.emit("idref-attribute-link-failed");
+            return;
+          }
+          this.inspector.selection.setNodeFront(node);
+        });
+      }).catch(console.error);
+    }
+  },
+
+  /**
    * Register all key shortcuts.
    */
   _initShortcuts: function() {
     const shortcuts = new KeyShortcuts({
       window: this.win,
     });
 
     this._onShortcut = this._onShortcut.bind(this);
@@ -815,18 +895,17 @@ MarkupView.prototype = {
         }
         break;
       }
       case "markupView.edit.key": {
         this.beginEditingOuterHTML(this._selectedContainer.node);
         break;
       }
       case "markupView.scrollInto.key": {
-        const selection = this._selectedContainer.node;
-        this.inspector.scrollNodeIntoView(selection);
+        this.scrollNodeIntoView();
         break;
       }
       // Generic keys
       case "Delete": {
         this.deleteNodeOrAttribute();
         break;
       }
       case "Backspace": {
@@ -956,26 +1035,38 @@ MarkupView.prototype = {
       const container = focusedAttribute.closest("li.child").container;
       container.removeAttribute(focusedAttribute.dataset.attr);
     } else {
       this.deleteNode(this._selectedContainer.node, moveBackward);
     }
   },
 
   /**
+   * Returns a value indicating whether a node can be deleted.
+   *
+   * @param {NodeFront} nodeFront
+   *        The node to test for deletion
+   */
+  isDeletable(nodeFront) {
+    return !(nodeFront.isDocumentElement ||
+           nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
+           nodeFront.isAnonymous);
+  },
+
+  /**
    * Delete a node from the DOM.
    * This is an undoable action.
    *
    * @param  {NodeFront} node
    *         The node to remove.
    * @param  {Boolean} moveBackward
    *         If set to true, focus the previous sibling, otherwise the next one.
    */
   deleteNode: function(node, moveBackward) {
-    if (!this.inspector.isDeletable(node)) {
+    if (!this.isDeletable(node)) {
       return;
     }
 
     const container = this.getContainer(node);
 
     // Retain the node so we can undo this...
     this.walker.retainNode(node).then(() => {
       const parent = node.parentNode();
@@ -1013,16 +1104,27 @@ MarkupView.prototype = {
         const isValidSibling = nextSibling && !nextSibling.isPseudoElement;
         nextSibling = isValidSibling ? nextSibling : null;
         this.walker.insertBefore(node, parent, nextSibling);
       });
     }).catch(console.error);
   },
 
   /**
+   * Scroll the node into view.
+   */
+  scrollNodeIntoView() {
+    if (!this.inspector.selection.isNode()) {
+      return;
+    }
+
+    this.inspector.selection.nodeFront.scrollIntoView();
+  },
+
+  /**
    * If an editable item is focused, select its container.
    */
   _onFocus: function(event) {
     let parent = event.target;
     while (!parent.container) {
       parent = parent.parentNode;
     }
     if (parent) {
@@ -1947,16 +2049,17 @@ MarkupView.prototype = {
       this._undo = null;
     }
 
     this.popup.destroy();
     this.popup = null;
 
     this._elt.removeEventListener("blur", this._onBlur, true);
     this._elt.removeEventListener("click", this._onMouseClick);
+    this._elt.removeEventListener("contextmenu", this._onContextMenu);
     this._elt.removeEventListener("mousemove", this._onMouseMove);
     this._elt.removeEventListener("mouseout", this._onMouseOut);
     this._frame.removeEventListener("focus", this._onFocus);
     this.inspector.selection.off("new-node-front", this._onNewSelection);
     this.inspector.inspector.nodePicker.off(
       "picker-node-hovered", this._onToolboxPickerHover
     );
     this.walker.off("display-change", this._onWalkerNodeStatesChanged);
--- a/devtools/client/inspector/markup/moz.build
+++ b/devtools/client/inspector/markup/moz.build
@@ -4,13 +4,14 @@
 # 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/.
 
 DIRS += [
     'views',
 ]
 
 DevToolsModules(
+    'markup-context-menu.js',
     'markup.js',
     'utils.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/markup/test/browser_markup_links_05.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js
@@ -18,17 +18,17 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   let {editor} = await getContainerForSelector("video", inspector);
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("poster").querySelector(".link"),
   });
 
   info("Follow the link and wait for the new tab to open");
   const onTabOpened = once(gBrowser.tabContainer, "TabOpen");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   const {target: tab} = await onTabOpened;
   await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   ok(true, "A new tab opened");
   is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "doc_markup_tooltip.png",
     "The URL for the new tab is correct");
   gBrowser.removeTab(tab);
 
@@ -38,32 +38,32 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the ref");
   ({editor} = await getContainerForSelector("label", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("for").querySelector(".link"),
   });
 
   info("Follow the link and wait for the new node to be selected");
   const onSelection = inspector.selection.once("new-node-front");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onSelection;
 
   ok(true, "A new node was selected");
   is(inspector.selection.nodeFront.id, "name", "The right node was selected");
 
   info("Select a node with an invalid IDREF attribute");
   await selectNode("output", inspector);
 
   info("Set the popupNode to the node that contains the ref");
   ({editor} = await getContainerForSelector("output", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("for").querySelectorAll(".link")[2],
   });
 
   info("Try to follow the link and check that no new node were selected");
-  const onFailed = inspector.once("idref-attribute-link-failed");
-  inspector.onFollowLink();
+  const onFailed = inspector.markup.once("idref-attribute-link-failed");
+  inspector.markup.contextMenu._onFollowLink();
   await onFailed;
 
   ok(true, "The node selection failed");
   is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
     "The <output> node is still selected");
 });
--- a/devtools/client/inspector/markup/test/browser_markup_links_06.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_06.js
@@ -18,17 +18,17 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   let {editor} = await getContainerForSelector("link", inspector);
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("href").querySelector(".link"),
   });
 
   info("Follow the link and wait for the style-editor to open");
   const onStyleEditorReady = toolbox.once("styleeditor-ready");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onStyleEditorReady;
 
   // No real need to test that the editor opened on the right file here as this
   // is already tested in /framework/test/browser_toolbox_view_source_*
   ok(true, "The style-editor was open");
 
   info("Switch back to the inspector");
   await toolbox.selectTool("inspector");
@@ -39,15 +39,15 @@ add_task(async function() {
   info("Set the popupNode to the node that contains the uri");
   ({editor} = await getContainerForSelector("script", inspector));
   openContextMenuAndGetAllItems(inspector, {
     target: editor.attrElements.get("src").querySelector(".link"),
   });
 
   info("Follow the link and wait for the debugger to open");
   const onDebuggerReady = toolbox.once("jsdebugger-ready");
-  inspector.onFollowLink();
+  inspector.markup.contextMenu._onFollowLink();
   await onDebuggerReady;
 
   // No real need to test that the debugger opened on the right file here as
   // this is already tested in /framework/test/browser_toolbox_view_source_*
   ok(true, "The debugger was open");
 });
--- a/devtools/client/inspector/markup/test/browser_markup_links_07.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_07.js
@@ -94,16 +94,16 @@ async function followLinkWaitForNewNode(
   performMouseDown(linkEl, isMetaClick);
   await onSelection;
 
   ok(true, "A new node was selected");
   is(inspector.selection.nodeFront.id, "name", "The right node was selected");
 }
 
 async function followLinkNoNewNode(linkEl, isMetaClick, inspector) {
-  const onFailed = inspector.once("idref-attribute-link-failed");
+  const onFailed = inspector.markup.once("idref-attribute-link-failed");
   performMouseDown(linkEl, isMetaClick);
   await onFailed;
 
   ok(true, "The node selection failed");
   is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
     "The <output> node is still selected");
 }
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_copy_paths.js
@@ -40,25 +40,30 @@ add_task(async function() {
   const shadowRootContainer = hostContainer.getChildContainers()[0];
   await expandContainer(inspector, shadowRootContainer);
 
   info("Select the div under the shadow root");
   const divContainer = shadowRootContainer.getChildContainers()[0];
   await selectNode(divContainer.node, inspector);
 
   info("Check the copied values for the various copy*Path helpers");
-  await waitForClipboardPromise(() => inspector.copyXPath(), '//*[@id="el1"]');
-  await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1");
-  await waitForClipboardPromise(() => inspector.copyUniqueSelector(), "#el1");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
+    '//*[@id="el1"]');
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
+    "div#el1");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
+    "#el1");
 
   info("Expand the div");
   await expandContainer(inspector, divContainer);
 
   info("Select the third span");
   const spanContainer = divContainer.getChildContainers()[2];
   await selectNode(spanContainer.node, inspector);
 
   info("Check the copied values for the various copy*Path helpers");
-  await waitForClipboardPromise(() => inspector.copyXPath(), "/div/span[3]");
-  await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1 span");
-  await waitForClipboardPromise(() => inspector.copyUniqueSelector(),
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
+    "/div/span[3]");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
+    "div#el1 span");
+  await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
     "#el1 > span:nth-child(3)");
 });
--- a/devtools/client/inspector/markup/views/markup-container.js
+++ b/devtools/client/inspector/markup/views/markup-container.js
@@ -559,17 +559,17 @@ MarkupContainer.prototype = {
     }
 
     // Follow attribute links if middle or meta click.
     if (isMiddleClick || isMetaClick) {
       const link = target.dataset.link;
       const type = target.dataset.type;
       // Make container tabbable descendants not tabbable (by default).
       this.canFocus = false;
-      this.markup.inspector.followAttributeLink(type, link);
+      this.markup.followAttributeLink(type, link);
       return;
     }
 
     // Start node drag & drop (if the mouse moved, see _onMouseMove).
     if (isLeftClick && this.isDraggable()) {
       this._isPreDragging = true;
       this._dragStartY = event.pageY;
       this.markup._draggedContainer = this;
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -6,16 +6,17 @@
 
 "use strict";
 
 const promise = require("promise");
 
 loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
 loader.lazyRequireGetter(this, "getCSSLexer", "devtools/shared/css/lexer", true);
 loader.lazyRequireGetter(this, "parseDeclarations", "devtools/shared/css/parsing-utils", true);
+loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * Called when a character is typed in a value editor.  This decides
  * whether to advance or not, first by checking to see if ";" was
  * typed, and then by lexing the input and seeing whether the ";"
  * would be a terminator at this point.
@@ -78,16 +79,31 @@ function blurOnMultipleProperties(cssPro
       if (props.length > 1) {
         e.target.blur();
       }
     }, 0);
   };
 }
 
 /**
+ * Copy the content of a longString (via a promise resolving a
+ * LongStringActor) to the clipboard.
+ *
+ * @param  {Promise} longStringActorPromise
+ *         promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving (with no argument) when the
+ *         string is sent to the clipboard
+ */
+function copyLongString(longStringActorPromise) {
+  return getLongString(longStringActorPromise).then(string => {
+    clipboardHelper.copyString(string);
+  }).catch(console.error);
+}
+
+/**
  * Create a child element with a set of attributes.
  *
  * @param {Element} parent
  *        The parent node.
  * @param {string} tagName
  *        The tag name.
  * @param {object} attributes
  *        A set of attributes to set on the node.
@@ -105,16 +121,32 @@ function createChild(parent, tagName, at
       }
     }
   }
   parent.appendChild(elt);
   return elt;
 }
 
 /**
+ * Retrieve the content of a longString (via a promise resolving a LongStringActor).
+ *
+ * @param  {Promise} longStringActorPromise
+ *         promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving with the retrieved string as argument
+ */
+function getLongString(longStringActorPromise) {
+  return longStringActorPromise.then(longStringActor => {
+    return longStringActor.string().then(string => {
+      longStringActor.release().catch(console.error);
+      return string;
+    });
+  }).catch(console.error);
+}
+
+/**
  * Returns a selector of the Element Rep from the grip. This is based on the
  * getElements() function in our devtools-reps component for a ElementNode.
  *
  * @param  {Object} grip
  *         Grip-like object that can be used with Reps.
  * @return {String} selector of the element node.
  */
 function getSelectorFromGrip(grip) {
@@ -191,12 +223,14 @@ function translateNodeFrontToGrip(nodeFr
       nodeType: nodeFront.nodeType,
     },
   };
 }
 
 exports.advanceValidate = advanceValidate;
 exports.appendText = appendText;
 exports.blurOnMultipleProperties = blurOnMultipleProperties;
+exports.copyLongString = copyLongString;
 exports.createChild = createChild;
+exports.getLongString = getLongString;
 exports.getSelectorFromGrip = getSelectorFromGrip;
 exports.promiseWarn = promiseWarn;
 exports.translateNodeFrontToGrip = translateNodeFrontToGrip;
--- a/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js
@@ -31,48 +31,48 @@ add_task(async function() {
     ok(hasAttribute, "attribute was successfully added");
   }
 
   async function testCopyAttributeValue() {
     info("Testing 'Copy Attribute Value' and waiting for clipboard promise to resolve");
     const copyAttributeValue = getMenuItem("node-menu-copy-attribute");
 
     info("Triggering 'Copy Attribute Value' and waiting for clipboard to copy the value");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
       value: "the",
     };
 
     await waitForClipboardPromise(() => copyAttributeValue.click(), "the");
   }
 
   async function testCopyLongAttributeValue() {
     info("Testing 'Copy Attribute Value' copies very long attribute values");
     const copyAttributeValue = getMenuItem("node-menu-copy-attribute");
     const longAttribute = "#01234567890123456789012345678901234567890123456789" +
     "12345678901234567890123456789012345678901234567890123456789012345678901" +
     "23456789012345678901234567890123456789012345678901234567890123456789012" +
     "34567890123456789012345678901234567890123456789012345678901234567890123";
 
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
       value: longAttribute,
     };
 
     await waitForClipboardPromise(() => copyAttributeValue.click(), longAttribute);
   }
 
   async function testEditAttribute() {
     info("Testing 'Edit Attribute' menu item");
     const editAttribute = getMenuItem("node-menu-edit-attribute");
 
     info("Triggering 'Edit Attribute' and waiting for mutation to occur");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-edit",
     };
     editAttribute.click();
     EventUtils.sendString("data-edit='edited'");
     const onMutation = inspector.once("markupmutation");
     EventUtils.synthesizeKey("KEY_Enter");
     await onMutation;
@@ -82,17 +82,17 @@ add_task(async function() {
     ok(isAttributeChanged, "attribute was successfully edited");
   }
 
   async function testRemoveAttribute() {
     info("Testing 'Remove Attribute' menu item");
     const removeAttribute = getMenuItem("node-menu-remove-attribute");
 
     info("Triggering 'Remove Attribute' and waiting for mutation to occur");
-    inspector.nodeMenuTriggerInfo = {
+    inspector.markup.contextMenu.nodeMenuTriggerInfo = {
       type: "attribute",
       name: "data-remove",
     };
     const onMutation = inspector.once("markupmutation");
     removeAttribute.click();
     await onMutation;
 
     const hasAttribute = await testActor.hasNode("#attributes[data-remove]");
--- a/devtools/client/inspector/test/browser_inspector_textbox-menu.js
+++ b/devtools/client/inspector/test/browser_inspector_textbox-menu.js
@@ -77,17 +77,17 @@ add_task(async function() {
 
   // Move the mouse out of the box-model region to avoid triggering the box model
   // highlighter.
   EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.panelWin);
 });
 
 async function checkTextBox(textBox, toolbox) {
   let textboxContextMenu = toolbox.doc.getElementById("toolbox-menu");
-  ok(!textboxContextMenu, "The menu is  closed");
+  ok(!textboxContextMenu, "The menu is closed");
 
   info("Simulating context click on the textbox and expecting the menu to open");
   const onContextMenu = toolbox.once("menu-open");
   synthesizeContextMenuEvent(textBox);
   await onContextMenu;
 
   textboxContextMenu = toolbox.doc.getElementById("toolbox-menu");
   ok(textboxContextMenu, "The menu is now visible");
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -655,11 +655,11 @@ function openStyleContextMenuAndGetAllIt
 
 /**
  * Open the inspector menu and return all of it's items in a flat array
  * @param {InspectorPanel} inspector
  * @param {Object} options to pass into openMenu
  * @return An array of MenuItems
  */
 function openContextMenuAndGetAllItems(inspector, options) {
-  const menu = inspector._openMenu(options);
+  const menu = inspector.markup.contextMenu._openMenu(options);
   return buildContextMenuItems(menu);
 }
--- a/dom/base/test/test_bug422403-1.html
+++ b/dom/base/test/test_bug422403-1.html
@@ -26,24 +26,24 @@ function loadFileContent(aFile, aCharset
 
     var baseUri = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1']
                                   .createInstance(SpecialPowers.Ci.nsIURIMutator)
                                   .setSpec(window.location.href)
                                   .finalize();
 
     var ios = SpecialPowers.Cc['@mozilla.org/network/io-service;1']
             .getService(SpecialPowers.Ci.nsIIOService);
-    var chann = ios.newChannel2(aFile,
-                                aCharset,
-                                baseUri,
-                                null,      // aLoadingNode
-                                SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
-                                null,      // aTriggeringPrincipal
-                                SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
+    var chann = ios.newChannel(aFile,
+                               aCharset,
+                               baseUri,
+                               null,      // aLoadingNode
+                               SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                               SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
 
     var cis = SpecialPowers.Ci.nsIConverterInputStream;
 
     var inputStream = SpecialPowers.Cc["@mozilla.org/intl/converter-input-stream;1"]
                        .createInstance(cis);
     inputStream.init(chann.open(), aCharset, 1024, cis.DEFAULT_REPLACEMENT_CHARACTER);
     var str = {}, content = '';
     while (inputStream.readString(4096, str) != 0) {
--- a/dom/base/test/test_bug422403-2.xhtml
+++ b/dom/base/test/test_bug422403-2.xhtml
@@ -25,24 +25,24 @@ function loadFileContent(aFile, aCharset
 
     var baseUri = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1']
                                   .createInstance(SpecialPowers.Ci.nsIURIMutator)
                                   .setSpec(window.location.href)
                                   .finalize();
 
     var ios = SpecialPowers.Cc['@mozilla.org/network/io-service;1']
             .getService(SpecialPowers.Ci.nsIIOService);
-    var chann = ios.newChannel2(aFile,
-                                aCharset,
-                                baseUri,
-                                null,      // aLoadingNode
-                                SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
-                                null,      // aTriggeringPrincipal
-                                SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
+    var chann = ios.newChannel(aFile,
+                               aCharset,
+                               baseUri,
+                               null,      // aLoadingNode
+                               SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                               SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
 
     var cis = SpecialPowers.Ci.nsIConverterInputStream;
 
     var inputStream = SpecialPowers.Cc["@mozilla.org/intl/converter-input-stream;1"]
                        .createInstance(cis);
     inputStream.init(chann.open(), aCharset, 1024, cis.DEFAULT_REPLACEMENT_CHARACTER);
     var str = {}, content = '';
     while (inputStream.readString(4096, str) != 0) {
--- a/dom/base/test/test_bug424359-1.html
+++ b/dom/base/test/test_bug424359-1.html
@@ -26,24 +26,24 @@ function loadFileContent(aFile, aCharset
 
     var baseUri = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1']
                                   .createInstance(SpecialPowers.Ci.nsIURIMutator)
                                   .setSpec(window.location.href)
                                   .finalize();
 
     var ios = SpecialPowers.Cc['@mozilla.org/network/io-service;1']
             .getService(SpecialPowers.Ci.nsIIOService);
-    var chann = ios.newChannel2(aFile,
-                                aCharset,
-                                baseUri,
-                                null,      // aLoadingNode
-                                SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
-                                null,      // aTriggeringPrincipal
-                                SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
+    var chann = ios.newChannel(aFile,
+                               aCharset,
+                               baseUri,
+                               null,      // aLoadingNode
+                               SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                               SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
 
     var cis = SpecialPowers.Ci.nsIConverterInputStream;
 
     var inputStream = SpecialPowers.Cc["@mozilla.org/intl/converter-input-stream;1"]
                        .createInstance(cis);
     inputStream.init(chann.open(), aCharset, 1024, cis.DEFAULT_REPLACEMENT_CHARACTER);
     var str = {}, content = '';
     while (inputStream.readString(4096, str) != 0) {
--- a/dom/base/test/test_bug424359-2.html
+++ b/dom/base/test/test_bug424359-2.html
@@ -25,24 +25,24 @@ function loadFileContent(aFile, aCharset
 
     var baseUri = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1']
                                   .createInstance(SpecialPowers.Ci.nsIURIMutator)
                                   .setSpec(window.location.href)
                                   .finalize();
 
     var ios = SpecialPowers.Cc['@mozilla.org/network/io-service;1']
             .getService(SpecialPowers.Ci.nsIIOService);
-    var chann = ios.newChannel2(aFile,
-                                aCharset,
-                                baseUri,
-                                null,      // aLoadingNode
-                                SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
-                                null,      // aTriggeringPrincipal
-                                SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
+    var chann = ios.newChannel(aFile,
+                               aCharset,
+                               baseUri,
+                               null,      // aLoadingNode
+                               SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                               SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
 
     var cis = SpecialPowers.Ci.nsIConverterInputStream;
 
     var inputStream = SpecialPowers.Cc["@mozilla.org/intl/converter-input-stream;1"]
                        .createInstance(cis);
     inputStream.init(chann.open(), aCharset, 1024, cis.DEFAULT_REPLACEMENT_CHARACTER);
     var str = {}, content = '';
     while (inputStream.readString(4096, str) != 0) {
--- a/dom/base/test/test_bug498433.html
+++ b/dom/base/test/test_bug498433.html
@@ -24,24 +24,24 @@ function loadFileContent(aFile, aCharset
 
     var baseUri = SpecialPowers.Cc['@mozilla.org/network/standard-url-mutator;1']
                                   .createInstance(SpecialPowers.Ci.nsIURIMutator)
                                   .setSpec(window.location.href)
                                   .finalize();
 
     var ios = SpecialPowers.Cc['@mozilla.org/network/io-service;1']
             .getService(SpecialPowers.Ci.nsIIOService);
-    var chann = ios.newChannel2(aFile,
-                                aCharset,
-                                baseUri,
-                                null,      // aLoadingNode
-                                SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
-                                null,      // aTriggeringPrincipal
-                                SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
+    var chann = ios.newChannel(aFile,
+                               aCharset,
+                               baseUri,
+                               null,      // aLoadingNode
+                               SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               SpecialPowers.Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                               SpecialPowers.Ci.nsIContentPolicy.TYPE_OTHER);
 
     var cis = SpecialPowers.Ci.nsIConverterInputStream;
 
     var inputStream = SpecialPowers.Cc["@mozilla.org/intl/converter-input-stream;1"]
                        .createInstance(cis);
     inputStream.init(chann.open(), aCharset, 1024, cis.DEFAULT_REPLACEMENT_CHARACTER);
     var str = {}, content = '';
     while (inputStream.readString(4096, str) != 0) {
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -3417,16 +3417,24 @@ static MathSpace ExtractMathSpace(MDefin
       return MathSpace::Infinite;
     case MDefinition::IndirectTruncate:
     case MDefinition::Truncate:
       return MathSpace::Modulo;
   }
   MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unknown TruncateKind");
 }
 
+static bool MonotoneAdd(int32_t lhs, int32_t rhs) {
+  return (lhs >= 0 && rhs >= 0) || (lhs <= 0 && rhs <= 0);
+}
+
+static bool MonotoneSub(int32_t lhs, int32_t rhs) {
+  return (lhs >= 0 && rhs <= 0) || (lhs <= 0 && rhs >= 0);
+}
+
 // Extract a linear sum from ins, if possible (otherwise giving the
 // sum 'ins + 0').
 SimpleLinearSum jit::ExtractLinearSum(MDefinition* ins, MathSpace space) {
   if (ins->isBeta()) {
     ins = ins->getOperand(0);
   }
 
   if (ins->type() != MIRType::Int32) {
@@ -3466,29 +3474,31 @@ SimpleLinearSum jit::ExtractLinearSum(MD
     return SimpleLinearSum(ins, 0);
   }
 
   // Check if this is of the form <SUM> + n or n + <SUM>.
   if (ins->isAdd()) {
     int32_t constant;
     if (space == MathSpace::Modulo) {
       constant = uint32_t(lsum.constant) + uint32_t(rsum.constant);
-    } else if (!SafeAdd(lsum.constant, rsum.constant, &constant)) {
+    } else if (!SafeAdd(lsum.constant, rsum.constant, &constant) ||
+               !MonotoneAdd(lsum.constant, rsum.constant)) {
       return SimpleLinearSum(ins, 0);
     }
     return SimpleLinearSum(lsum.term ? lsum.term : rsum.term, constant);
   }
 
   MOZ_ASSERT(ins->isSub());
   // Check if this is of the form <SUM> - n.
   if (lsum.term) {
     int32_t constant;
     if (space == MathSpace::Modulo) {
       constant = uint32_t(lsum.constant) - uint32_t(rsum.constant);
-    } else if (!SafeSub(lsum.constant, rsum.constant, &constant)) {
+    } else if (!SafeSub(lsum.constant, rsum.constant, &constant) ||
+               !MonotoneSub(lsum.constant, rsum.constant)) {
       return SimpleLinearSum(ins, 0);
     }
     return SimpleLinearSum(lsum.term, constant);
   }
 
   // Ignore any of the form n - <SUM>.
   return SimpleLinearSum(ins, 0);
 }
--- a/media/mtransport/ipc/WebrtcProxyChannel.cpp
+++ b/media/mtransport/ipc/WebrtcProxyChannel.cpp
@@ -164,17 +164,17 @@ nsresult WebrtcProxyChannel::Open(const 
   }
 
   // -need to always tunnel since we're using a proxy
   // -there shouldn't be an opportunity to send cookies, but explicitly disallow
   // them anyway.
   // -the previous proxy tunnel didn't support redirects e.g. 307. don't need to
   // introduce new behavior. can't follow redirects on connect anyway.
   nsCOMPtr<nsIChannel> localChannel;
-  rv = ioService->NewChannelFromURIWithProxyFlags2(
+  rv = ioService->NewChannelFromURIWithProxyFlags(
       uri, nullptr,
       // Proxy flags are overridden by SetConnectOnly()
       0, aLoadInfo->LoadingNode(), aLoadInfo->LoadingPrincipal(),
       aLoadInfo->TriggeringPrincipal(),
       nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS | nsILoadInfo::SEC_COOKIES_OMIT |
           // We need this flag to allow loads from any origin since this channel
           // is being used to CONNECT to an HTTP proxy.
           nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
--- a/netwerk/base/NetUtil.jsm
+++ b/netwerk/base/NetUtil.jsm
@@ -296,22 +296,22 @@ var NetUtil = {
                     " the options object unless loading from system principal.",
                     Cr.NS_ERROR_INVALID_ARG,
                     Components.stack.caller
                 );
             }
             contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER;
         }
 
-        return Services.io.newChannelFromURI2(uri,
-                                              loadingNode || null,
-                                              loadingPrincipal || null,
-                                              triggeringPrincipal || null,
-                                              securityFlags,
-                                              contentPolicyType);
+        return Services.io.newChannelFromURI(uri,
+                                             loadingNode || null,
+                                             loadingPrincipal || null,
+                                             triggeringPrincipal || null,
+                                             securityFlags,
+                                             contentPolicyType);
     },
 
     /**
      * Reads aCount bytes from aInputStream into a string.
      *
      * @param aInputStream
      *        The input stream to read from.
      * @param aCount
--- a/netwerk/base/nsIIOService.idl
+++ b/netwerk/base/nsIIOService.idl
@@ -96,51 +96,51 @@ interface nsIIOService : nsISupports
      *
      * Please note, if you provide both a loadingNode and a loadingPrincipal,
      * then loadingPrincipal must be equal to loadingNode->NodePrincipal().
      * But less error prone is to just supply a loadingNode.
      *
      * Keep in mind that URIs coming from a webpage should *never* use the
      * systemPrincipal as the loadingPrincipal.
      */
-    nsIChannel newChannelFromURI2(in nsIURI aURI,
-                                  in Node aLoadingNode,
-                                  in nsIPrincipal aLoadingPrincipal,
-                                  in nsIPrincipal aTriggeringPrincipal,
-                                  in unsigned long aSecurityFlags,
-                                  in unsigned long aContentPolicyType);
+    nsIChannel newChannelFromURI(in nsIURI aURI,
+                                 in Node aLoadingNode,
+                                 in nsIPrincipal aLoadingPrincipal,
+                                 in nsIPrincipal aTriggeringPrincipal,
+                                 in unsigned long aSecurityFlags,
+                                 in unsigned long aContentPolicyType);
 
     [noscript, nostdcall, notxpcom]
     nsresult NewChannelFromURIWithClientAndController(in nsIURI aURI,
                                                       in Node aLoadingNode,
                                                       in nsIPrincipal aLoadingPrincipal,
                                                       in nsIPrincipal aTriggeringPrincipal,
                                                       in const_MaybeClientInfoRef aLoadingClientInfo,
                                                       in const_MaybeServiceWorkerDescriptorRef aController,
                                                       in unsigned long aSecurityFlags,
                                                       in unsigned long aContentPolicyType,
                                                       out nsIChannel aResult);
 
     /**
-     * Equivalent to newChannelFromURI2(aURI, aLoadingNode, ...)
+     * Equivalent to newChannelFromURI(aURI, aLoadingNode, ...)
      */
     nsIChannel newChannelFromURIWithLoadInfo(in nsIURI aURI,
                                              in nsILoadInfo aLoadInfo);
 
     /**
-     * Equivalent to newChannelFromURI2(newURI(...))
+     * Equivalent to newChannelFromURI(newURI(...))
      */
-    nsIChannel newChannel2(in AUTF8String aSpec,
-                           in string aOriginCharset,
-                           in nsIURI aBaseURI,
-                           in Node aLoadingNode,
-                           in nsIPrincipal aLoadingPrincipal,
-                           in nsIPrincipal aTriggeringPrincipal,
-                           in unsigned long aSecurityFlags,
-                           in unsigned long aContentPolicyType);
+    nsIChannel newChannel(in AUTF8String aSpec,
+                          in string aOriginCharset,
+                          in nsIURI aBaseURI,
+                          in Node aLoadingNode,
+                          in nsIPrincipal aLoadingPrincipal,
+                          in nsIPrincipal aTriggeringPrincipal,
+                          in unsigned long aSecurityFlags,
+                          in unsigned long aContentPolicyType);
 
     /**
      * Returns true if networking is in "offline" mode. When in offline mode, 
      * attempts to access the network will fail (although this does not 
      * necessarily correlate with whether there is actually a network 
      * available -- that's hard to detect without causing the dialer to 
      * come up).
      *
@@ -221,24 +221,24 @@ interface nsIIOService : nsISupports
      *        These will be used as values for the nsILoadInfo object on the
      *        created channel. For details, see nsILoadInfo in nsILoadInfo.idl
      * @return reference to the new nsIChannel object
      *
      * Please note, if you provide both a loadingNode and a loadingPrincipal,
      * then loadingPrincipal must be equal to loadingNode->NodePrincipal().
      * But less error prone is to just supply a loadingNode.
      */
-    nsIChannel newChannelFromURIWithProxyFlags2(in nsIURI aURI,
-                                                in nsIURI aProxyURI,
-                                                in unsigned long aProxyFlags,
-                                                in Node aLoadingNode,
-                                                in nsIPrincipal aLoadingPrincipal,
-                                                in nsIPrincipal aTriggeringPrincipal,
-                                                in unsigned long aSecurityFlags,
-                                                in unsigned long aContentPolicyType);
+    nsIChannel newChannelFromURIWithProxyFlags(in nsIURI aURI,
+                                               in nsIURI aProxyURI,
+                                               in unsigned long aProxyFlags,
+                                               in Node aLoadingNode,
+                                               in nsIPrincipal aLoadingPrincipal,
+                                               in nsIPrincipal aTriggeringPrincipal,
+                                               in unsigned long aSecurityFlags,
+                                               in unsigned long aContentPolicyType);
 };
 
 %{C++
 /**
  * We send notifications through nsIObserverService with topic
  * NS_IOSERVICE_GOING_OFFLINE_TOPIC and data NS_IOSERVICE_OFFLINE
  * when 'offline' has changed from false to true, and we are about
  * to shut down network services such as DNS. When those
--- a/netwerk/base/nsIOService.cpp
+++ b/netwerk/base/nsIOService.cpp
@@ -831,28 +831,28 @@ nsIOService::NewFileURI(nsIFile *file, n
 
   nsCOMPtr<nsIFileProtocolHandler> fileHandler(do_QueryInterface(handler, &rv));
   if (NS_FAILED(rv)) return rv;
 
   return fileHandler->NewFileURI(file, result);
 }
 
 NS_IMETHODIMP
-nsIOService::NewChannelFromURI2(nsIURI *aURI, nsINode *aLoadingNode,
-                                nsIPrincipal *aLoadingPrincipal,
-                                nsIPrincipal *aTriggeringPrincipal,
-                                uint32_t aSecurityFlags,
-                                uint32_t aContentPolicyType,
-                                nsIChannel **result) {
-  return NewChannelFromURIWithProxyFlags2(aURI,
-                                          nullptr,  // aProxyURI
-                                          0,        // aProxyFlags
-                                          aLoadingNode, aLoadingPrincipal,
-                                          aTriggeringPrincipal, aSecurityFlags,
-                                          aContentPolicyType, result);
+nsIOService::NewChannelFromURI(nsIURI *aURI, nsINode *aLoadingNode,
+                               nsIPrincipal *aLoadingPrincipal,
+                               nsIPrincipal *aTriggeringPrincipal,
+                               uint32_t aSecurityFlags,
+                               uint32_t aContentPolicyType,
+                               nsIChannel **result) {
+  return NewChannelFromURIWithProxyFlags(aURI,
+                                         nullptr,  // aProxyURI
+                                         0,        // aProxyFlags
+                                         aLoadingNode, aLoadingPrincipal,
+                                         aTriggeringPrincipal, aSecurityFlags,
+                                         aContentPolicyType, result);
 }
 nsresult nsIOService::NewChannelFromURIWithClientAndController(
     nsIURI *aURI, nsINode *aLoadingNode, nsIPrincipal *aLoadingPrincipal,
     nsIPrincipal *aTriggeringPrincipal,
     const Maybe<ClientInfo> &aLoadingClientInfo,
     const Maybe<ServiceWorkerDescriptor> &aController, uint32_t aSecurityFlags,
     uint32_t aContentPolicyType, nsIChannel **aResult) {
   return NewChannelFromURIWithProxyFlagsInternal(
@@ -919,18 +919,18 @@ nsresult nsIOService::NewChannelFromURIW
 
   uint32_t protoFlags;
   rv = handler->DoGetProtocolFlags(aURI, &protoFlags);
   if (NS_FAILED(rv)) return rv;
 
   nsCOMPtr<nsIChannel> channel;
   nsCOMPtr<nsIProxiedProtocolHandler> pph = do_QueryInterface(handler);
   if (pph) {
-    rv = pph->NewProxiedChannel2(aURI, nullptr, aProxyFlags, aProxyURI,
-                                 aLoadInfo, getter_AddRefs(channel));
+    rv = pph->NewProxiedChannel(aURI, nullptr, aProxyFlags, aProxyURI,
+                                aLoadInfo, getter_AddRefs(channel));
   } else {
     rv = handler->NewChannel(aURI, aLoadInfo, getter_AddRefs(channel));
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Make sure that all the individual protocolhandlers attach a loadInfo.
   if (aLoadInfo) {
     // make sure we have the same instance of loadInfo on the newly created
@@ -971,43 +971,43 @@ nsresult nsIOService::NewChannelFromURIW
     }
   }
 
   channel.forget(result);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsIOService::NewChannelFromURIWithProxyFlags2(
+nsIOService::NewChannelFromURIWithProxyFlags(
     nsIURI *aURI, nsIURI *aProxyURI, uint32_t aProxyFlags,
     nsINode *aLoadingNode, nsIPrincipal *aLoadingPrincipal,
     nsIPrincipal *aTriggeringPrincipal, uint32_t aSecurityFlags,
     uint32_t aContentPolicyType, nsIChannel **result) {
   return NewChannelFromURIWithProxyFlagsInternal(
       aURI, aProxyURI, aProxyFlags, aLoadingNode, aLoadingPrincipal,
       aTriggeringPrincipal, Maybe<ClientInfo>(),
       Maybe<ServiceWorkerDescriptor>(), aSecurityFlags, aContentPolicyType,
       result);
 }
 
 NS_IMETHODIMP
-nsIOService::NewChannel2(const nsACString &aSpec, const char *aCharset,
-                         nsIURI *aBaseURI, nsINode *aLoadingNode,
-                         nsIPrincipal *aLoadingPrincipal,
-                         nsIPrincipal *aTriggeringPrincipal,
-                         uint32_t aSecurityFlags, uint32_t aContentPolicyType,
-                         nsIChannel **result) {
+nsIOService::NewChannel(const nsACString &aSpec, const char *aCharset,
+                        nsIURI *aBaseURI, nsINode *aLoadingNode,
+                        nsIPrincipal *aLoadingPrincipal,
+                        nsIPrincipal *aTriggeringPrincipal,
+                        uint32_t aSecurityFlags, uint32_t aContentPolicyType,
+                        nsIChannel **result) {
   nsresult rv;
   nsCOMPtr<nsIURI> uri;
   rv = NewURI(aSpec, aCharset, aBaseURI, getter_AddRefs(uri));
   if (NS_FAILED(rv)) return rv;
 
-  return NewChannelFromURI2(uri, aLoadingNode, aLoadingPrincipal,
-                            aTriggeringPrincipal, aSecurityFlags,
-                            aContentPolicyType, result);
+  return NewChannelFromURI(uri, aLoadingNode, aLoadingPrincipal,
+                           aTriggeringPrincipal, aSecurityFlags,
+                           aContentPolicyType, result);
 }
 
 bool nsIOService::IsLinkUp() {
   InitializeNetworkLinkService();
 
   if (!mNetworkLinkService) {
     // We cannot decide, assume the link is up
     return true;
@@ -1758,23 +1758,23 @@ nsresult nsIOService::SpeculativeConnect
 
   // dummy channel used to create a TCP connection.
   // we perform security checks on the *real* channel, responsible
   // for any network loads. this real channel just checks the TCP
   // pool if there is an available connection created by the
   // channel we create underneath - hence it's safe to use
   // the systemPrincipal as the loadingPrincipal for this channel.
   nsCOMPtr<nsIChannel> channel;
-  rv = NewChannelFromURI2(aURI,
-                          nullptr,  // aLoadingNode,
-                          loadingPrincipal,
-                          nullptr,  // aTriggeringPrincipal,
-                          nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                          nsIContentPolicy::TYPE_SPECULATIVE,
-                          getter_AddRefs(channel));
+  rv = NewChannelFromURI(aURI,
+                         nullptr,  // aLoadingNode,
+                         loadingPrincipal,
+                         nullptr,  // aTriggeringPrincipal,
+                         nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                         nsIContentPolicy::TYPE_SPECULATIVE,
+                         getter_AddRefs(channel));
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (aAnonymous) {
     nsLoadFlags loadFlags = 0;
     channel->GetLoadFlags(&loadFlags);
     loadFlags |= nsIRequest::LOAD_ANONYMOUS;
     channel->SetLoadFlags(loadFlags);
   }
--- a/netwerk/base/nsIProxiedProtocolHandler.idl
+++ b/netwerk/base/nsIProxiedProtocolHandler.idl
@@ -24,32 +24,13 @@ interface nsIProxiedProtocolHandler : ns
      * @param proxyURI used if the proxy is later determined from
      *        nsIProtocolProxyService::asyncResolve with this as the proxyURI name.
      *        Generally this is the same as uri (or null which has the same
      *        effect), except in the case of websockets which wants to bootstrap
      *        to an http:// channel but make its proxy determination based on
      *        a ws:// uri.
      * @param aLoadInfo used to evaluate who initated the resource request.
      */
-    nsIChannel newProxiedChannel2(in nsIURI uri, in nsIProxyInfo proxyInfo,
-                                  in unsigned long proxyResolveFlags,
-                                  in nsIURI proxyURI,
-                                  in nsILoadInfo aLoadInfo);
-
-    /** Create a new channel with the given proxyInfo
-     *
-     * @param uri the channel uri
-     * @param proxyInfo any proxy information that has already been determined,
-     *        or null if channel should later determine the proxy on its own using
-     *        proxyResolveFlags/proxyURI
-     * @param proxyResolveFlags used if the proxy is later determined
-     *        from nsIProtocolProxyService::asyncResolve
-     * @param proxyURI used if the proxy is later determined from
-     *        nsIProtocolProxyService::asyncResolve with this as the proxyURI name.
-     *        Generally this is the same as uri (or null which has the same
-     *        effect), except in the case of websockets which wants to bootstrap
-     *        to an http:// channel but make its proxy determination based on
-     *        a ws:// uri.
-     */
     nsIChannel newProxiedChannel(in nsIURI uri, in nsIProxyInfo proxyInfo,
                                  in unsigned long proxyResolveFlags,
-                                 in nsIURI proxyURI);
+                                 in nsIURI proxyURI,
+                                 in nsILoadInfo aLoadInfo);
 };
--- a/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
+++ b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
@@ -1884,17 +1884,17 @@ static nsresult CreateHTTPProxiedChannel
   if (NS_FAILED(rv)) return rv;
 
   nsCOMPtr<nsIURI> uri;
   channel->GetURI(getter_AddRefs(uri));
 
   nsCOMPtr<nsILoadInfo> loadInfo;
   channel->GetLoadInfo(getter_AddRefs(loadInfo));
 
-  return pph->NewProxiedChannel2(uri, pi, 0, nullptr, loadInfo, newChannel);
+  return pph->NewProxiedChannel(uri, pi, 0, nullptr, loadInfo, newChannel);
 }
 
 NS_IMETHODIMP
 nsFtpState::OnProxyAvailable(nsICancelable *request, nsIChannel *channel,
                              nsIProxyInfo *pi, nsresult status) {
   mProxyRequest = nullptr;
 
   // failed status code just implies DIRECT processing
--- a/netwerk/protocol/ftp/nsFtpProtocolHandler.cpp
+++ b/netwerk/protocol/ftp/nsFtpProtocolHandler.cpp
@@ -168,25 +168,25 @@ nsFtpProtocolHandler::NewURI(const nsACS
                               nsIStandardURL::URLTYPE_AUTHORITY, 21,
                               nsCString(aSpec), aCharset, base, nullptr))
       .Finalize(result);
 }
 
 NS_IMETHODIMP
 nsFtpProtocolHandler::NewChannel(nsIURI *url, nsILoadInfo *aLoadInfo,
                                  nsIChannel **result) {
-  return NewProxiedChannel2(url, nullptr, 0, nullptr, aLoadInfo, result);
+  return NewProxiedChannel(url, nullptr, 0, nullptr, aLoadInfo, result);
 }
 
 NS_IMETHODIMP
-nsFtpProtocolHandler::NewProxiedChannel2(nsIURI *uri, nsIProxyInfo *proxyInfo,
-                                         uint32_t proxyResolveFlags,
-                                         nsIURI *proxyURI,
-                                         nsILoadInfo *aLoadInfo,
-                                         nsIChannel **result) {
+nsFtpProtocolHandler::NewProxiedChannel(nsIURI *uri, nsIProxyInfo *proxyInfo,
+                                        uint32_t proxyResolveFlags,
+                                        nsIURI *proxyURI,
+                                        nsILoadInfo *aLoadInfo,
+                                        nsIChannel **result) {
   NS_ENSURE_ARG_POINTER(uri);
   RefPtr<nsBaseChannel> channel;
   if (IsNeckoChild())
     channel = new FTPChannelChild(uri);
   else
     channel = new nsFtpChannel(uri, proxyInfo);
 
   nsresult rv = channel->Init();
@@ -200,24 +200,16 @@ nsFtpProtocolHandler::NewProxiedChannel2
     return rv;
   }
 
   channel.forget(result);
   return rv;
 }
 
 NS_IMETHODIMP
-nsFtpProtocolHandler::NewProxiedChannel(nsIURI *uri, nsIProxyInfo *proxyInfo,
-                                        uint32_t proxyResolveFlags,
-                                        nsIURI *proxyURI, nsIChannel **result) {
-  return NewProxiedChannel2(uri, proxyInfo, proxyResolveFlags, proxyURI,
-                            nullptr /*loadinfo*/, result);
-}
-
-NS_IMETHODIMP
 nsFtpProtocolHandler::AllowPort(int32_t port, const char *scheme,
                                 bool *_retval) {
   *_retval = (port == 21 || port == 22);
   return NS_OK;
 }
 
 // connection cache methods
 
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -3114,18 +3114,18 @@ nsresult nsHttpChannel::OpenRedirectChan
   return NS_OK;
 }
 
 nsresult nsHttpChannel::AsyncDoReplaceWithProxy(nsIProxyInfo *pi) {
   LOG(("nsHttpChannel::AsyncDoReplaceWithProxy [this=%p pi=%p]", this, pi));
   nsresult rv;
 
   nsCOMPtr<nsIChannel> newChannel;
-  rv = gHttpHandler->NewProxiedChannel2(mURI, pi, mProxyResolveFlags, mProxyURI,
-                                        mLoadInfo, getter_AddRefs(newChannel));
+  rv = gHttpHandler->NewProxiedChannel(mURI, pi, mProxyResolveFlags, mProxyURI,
+                                       mLoadInfo, getter_AddRefs(newChannel));
   if (NS_FAILED(rv)) return rv;
 
   uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL;
 
   rv = SetupReplacementChannel(mURI, newChannel, true, flags);
   if (NS_FAILED(rv)) return rv;
 
   // Inform consumers about this fake redirect
--- a/netwerk/protocol/http/nsHttpHandler.cpp
+++ b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -2042,34 +2042,34 @@ nsHttpHandler::NewChannel(nsIURI *uri, n
     rv = uri->SchemeIs("https", &isHttps);
     if (NS_FAILED(rv)) return rv;
     if (!isHttps) {
       NS_WARNING("Invalid URI scheme");
       return NS_ERROR_UNEXPECTED;
     }
   }
 
-  return NewProxiedChannel2(uri, nullptr, 0, nullptr, aLoadInfo, result);
+  return NewProxiedChannel(uri, nullptr, 0, nullptr, aLoadInfo, result);
 }
 
 NS_IMETHODIMP
 nsHttpHandler::AllowPort(int32_t port, const char *scheme, bool *_retval) {
   // don't override anything.
   *_retval = false;
   return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsHttpHandler::nsIProxiedProtocolHandler
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
-nsHttpHandler::NewProxiedChannel2(nsIURI *uri, nsIProxyInfo *givenProxyInfo,
-                                  uint32_t proxyResolveFlags, nsIURI *proxyURI,
-                                  nsILoadInfo *aLoadInfo, nsIChannel **result) {
+nsHttpHandler::NewProxiedChannel(nsIURI *uri, nsIProxyInfo *givenProxyInfo,
+                                 uint32_t proxyResolveFlags, nsIURI *proxyURI,
+                                 nsILoadInfo *aLoadInfo, nsIChannel **result) {
   RefPtr<HttpBaseChannel> httpChannel;
 
   LOG(("nsHttpHandler::NewProxiedChannel [proxyInfo=%p]\n", givenProxyInfo));
 
 #ifdef MOZ_TASK_TRACER
   if (tasktracer::IsStartLogging()) {
     nsAutoCString urispec;
     uri->GetSpec(urispec);
@@ -2122,24 +2122,16 @@ nsHttpHandler::NewProxiedChannel2(nsIURI
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   httpChannel.forget(result);
   return NS_OK;
 }
 
-NS_IMETHODIMP
-nsHttpHandler::NewProxiedChannel(nsIURI *uri, nsIProxyInfo *givenProxyInfo,
-                                 uint32_t proxyResolveFlags, nsIURI *proxyURI,
-                                 nsIChannel **result) {
-  return NewProxiedChannel2(uri, givenProxyInfo, proxyResolveFlags, proxyURI,
-                            nullptr, result);
-}
-
 //-----------------------------------------------------------------------------
 // nsHttpHandler::nsIHttpProtocolHandler
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpHandler::GetUserAgent(nsACString &value) {
   value = UserAgent();
   return NS_OK;
--- a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
+++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
@@ -55,25 +55,25 @@ nsresult nsViewSourceChannel::Init(nsIUR
   if (NS_FAILED(rv)) return rv;
 
   // prevent viewing source of javascript URIs (see bug 204779)
   if (scheme.EqualsLiteral("javascript")) {
     NS_WARNING("blocking view-source:javascript:");
     return NS_ERROR_INVALID_ARG;
   }
 
-  // This function is called from within nsViewSourceHandler::NewChannel2
+  // This function is called from within nsViewSourceHandler::NewChannel
   // and sets the right loadInfo right after returning from this function.
   // Until then we follow the principal of least privilege and use
   // nullPrincipal as the loadingPrincipal and the least permissive
   // securityflag.
   nsCOMPtr<nsIPrincipal> nullPrincipal =
       mozilla::NullPrincipal::CreateWithoutOriginAttributes();
 
-  rv = pService->NewChannel2(
+  rv = pService->NewChannel(
       path,
       nullptr,  // aOriginCharset
       nullptr,  // aCharSet
       nullptr,  // aLoadingNode
       nullPrincipal,
       nullptr,  // aTriggeringPrincipal
       nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
       nsIContentPolicy::TYPE_OTHER, getter_AddRefs(mChannel));
--- a/netwerk/protocol/websocket/WebSocketChannel.cpp
+++ b/netwerk/protocol/websocket/WebSocketChannel.cpp
@@ -3382,17 +3382,17 @@ WebSocketChannel::AsyncOpen(nsIURI *aURI
   ioService = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
   if (NS_FAILED(rv)) {
     NS_WARNING("unable to continue without io service");
     return rv;
   }
 
   // Ideally we'd call newChannelFromURIWithLoadInfo here, but that doesn't
   // allow setting proxy uri/flags
-  rv = ioService->NewChannelFromURIWithProxyFlags2(
+  rv = ioService->NewChannelFromURIWithProxyFlags(
       localURI, mURI,
       nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY |
           nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL,
       mLoadInfo->LoadingNode(), mLoadInfo->LoadingPrincipal(),
       mLoadInfo->TriggeringPrincipal(), mLoadInfo->GetSecurityFlags(),
       mLoadInfo->InternalContentPolicyType(), getter_AddRefs(localChannel));
   NS_ENSURE_SUCCESS(rv, rv);
 
--- a/netwerk/test/unit/test_NetUtil.js
+++ b/netwerk/test/unit/test_NetUtil.js
@@ -508,65 +508,65 @@ function test_newChannel_no_specifier()
 
 function test_newChannel_with_string()
 {
   const TEST_SPEC = "http://mozilla.org";
 
   // Check that we get the same URI back from channel the IO service creates and
   // the channel the utility method creates.
   let ios = Services.io
-  let iosChannel = ios.newChannel2(TEST_SPEC,
-                                   null,
-                                   null,
-                                   null,      // aLoadingNode
-                                   Services.scriptSecurityManager.getSystemPrincipal(),
-                                   null,      // aTriggeringPrincipal
-                                   Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                   Ci.nsIContentPolicy.TYPE_OTHER);
+  let iosChannel = ios.newChannel(TEST_SPEC,
+                                  null,
+                                  null,
+                                  null,      // aLoadingNode
+                                  Services.scriptSecurityManager.getSystemPrincipal(),
+                                  null,      // aTriggeringPrincipal
+                                  Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                  Ci.nsIContentPolicy.TYPE_OTHER);
   let NetUtilChannel = NetUtil.newChannel({
     uri: TEST_SPEC,
     loadUsingSystemPrincipal: true
   });
   Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI));
 
   run_next_test();
 }
 
 function test_newChannel_with_nsIURI()
 {
   const TEST_SPEC = "http://mozilla.org";
 
   // Check that we get the same URI back from channel the IO service creates and
   // the channel the utility method creates.
   let uri = NetUtil.newURI(TEST_SPEC);
-  let iosChannel = Services.io.newChannelFromURI2(uri,
-                                                  null,      // aLoadingNode
-                                                  Services.scriptSecurityManager.getSystemPrincipal(),
-                                                  null,      // aTriggeringPrincipal
-                                                  Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                                  Ci.nsIContentPolicy.TYPE_OTHER);
+  let iosChannel = Services.io.newChannelFromURI(uri,
+                                                 null,      // aLoadingNode
+                                                 Services.scriptSecurityManager.getSystemPrincipal(),
+                                                 null,      // aTriggeringPrincipal
+                                                 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                                 Ci.nsIContentPolicy.TYPE_OTHER);
   let NetUtilChannel = NetUtil.newChannel({
     uri: uri,
     loadUsingSystemPrincipal: true
   });
   Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI));
 
   run_next_test();
 }
 
 function test_newChannel_with_options()
 {
   let uri = "data:text/plain,";
 
-  let iosChannel = Services.io.newChannelFromURI2(NetUtil.newURI(uri),
-                                                  null,      // aLoadingNode
-                                                  Services.scriptSecurityManager.getSystemPrincipal(),
-                                                  null,      // aTriggeringPrincipal
-                                                  Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                                  Ci.nsIContentPolicy.TYPE_OTHER);
+  let iosChannel = Services.io.newChannelFromURI(NetUtil.newURI(uri),
+                                                 null,      // aLoadingNode
+                                                 Services.scriptSecurityManager.getSystemPrincipal(),
+                                                 null,      // aTriggeringPrincipal
+                                                 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                                 Ci.nsIContentPolicy.TYPE_OTHER);
 
   function checkEqualToIOSChannel(channel) {
     Assert.ok(iosChannel.URI.equals(channel.URI));  
   }
 
   checkEqualToIOSChannel(NetUtil.newChannel({
     uri,
     loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
--- a/netwerk/test/unit/test_bug1177909.js
+++ b/netwerk/test/unit/test_bug1177909.js
@@ -103,24 +103,24 @@ add_task(async function testDirectProxy(
                    .finalize();
   let uri = proxyURI.mutate()
                     .setScheme("https")
                     .finalize();
 
   let ioService = Cc["@mozilla.org/network/io-service;1"].
                     getService(Ci.nsIIOService);
   let chan = ioService.
-    newChannelFromURIWithProxyFlags2(uri,
-                                     proxyURI,
-                                     0,
-                                     null,
-                                     Services.scriptSecurityManager.getSystemPrincipal(),
-                                     null,
-                                     Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                     Ci.nsIContentPolicy.TYPE_OTHER);
+    newChannelFromURIWithProxyFlags(uri,
+                                    proxyURI,
+                                    0,
+                                    null,
+                                    Services.scriptSecurityManager.getSystemPrincipal(),
+                                    null,
+                                    Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                    Ci.nsIContentPolicy.TYPE_OTHER);
 
   let pi = await TestProxyType(chan, 0);
   equal(pi, null, "Expected proxy host to be null");
 });
 
 add_task(async function testWebSocketProxy() {
   // Do what |WebSocketChannel::AsyncOpen| do
   let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"]
@@ -132,22 +132,22 @@ add_task(async function testWebSocketPro
                     .finalize();
 
   let proxyFlags = Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY |
                    Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
 
   let ioService = Cc["@mozilla.org/network/io-service;1"].
                     getService(Ci.nsIIOService);
   let chan = ioService.
-    newChannelFromURIWithProxyFlags2(uri,
-                                     proxyURI,
-                                     proxyFlags,
-                                     null,
-                                     Services.scriptSecurityManager.getSystemPrincipal(),
-                                     null,
-                                     Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                                     Ci.nsIContentPolicy.TYPE_OTHER);
+    newChannelFromURIWithProxyFlags(uri,
+                                    proxyURI,
+                                    proxyFlags,
+                                    null,
+                                    Services.scriptSecurityManager.getSystemPrincipal(),
+                                    null,
+                                    Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                    Ci.nsIContentPolicy.TYPE_OTHER);
 
   let pi = await TestProxyType(chan, proxyFlags);
   equal(pi.host, "localhost", "Expected proxy host to be localhost");
   equal(pi.port, 8080, "Expected proxy port to be 8080");
   equal(pi.type, "http", "Expected proxy type to be http");
 });
--- a/security/manager/ssl/nsNSSCallbacks.cpp
+++ b/security/manager/ssl/nsNSSCallbacks.cpp
@@ -234,22 +234,22 @@ OCSPRequest::Run() {
     return NotifyDone(rv, lock);
   }
 
   if (pps->GetIsPACLoading()) {
     return NotifyDone(NS_ERROR_FAILURE, lock);
   }
 
   nsCOMPtr<nsIChannel> channel;
-  rv = ios->NewChannel2(mAIALocation, nullptr, nullptr,
-                        nullptr,  // aLoadingNode
-                        nsContentUtils::GetSystemPrincipal(),
-                        nullptr,  // aTriggeringPrincipal
-                        nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-                        nsIContentPolicy::TYPE_OTHER, getter_AddRefs(channel));
+  rv = ios->NewChannel(mAIALocation, nullptr, nullptr,
+                       nullptr,  // aLoadingNode
+                       nsContentUtils::GetSystemPrincipal(),
+                       nullptr,  // aTriggeringPrincipal
+                       nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                       nsIContentPolicy::TYPE_OTHER, getter_AddRefs(channel));
   if (NS_FAILED(rv)) {
     return NotifyDone(rv, lock);
   }
 
   // Security operations scheduled through normal HTTP channels are given
   // high priority to accommodate real time OCSP transactions.
   nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(channel);
   if (priorityChannel) {
--- a/security/nss/TAG-INFO
+++ b/security/nss/TAG-INFO
@@ -1,1 +1,1 @@
-b7713856ebf2
+1f04eea8834a
--- a/security/nss/automation/abi-check/expected-report-libssl3.so.txt
+++ b/security/nss/automation/abi-check/expected-report-libssl3.so.txt
@@ -0,0 +1,20 @@
+
+2 functions with some indirect sub-type change:
+
+  [C]'function SECStatus SSL_GetCipherSuiteInfo(PRUint16, SSLCipherSuiteInfo*, PRUintn)' at sslinfo.c:326:1 has some indirect sub-type changes:
+    parameter 2 of type 'SSLCipherSuiteInfo*' has sub-type changes:
+      in pointed to type 'typedef SSLCipherSuiteInfo' at sslt.h:433:1:
+        underlying type 'struct SSLCipherSuiteInfoStr' at sslt.h:366:1 changed:
+          type size changed from 768 to 832 (in bits)
+          1 data member insertion:
+            'SSLHashType SSLCipherSuiteInfoStr::kdfHash', at offset 768 (in bits) at sslt.h:429:1
+
+  [C]'function SECStatus SSL_GetPreliminaryChannelInfo(PRFileDesc*, SSLPreliminaryChannelInfo*, PRUintn)' at sslinfo.c:111:1 has some indirect sub-type changes:
+    parameter 2 of type 'SSLPreliminaryChannelInfo*' has sub-type changes:
+      in pointed to type 'typedef SSLPreliminaryChannelInfo' at sslt.h:379:1:
+        underlying type 'struct SSLPreliminaryChannelInfoStr' at sslt.h:333:1 changed:
+          type size changed from 160 to 192 (in bits)
+          1 data member insertion:
+            'PRUint16 SSLPreliminaryChannelInfoStr::zeroRttCipherSuite', at offset 160 (in bits) at sslt.h:375:1
+
+
--- a/security/nss/cmd/manifest.mn
+++ b/security/nss/cmd/manifest.mn
@@ -51,16 +51,17 @@ NSS_SRCDIRS = \
  ocspclnt  \
  ocspresp \
  oidcalc  \
  p7content  \
  p7env  \
  p7sign  \
  p7verify  \
  pk12util \
+ pk11importtest \
  pk11ectest \
  pk11gcmtest \
  pk11mode \
  pk1sign  \
  pp  \
  pwdecrypt \
  rsaperf \
  rsapoptst \
new file mode 100644
--- /dev/null
+++ b/security/nss/cmd/pk11importtest/Makefile
@@ -0,0 +1,43 @@
+#! gmake
+#
+# 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/.
+
+#######################################################################
+# (1) Include initial platform-independent assignments (MANDATORY).   #
+#######################################################################
+
+include manifest.mn
+
+#######################################################################
+# (2) Include "global" configuration information. (OPTIONAL)          #
+#######################################################################
+
+include $(CORE_DEPTH)/coreconf/config.mk
+
+#######################################################################
+# (3) Include "component" configuration information. (OPTIONAL)       #
+#######################################################################
+
+#######################################################################
+# (4) Include "local" platform-dependent assignments (OPTIONAL).      #
+#######################################################################
+
+include ../platlibs.mk
+
+#######################################################################
+# (5) Execute "global" rules. (OPTIONAL)                              #
+#######################################################################
+
+include $(CORE_DEPTH)/coreconf/rules.mk
+
+#######################################################################
+# (6) Execute "component" rules. (OPTIONAL)                           #
+#######################################################################
+
+#######################################################################
+# (7) Execute "local" rules. (OPTIONAL).                              #
+#######################################################################
+
+include ../platrules.mk
new file mode 100644
--- /dev/null
+++ b/security/nss/cmd/pk11importtest/manifest.mn
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+CORE_DEPTH = ../..
+
+MODULE = nss
+
+CSRCS = pk11importtest.c \
+        $(NULL)
+
+REQUIRES = seccmd
+
+PROGRAM = pk11importtest
+
new file mode 100644
--- /dev/null
+++ b/security/nss/cmd/pk11importtest/pk11importtest.c
@@ -0,0 +1,406 @@
+/* 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/. */
+
+#include "secutil.h"
+#include "secmod.h"
+#include "cert.h"
+#include "secoid.h"
+#include "nss.h"
+#include "pk11pub.h"
+#include "pk11pqg.h"
+
+/* NSPR 2.0 header files */
+#include "prinit.h"
+#include "prprf.h"
+#include "prsystem.h"
+#include "prmem.h"
+/* Portable layer header files */
+#include "plstr.h"
+
+SECOidData *
+getCurveFromString(char *curve_name)
+{
+    SECOidTag tag = SEC_OID_SECG_EC_SECP256R1;
+
+    if (PORT_Strcasecmp(curve_name, "NISTP256") == 0) {
+    } else if (PORT_Strcasecmp(curve_name, "NISTP384") == 0) {
+        tag = SEC_OID_SECG_EC_SECP384R1;
+    } else if (PORT_Strcasecmp(curve_name, "NISTP521") == 0) {
+        tag = SEC_OID_SECG_EC_SECP521R1;
+    } else if (PORT_Strcasecmp(curve_name, "Curve25519") == 0) {
+        tag = SEC_OID_CURVE25519;
+    }
+    return SECOID_FindOIDByTag(tag);
+}
+
+void
+dumpItem(const char *label, const SECItem *item)
+{
+    int i;
+    printf("%s = [%d bytes] {", label, item->len);
+    for (i = 0; i < item->len; i++) {
+        if ((i & 0xf) == 0)
+            printf("\n    ");
+        else
+            printf(", ");
+        printf("%02x", item->data[i]);
+    }
+    printf("};\n");
+}
+
+SECStatus
+handleEncryptedPrivateImportTest(char *progName, PK11SlotInfo *slot,
+                                 char *testname, CK_MECHANISM_TYPE genMech, void *params, void *pwArgs)
+{
+    SECStatus rv = SECSuccess;
+    SECItem privID = { 0 };
+    SECItem pubID = { 0 };
+    SECItem pubValue = { 0 };
+    SECItem pbePwItem = { 0 };
+    SECItem nickname = { 0 };
+    SECItem token = { 0 };
+    SECKEYPublicKey *pubKey = NULL;
+    SECKEYPrivateKey *privKey = NULL;
+    PK11GenericObject *objs = NULL;
+    PK11GenericObject *obj = NULL;
+    SECKEYEncryptedPrivateKeyInfo *epki = NULL;
+    PRBool keyFound = 0;
+    KeyType keyType;
+
+    fprintf(stderr, "Testing %s PrivateKeyImport ***********************\n",
+            testname);
+
+    /* generate a temp key */
+    privKey = PK11_GenerateKeyPair(slot, genMech, params, &pubKey,
+                                   PR_FALSE, PR_TRUE, pwArgs);
+    if (privKey == NULL) {
+        SECU_PrintError(progName, "PK11_GenerateKeyPair Failed");
+        goto cleanup;
+    }
+
+    /* wrap the temp key */
+    pbePwItem.data = (unsigned char *)"pw";
+    pbePwItem.len = 2;
+    epki = PK11_ExportEncryptedPrivKeyInfo(slot, SEC_OID_AES_256_CBC,
+                                           &pbePwItem, privKey, 1, NULL);
+    if (epki == NULL) {
+        SECU_PrintError(progName, "PK11_ExportEncryptedPrivKeyInfo Failed");
+        goto cleanup;
+    }
+
+    /* Save the public value, which we will need on import */
+    keyType = pubKey->keyType;
+    switch (keyType) {
+        case rsaKey:
+            SECITEM_CopyItem(NULL, &pubValue, &pubKey->u.rsa.modulus);
+            break;
+        case dhKey:
+            SECITEM_CopyItem(NULL, &pubValue, &pubKey->u.dh.publicValue);
+            break;
+        case dsaKey:
+            SECITEM_CopyItem(NULL, &pubValue, &pubKey->u.dsa.publicValue);
+            break;
+        case ecKey:
+            SECITEM_CopyItem(NULL, &pubValue, &pubKey->u.ec.publicValue);
+            break;
+        default:
+            fprintf(stderr, "Unknown keytype = %d\n", keyType);
+            goto cleanup;
+    }
+    if (pubValue.data == NULL) {
+        SECU_PrintError(progName, "Unable to allocate memory");
+        goto cleanup;
+    }
+    dumpItem("pubValue", &pubValue);
+
+    /* when Asymetric keys represent session keys, those session keys are
+     * destroyed when we destroy the Asymetric key representations */
+    SECKEY_DestroyPublicKey(pubKey);
+    pubKey = NULL;
+    SECKEY_DestroyPrivateKey(privKey);
+    privKey = NULL;
+
+    /* unwrap the temp key as a perm */
+    nickname.data = (unsigned char *)"testKey";
+    nickname.len = sizeof("testKey");
+    rv = PK11_ImportEncryptedPrivateKeyInfoAndReturnKey(
+        slot, epki, &pbePwItem, &nickname, &pubValue,
+        PR_TRUE, PR_TRUE, keyType, 0, &privKey, NULL);
+    if (rv != SECSuccess) {
+        SECU_PrintError(progName, "PK11_ImportEncryptedPrivateKeyInfo Failed");
+        goto cleanup;
+    }
+
+    /* verify the public key exists */
+    rv = PK11_ReadRawAttribute(PK11_TypePrivKey, privKey, CKA_ID, &privID);
+    if (rv != SECSuccess) {
+        SECU_PrintError(progName,
+                        "Couldn't read CKA_ID from pub key, checking next key");
+        goto cleanup;
+    }
+    dumpItem("privKey CKA_ID", &privID);
+    objs = PK11_FindGenericObjects(slot, CKO_PUBLIC_KEY);
+    for (obj = objs; obj; obj = PK11_GetNextGenericObject(obj)) {
+        rv = PK11_ReadRawAttribute(PK11_TypeGeneric, obj, CKA_ID, &pubID);
+        if (rv != SECSuccess) {
+            SECU_PrintError(progName,
+                            "Couldn't read CKA_ID from object, checking next key");
+            continue;
+        }
+        dumpItem("pubKey CKA_ID", &pubID);
+        if (!SECITEM_ItemsAreEqual(&privID, &pubID)) {
+            fprintf(stderr,
+                    "CKA_ID does not match priv key, checking next key\n");
+            SECITEM_FreeItem(&pubID, PR_FALSE);
+            continue;
+        }
+        SECITEM_FreeItem(&pubID, PR_FALSE);
+        rv = PK11_ReadRawAttribute(PK11_TypeGeneric, obj, CKA_TOKEN, &token);
+        if (rv == SECSuccess) {
+            if (token.len == 1) {
+                keyFound = token.data[0];
+            }
+            SECITEM_FreeItem(&token, PR_FALSE);
+        }
+        if (keyFound) {
+            printf("matching public key found\n");
+            break;
+        }
+        printf("Matching key was not a token key, checking next key\n");
+    }
+
+cleanup:
+    if (objs) {
+        PK11_DestroyGenericObjects(objs);
+    }
+    SECITEM_FreeItem(&pubValue, PR_FALSE);
+    SECITEM_FreeItem(&privID, PR_FALSE);
+    PORT_FreeArena(epki->arena, PR_TRUE);
+    SECKEY_DestroyPublicKey(pubKey);
+    SECKEY_DestroyPrivateKey(privKey);
+    fprintf(stderr, "%s PrivateKeyImport %s ***********************\n",
+            testname, keyFound ? "PASSED" : "FAILED");
+    return keyFound ? SECSuccess : SECFailure;
+}
+
+static const char *const usageInfo[] = {
+    "pk11import - test PK11_PrivateKeyImport()"
+    "Options:",
+    " -d certdir            directory containing cert database",
+    " -k keysize            size of the rsa, dh, and dsa key to test (default 1024)",
+    " -C ecc_curve          ecc curve (default )",
+    " -f pwFile             file to fetch the password from",
+    " -p pwString           password",
+    " -r                    skip rsa test",
+    " -D                    skip dsa test",
+    " -h                    skip dh test",
+    " -e                    skip ec test",
+};
+static int nUsageInfo = sizeof(usageInfo) / sizeof(char *);
+
+static void
+Usage(char *progName, FILE *outFile)
+{
+    int i;
+    fprintf(outFile, "Usage:  %s [ commands ] options\n", progName);
+    for (i = 0; i < nUsageInfo; i++)
+        fprintf(outFile, "%s\n", usageInfo[i]);
+    exit(-1);
+}
+
+enum {
+    opt_CertDir,
+    opt_KeySize,
+    opt_ECCurve,
+    opt_PWFile,
+    opt_PWString,
+    opt_NoRSA,
+    opt_NoDSA,
+    opt_NoEC,
+    opt_NoDH
+};
+
+static secuCommandFlag options[] =
+    {
+      { /* opt_CertDir          */ 'd', PR_TRUE, 0, PR_FALSE },
+      { /* opt_KeySize          */ 'k', PR_TRUE, 0, PR_FALSE },
+      { /* opt_ECCurve          */ 'C', PR_TRUE, 0, PR_FALSE },
+      { /* opt_PWFile           */ 'f', PR_TRUE, 0, PR_FALSE },
+      { /* opt_PWString         */ 'p', PR_TRUE, 0, PR_FALSE },
+      { /* opt_NORSA            */ 'r', PR_TRUE, 0, PR_FALSE },
+      { /* opt_NoDSA            */ 'D', PR_TRUE, 0, PR_FALSE },
+      { /* opt_NoDH             */ 'h', PR_TRUE, 0, PR_FALSE },
+      { /* opt_NoEC             */ 'e', PR_TRUE, 0, PR_FALSE },
+    };
+
+int
+main(int argc, char **argv)
+{
+    char *progName;
+    SECStatus rv;
+    secuCommand args;
+    PK11SlotInfo *slot = NULL;
+    PRBool failed = PR_FALSE;
+    secuPWData pwArgs = { PW_NONE, 0 };
+    PRBool doRSA = PR_TRUE;
+    PRBool doDSA = PR_TRUE;
+    PRBool doDH = PR_FALSE; /* NSS currently can't export wrapped DH keys */
+    PRBool doEC = PR_TRUE;
+    PQGParams *pqgParams = NULL;
+    int keySize;
+
+    args.numCommands = 0;
+    args.numOptions = sizeof(options) / sizeof(secuCommandFlag);
+    args.commands = NULL;
+    args.options = options;
+
+#ifdef XP_PC
+    progName = strrchr(argv[0], '\\');
+#else
+    progName = strrchr(argv[0], '/');
+#endif
+    progName = progName ? progName + 1 : argv[0];
+
+    rv = SECU_ParseCommandLine(argc, argv, progName, &args);
+    if (SECSuccess != rv) {
+        Usage(progName, stderr);
+    }
+
+    /*  Set the certdb directory (default is ~/.netscape) */
+    rv = NSS_InitReadWrite(SECU_ConfigDirectory(args.options[opt_CertDir].arg));
+    if (rv != SECSuccess) {
+        SECU_PrintPRandOSError(progName);
+        return 255;
+    }
+    PK11_SetPasswordFunc(SECU_GetModulePassword);
+
+    /* below here, goto cleanup */
+    SECU_RegisterDynamicOids();
+
+    /* handle the arguments */
+    if (args.options[opt_PWFile].arg) {
+        pwArgs.source = PW_FROMFILE;
+        pwArgs.data = args.options[opt_PWFile].arg;
+    }
+    if (args.options[opt_PWString].arg) {
+        pwArgs.source = PW_PLAINTEXT;
+        pwArgs.data = args.options[opt_PWString].arg;
+    }
+    if (args.options[opt_NoRSA].activated) {
+        doRSA = PR_FALSE;
+    }
+    if (args.options[opt_NoDSA].activated) {
+        doDSA = PR_FALSE;
+    }
+    if (args.options[opt_NoDH].activated) {
+        doDH = PR_FALSE;
+    }
+    if (args.options[opt_NoEC].activated) {
+        doEC = PR_FALSE;
+    }
+
+    slot = PK11_GetInternalKeySlot();
+    if (slot == NULL) {
+        SECU_PrintError(progName, "Couldn't find the internal key slot\n");
+        return 255;
+    }
+    rv = PK11_Authenticate(slot, PR_TRUE, &pwArgs);
+    if (rv != SECSuccess) {
+        SECU_PrintError(progName, "Failed to log into slot");
+        PK11_FreeSlot(slot);
+        return 255;
+    }
+
+    keySize = 1024;
+    if (args.options[opt_KeySize].activated &&
+        args.options[opt_KeySize].arg) {
+        keySize = atoi(args.options[opt_KeySize].arg);
+    }
+
+    if (doDSA || doDH) {
+        PQGVerify *pqgVfy;
+        rv = PK11_PQG_ParamGenV2(keySize, 0, keySize / 16, &pqgParams, &pqgVfy);
+        if (rv == SECSuccess) {
+            PK11_PQG_DestroyVerify(pqgVfy);
+        } else {
+            SECU_PrintError(progName,
+                            "PK11_PQG_ParamGenV2 failed, can't test DH or DSA");
+            doDSA = doDH = PR_FALSE;
+            failed = PR_TRUE;
+        }
+    }
+
+    if (doRSA) {
+        PK11RSAGenParams rsaParams;
+        rsaParams.keySizeInBits = keySize;
+        rsaParams.pe = 0x010001;
+        rv = handleEncryptedPrivateImportTest(progName, slot, "RSA",
+                                              CKM_RSA_PKCS_KEY_PAIR_GEN, &rsaParams, &pwArgs);
+        if (rv != SECSuccess) {
+            fprintf(stderr, "RSA Import Failed!\n");
+            failed = PR_TRUE;
+        }
+    }
+    if (doDSA) {
+        rv = handleEncryptedPrivateImportTest(progName, slot, "DSA",
+                                              CKM_DSA_KEY_PAIR_GEN, pqgParams, &pwArgs);
+        if (rv != SECSuccess) {
+            fprintf(stderr, "DSA Import Failed!\n");
+            failed = PR_TRUE;
+        }
+    }
+    if (doDH) {
+        SECKEYDHParams dhParams;
+        dhParams.prime = pqgParams->prime;
+        dhParams.base = pqgParams->base;
+        rv = handleEncryptedPrivateImportTest(progName, slot, "DH",
+                                              CKM_DH_PKCS_KEY_PAIR_GEN, &dhParams, &pwArgs);
+        if (rv != SECSuccess) {
+            fprintf(stderr, "DH Import Failed!\n");
+            failed = PR_TRUE;
+        }
+    }
+    if (doEC) {
+        SECKEYECParams ecParams;
+        SECOidData *curve = SECOID_FindOIDByTag(SEC_OID_SECG_EC_SECP256R1);
+        if (args.options[opt_ECCurve].activated &&
+            args.options[opt_ECCurve].arg) {
+            curve = getCurveFromString(args.options[opt_ECCurve].arg);
+        }
+        ecParams.data = PORT_Alloc(curve->oid.len + 2);
+        if (ecParams.data == NULL) {
+            rv = SECFailure;
+            goto ec_failed;
+        }
+        ecParams.data[0] = SEC_ASN1_OBJECT_ID;
+        ecParams.data[1] = (unsigned char)curve->oid.len;
+        PORT_Memcpy(&ecParams.data[2], curve->oid.data, curve->oid.len);
+        ecParams.len = curve->oid.len + 2;
+        rv = handleEncryptedPrivateImportTest(progName, slot, "ECC",
+                                              CKM_EC_KEY_PAIR_GEN, &ecParams, &pwArgs);
+        PORT_Free(ecParams.data);
+    ec_failed:
+        if (rv != SECSuccess) {
+            fprintf(stderr, "ECC Import Failed!\n");
+            failed = PR_TRUE;
+        }
+    }
+
+    if (pqgParams) {
+        PK11_PQG_DestroyParams(pqgParams);
+    }
+
+    if (slot) {
+        PK11_FreeSlot(slot);
+    }
+
+    rv = NSS_Shutdown();
+    if (rv != SECSuccess) {
+        fprintf(stderr, "Shutdown failed\n");
+        SECU_PrintPRandOSError(progName);
+        return 255;
+    }
+
+    return failed ? 1 : 0;
+}
new file mode 100644
--- /dev/null
+++ b/security/nss/cmd/pk11importtest/pk11importtest.gyp
@@ -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/.
+{
+  'includes': [
+    '../../coreconf/config.gypi',
+    '../../cmd/platlibs.gypi'
+  ],
+  'targets': [
+    {
+      'target_name': 'pk11importtest',
+      'type': 'executable',
+      'sources': [
+        'pk11importtest.c'
+      ],
+      'dependencies': [
+        '<(DEPTH)/exports.gyp:dbm_exports',
+        '<(DEPTH)/exports.gyp:nss_exports'
+      ]
+    }
+  ],
+  'variables': {
+    'module': 'nss'
+  }
+}
--- a/security/nss/coreconf/coreconf.dep
+++ b/security/nss/coreconf/coreconf.dep
@@ -5,9 +5,8 @@
 
 /*
  * A dummy header file that is a dependency for all the object files.
  * Used to force a full recompilation of NSS in Mozilla's Tinderbox
  * depend builds.  See comments in rules.mk.
  */
 
 #error "Do not include this header file."
-
--- a/security/nss/cpputil/nss_scoped_ptrs.h
+++ b/security/nss/cpputil/nss_scoped_ptrs.h
@@ -6,16 +6,17 @@
 
 #ifndef nss_scoped_ptrs_h__
 #define nss_scoped_ptrs_h__
 
 #include <memory>
 #include "cert.h"
 #include "keyhi.h"
 #include "p12.h"
+#include "pk11pqg.h"
 #include "pk11pub.h"
 #include "pkcs11uri.h"
 
 struct ScopedDelete {
   void operator()(CERTCertificate* cert) { CERT_DestroyCertificate(cert); }
   void operator()(CERTCertificateList* list) {
     CERT_DestroyCertificateList(list);
   }
@@ -36,16 +37,17 @@ struct ScopedDelete {
   void operator()(SECKEYPrivateKey* key) { SECKEY_DestroyPrivateKey(key); }
   void operator()(SECKEYPrivateKeyList* list) {
     SECKEY_DestroyPrivateKeyList(list);
   }
   void operator()(PK11URI* uri) { PK11URI_DestroyURI(uri); }
   void operator()(PLArenaPool* arena) { PORT_FreeArena(arena, PR_FALSE); }
   void operator()(PK11Context* context) { PK11_DestroyContext(context, true); }
   void operator()(PK11GenericObject* obj) { PK11_DestroyGenericObject(obj); }
+  void operator()(PQGParams* pqg) { PK11_PQG_DestroyParams(pqg); }
   void operator()(SEC_PKCS12DecoderContext* dcx) {
     SEC_PKCS12DecoderFinish(dcx);
   }
   void operator()(CERTDistNames* names) { CERT_FreeDistNames(names); }
 };
 
 template <class T>
 struct ScopedMaybeDelete {
@@ -61,25 +63,31 @@ struct ScopedMaybeDelete {
 
 SCOPED(CERTCertificate);
 SCOPED(CERTCertificateList);
 SCOPED(CERTCertList);
 SCOPED(CERTName);
 SCOPED(CERTSubjectPublicKeyInfo);
 SCOPED(PK11SlotInfo);
 SCOPED(PK11SymKey);
+SCOPED(PQGParams);
 SCOPED(PRFileDesc);
 SCOPED(SECAlgorithmID);
 SCOPED(SECKEYEncryptedPrivateKeyInfo);
 SCOPED(SECItem);
 SCOPED(SECKEYPublicKey);
 SCOPED(SECKEYPrivateKey);
 SCOPED(SECKEYPrivateKeyList);
 SCOPED(PK11URI);
 SCOPED(PLArenaPool);
 SCOPED(PK11Context);
 SCOPED(PK11GenericObject);
 SCOPED(SEC_PKCS12DecoderContext);
 SCOPED(CERTDistNames);
 
 #undef SCOPED
 
+struct StackSECItem : public SECItem {
+  StackSECItem() : SECItem({siBuffer, nullptr, 0}) {}
+  ~StackSECItem() { SECITEM_FreeItem(this, PR_FALSE); }
+};
+
 #endif  // nss_scoped_ptrs_h__
--- a/security/nss/cpputil/scoped_ptrs_util.h
+++ b/security/nss/cpputil/scoped_ptrs_util.h
@@ -28,12 +28,13 @@ struct ScopedMaybeDelete {
   }
 };
 
 #define SCOPED(x) typedef std::unique_ptr<x, ScopedMaybeDelete<x> > Scoped##x
 
 SCOPED(SECAlgorithmID);
 SCOPED(SECItem);
 SCOPED(PK11URI);
+SCOPED(PLArenaPool);
 
 #undef SCOPED
 
 #endif  // scoped_ptrs_util_h__
--- a/security/nss/cpputil/tls_parser.h
+++ b/security/nss/cpputil/tls_parser.h
@@ -75,16 +75,42 @@ static const uint8_t kTls13PskKe = 0;
 static const uint8_t kTls13PskDhKe = 1;
 static const uint8_t kTls13PskAuth = 0;
 static const uint8_t kTls13PskSignAuth = 1;
 
 inline std::ostream& operator<<(std::ostream& os, SSLProtocolVariant v) {
   return os << ((v == ssl_variant_stream) ? "TLS" : "DTLS");
 }
 
+inline std::ostream& operator<<(std::ostream& os, SSLContentType v) {
+  switch (v) {
+    case ssl_ct_change_cipher_spec:
+      return os << "CCS";
+    case ssl_ct_alert:
+      return os << "alert";
+    case ssl_ct_handshake:
+      return os << "handshake";
+    case ssl_ct_application_data:
+      return os << "application data";
+    case ssl_ct_ack:
+      return os << "ack";
+  }
+  return os << "UNKNOWN content type " << static_cast<int>(v);
+}
+
+inline std::ostream& operator<<(std::ostream& os, SSLSecretDirection v) {
+  switch (v) {
+    case ssl_secret_read:
+      return os << "read";
+    case ssl_secret_write:
+      return os << "write";
+  }
+  return os << "UNKNOWN secret direction " << static_cast<int>(v);
+}
+
 inline bool IsDtls(uint16_t version) { return (version & 0x8000) == 0x8000; }
 
 inline uint16_t NormalizeTlsVersion(uint16_t version) {
   if (version == 0xfeff) {
     return 0x0302;  // special: DTLS 1.0 == TLS 1.1
   }
   if (IsDtls(version)) {
     return (version ^ 0xffff) + 0x0201;
--- a/security/nss/gtests/common/gtests.cc
+++ b/security/nss/gtests/common/gtests.cc
@@ -5,17 +5,33 @@
 #include <cstdlib>
 
 #define GTEST_HAS_RTTI 0
 #include "gtest/gtest.h"
 
 int main(int argc, char **argv) {
   ::testing::InitGoogleTest(&argc, argv);
 
-  if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+  const char *workdir = "";
+  uint32_t flags = NSS_INIT_READONLY;
+
+  for (int i = 0; i < argc; i++) {
+    if (!strcmp(argv[i], "-d")) {
+      if (i + 1 >= argc) {
+        PR_fprintf(PR_STDERR, "Usage: %s [-d <dir> [-w]]\n", argv[0]);
+        exit(2);
+      }
+      workdir = argv[i + 1];
+      i++;
+    } else if (!strcmp(argv[i], "-w")) {
+      flags &= ~NSS_INIT_READONLY;
+    }
+  }
+
+  if (NSS_Initialize(workdir, "", "", SECMOD_DB, flags) != SECSuccess) {
     return 1;
   }
   if (NSS_SetDomesticPolicy() != SECSuccess) {
     return 1;
   }
   int rv = RUN_ALL_TESTS();
 
   if (NSS_Shutdown() != SECSuccess) {
--- a/security/nss/gtests/pk11_gtest/manifest.mn
+++ b/security/nss/gtests/pk11_gtest/manifest.mn
@@ -8,16 +8,17 @@ MODULE = nss
 
 CPPSRCS = \
       pk11_aeskeywrap_unittest.cc \
       pk11_chacha20poly1305_unittest.cc \
       pk11_curve25519_unittest.cc \
       pk11_ecdsa_unittest.cc \
       pk11_encrypt_derive_unittest.cc \
       pk11_export_unittest.cc \
+      pk11_import_unittest.cc \
       pk11_pbkdf2_unittest.cc \
       pk11_prf_unittest.cc \
       pk11_prng_unittest.cc \
       pk11_rsapkcs1_unittest.cc \
       pk11_rsapss_unittest.cc \
       pk11_der_private_key_import_unittest.cc \
       $(NULL)
 
@@ -28,9 +29,8 @@ INCLUDES += -I$(CORE_DEPTH)/gtests/googl
 REQUIRES = nspr nss libdbm gtest cpputil
 
 PROGRAM = pk11_gtest
 
 EXTRA_LIBS = $(DIST)/lib/$(LIB_PREFIX)gtest.$(LIB_SUFFIX) \
              $(DIST)/lib/$(LIB_PREFIX)cpputil.$(LIB_SUFFIX) \
              $(DIST)/lib/$(LIB_PREFIX)gtestutil.$(LIB_SUFFIX) \
              $(NULL)
-
--- a/security/nss/gtests/pk11_gtest/pk11_gtest.gyp
+++ b/security/nss/gtests/pk11_gtest/pk11_gtest.gyp
@@ -13,16 +13,17 @@
       'sources': [
         'pk11_aeskeywrap_unittest.cc',
         'pk11_aes_gcm_unittest.cc',
         'pk11_chacha20poly1305_unittest.cc',
         'pk11_cipherop_unittest.cc',
         'pk11_curve25519_unittest.cc',
         'pk11_ecdsa_unittest.cc',
         'pk11_encrypt_derive_unittest.cc',
+        'pk11_import_unittest.cc',
         'pk11_pbkdf2_unittest.cc',
         'pk11_prf_unittest.cc',
         'pk11_prng_unittest.cc',
         'pk11_rsapkcs1_unittest.cc',
         'pk11_rsapss_unittest.cc',
         'pk11_der_private_key_import_unittest.cc',
         '<(DEPTH)/gtests/common/gtests.cc'
       ],
--- a/security/nss/gtests/ssl_gtest/libssl_internals.c
+++ b/security/nss/gtests/ssl_gtest/libssl_internals.c
@@ -292,48 +292,16 @@ SECStatus SSLInt_AdvanceWriteSeqByAWindo
 
 SSLKEAType SSLInt_GetKEAType(SSLNamedGroup group) {
   const sslNamedGroupDef *groupDef = ssl_LookupNamedGroup(group);
   if (!groupDef) return ssl_kea_null;
 
   return groupDef->keaType;
 }
 
-SECStatus SSLInt_SetCipherSpecChangeFunc(PRFileDesc *fd,
-                                         sslCipherSpecChangedFunc func,
-                                         void *arg) {
-  sslSocket *ss;
-
-  ss = ssl_FindSocket(fd);
-  if (!ss) {
-    return SECFailure;
-  }
-
-  ss->ssl3.changedCipherSpecFunc = func;
-  ss->ssl3.changedCipherSpecArg = arg;
-
-  return SECSuccess;
-}
-
-PK11SymKey *SSLInt_CipherSpecToKey(const ssl3CipherSpec *spec) {
-  return spec->keyMaterial.key;
-}
-
-SSLCipherAlgorithm SSLInt_CipherSpecToAlgorithm(const ssl3CipherSpec *spec) {
-  return spec->cipherDef->calg;
-}
-
-const PRUint8 *SSLInt_CipherSpecToIv(const ssl3CipherSpec *spec) {
-  return spec->keyMaterial.iv;
-}
-
-PRUint16 SSLInt_CipherSpecToEpoch(const ssl3CipherSpec *spec) {
-  return spec->epoch;
-}
-
 void SSLInt_SetTicketLifetime(uint32_t lifetime) {
   ssl_ticket_lifetime = lifetime;
 }
 
 SECStatus SSLInt_SetSocketMaxEarlyDataSize(PRFileDesc *fd, uint32_t size) {
   sslSocket *ss;
 
   ss = ssl_FindSocket(fd);
@@ -355,21 +323,19 @@ SECStatus SSLInt_SetSocketMaxEarlyDataSi
 
   return SECSuccess;
 }
 
 void SSLInt_RolloverAntiReplay(void) {
   tls13_AntiReplayRollover(ssl_TimeUsec());
 }
 
-SECStatus SSLInt_GetEpochs(PRFileDesc *fd, PRUint16 *readEpoch,
-                           PRUint16 *writeEpoch) {
+SECStatus SSLInt_HasPendingHandshakeData(PRFileDesc *fd, PRBool *pending) {
   sslSocket *ss = ssl_FindSocket(fd);
-  if (!ss || !readEpoch || !writeEpoch) {
+  if (!ss) {
     return SECFailure;
   }
 
-  ssl_GetSpecReadLock(ss);
-  *readEpoch = ss->ssl3.crSpec->epoch;
-  *writeEpoch = ss->ssl3.cwSpec->epoch;
-  ssl_ReleaseSpecReadLock(ss);
+  ssl_GetSSL3HandshakeLock(ss);
+  *pending = ss->ssl3.hs.msg_body.len > 0;
+  ssl_ReleaseSSL3HandshakeLock(ss);
   return SECSuccess;
 }
--- a/security/nss/gtests/ssl_gtest/libssl_internals.h
+++ b/security/nss/gtests/ssl_gtest/libssl_internals.h
@@ -34,23 +34,14 @@ PRBool SSLInt_DamageServerHsTrafficSecre
 PRBool SSLInt_DamageEarlyTrafficSecret(PRFileDesc *fd);
 SECStatus SSLInt_Set0RttAlpn(PRFileDesc *fd, PRUint8 *data, unsigned int len);
 PRBool SSLInt_HasCertWithAuthType(PRFileDesc *fd, SSLAuthType authType);
 PRBool SSLInt_SendAlert(PRFileDesc *fd, uint8_t level, uint8_t type);
 SECStatus SSLInt_AdvanceWriteSeqNum(PRFileDesc *fd, PRUint64 to);
 SECStatus SSLInt_AdvanceReadSeqNum(PRFileDesc *fd, PRUint64 to);
 SECStatus SSLInt_AdvanceWriteSeqByAWindow(PRFileDesc *fd, PRInt32 extra);
 SSLKEAType SSLInt_GetKEAType(SSLNamedGroup group);
-SECStatus SSLInt_GetEpochs(PRFileDesc *fd, PRUint16 *readEpoch,
-                           PRUint16 *writeEpoch);
-
-SECStatus SSLInt_SetCipherSpecChangeFunc(PRFileDesc *fd,
-                                         sslCipherSpecChangedFunc func,
-                                         void *arg);
-PRUint16 SSLInt_CipherSpecToEpoch(const ssl3CipherSpec *spec);
-PK11SymKey *SSLInt_CipherSpecToKey(const ssl3CipherSpec *spec);
-SSLCipherAlgorithm SSLInt_CipherSpecToAlgorithm(const ssl3CipherSpec *spec);
-const PRUint8 *SSLInt_CipherSpecToIv(const ssl3CipherSpec *spec);
+SECStatus SSLInt_HasPendingHandshakeData(PRFileDesc *fd, PRBool *pending);
 void SSLInt_SetTicketLifetime(uint32_t lifetime);
 SECStatus SSLInt_SetSocketMaxEarlyDataSize(PRFileDesc *fd, uint32_t size);
 void SSLInt_RolloverAntiReplay(void);
 
 #endif  // ndef libssl_internals_h_
--- a/security/nss/gtests/ssl_gtest/manifest.mn
+++ b/security/nss/gtests/ssl_gtest/manifest.mn
@@ -31,16 +31,17 @@ CPPSRCS = \
       ssl_gather_unittest.cc \
       ssl_gtest.cc \
       ssl_hrr_unittest.cc \
       ssl_keylog_unittest.cc \
       ssl_keyupdate_unittest.cc \
       ssl_loopback_unittest.cc \
       ssl_misc_unittest.cc \
       ssl_record_unittest.cc \
+      ssl_recordsep_unittest.cc \
       ssl_recordsize_unittest.cc \
       ssl_resumption_unittest.cc \
       ssl_renegotiation_unittest.cc \
       ssl_skip_unittest.cc \
       ssl_staticrsa_unittest.cc \
       ssl_tls13compat_unittest.cc \
       ssl_v2_client_hello_unittest.cc \
       ssl_version_unittest.cc \
--- a/security/nss/gtests/ssl_gtest/ssl_auth_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/ssl_auth_unittest.cc
@@ -171,16 +171,331 @@ TEST_P(TlsConnectGenericPre13, ServerAut
 
 TEST_P(TlsConnectGeneric, ClientAuth) {
   client_->SetupClientAuth();
   server_->RequestClientAuth(true);
   Connect();
   CheckKeys();
 }
 
+class TlsCertificateRequestContextRecorder : public TlsHandshakeFilter {
+ public:
+  TlsCertificateRequestContextRecorder(const std::shared_ptr<TlsAgent>& a,
+                                       uint8_t handshake_type)
+      : TlsHandshakeFilter(a, {handshake_type}), buffer_(), filtered_(false) {
+    EnableDecryption();
+  }
+
+  bool filtered() const { return filtered_; }
+  const DataBuffer& buffer() const { return buffer_; }
+
+ protected:
+  virtual PacketFilter::Action FilterHandshake(const HandshakeHeader& header,
+                                               const DataBuffer& input,
+                                               DataBuffer* output) {
+    assert(1 < input.len());
+    size_t len = input.data()[0];
+    assert(len + 1 < input.len());
+    buffer_.Assign(input.data() + 1, len);
+    filtered_ = true;
+    return KEEP;
+  }
+
+ private:
+  DataBuffer buffer_;
+  bool filtered_;
+};
+
+// All stream only tests; DTLS isn't supported yet.
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuth) {
+  EnsureTlsSetup();
+  auto capture_cert_req = MakeTlsFilter<TlsCertificateRequestContextRecorder>(
+      server_, kTlsHandshakeCertificateRequest);
+  auto capture_certificate =
+      MakeTlsFilter<TlsCertificateRequestContextRecorder>(
+          client_, kTlsHandshakeCertificate);
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  size_t called = 0;
+  server_->SetAuthCertificateCallback(
+      [&called](TlsAgent*, PRBool, PRBool) -> SECStatus {
+        called++;
+        return SECSuccess;
+      });
+  Connect();
+  EXPECT_EQ(0U, called);
+  EXPECT_FALSE(capture_cert_req->filtered());
+  EXPECT_FALSE(capture_certificate->filtered());
+  // Send CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  // Need to do a round-trip so that the post-handshake message is
+  // handled on both client and server.
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ReadBytes(50);
+  EXPECT_EQ(1U, called);
+  EXPECT_TRUE(capture_cert_req->filtered());
+  EXPECT_TRUE(capture_certificate->filtered());
+  // Check if a non-empty request context is generated and it is
+  // properly sent back.
+  EXPECT_LT(0U, capture_cert_req->buffer().len());
+  EXPECT_EQ(capture_cert_req->buffer().len(),
+            capture_certificate->buffer().len());
+  EXPECT_EQ(0, memcmp(capture_cert_req->buffer().data(),
+                      capture_certificate->buffer().data(),
+                      capture_cert_req->buffer().len()));
+  ScopedCERTCertificate cert1(SSL_PeerCertificate(server_->ssl_fd()));
+  ScopedCERTCertificate cert2(SSL_LocalCertificate(client_->ssl_fd()));
+  EXPECT_TRUE(SECITEM_ItemsAreEqual(&cert1->derCert, &cert2->derCert));
+}
+
+static SECStatus GetClientAuthDataHook(void* self, PRFileDesc* fd,
+                                       CERTDistNames* caNames,
+                                       CERTCertificate** clientCert,
+                                       SECKEYPrivateKey** clientKey) {
+  ScopedCERTCertificate cert;
+  ScopedSECKEYPrivateKey priv;
+  // use a different certificate than TlsAgent::kClient
+  if (!TlsAgent::LoadCertificate(TlsAgent::kRsa2048, &cert, &priv)) {
+    return SECFailure;
+  }
+
+  *clientCert = cert.release();
+  *clientKey = priv.release();
+  return SECSuccess;
+}
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthMultiple) {
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  size_t called = 0;
+  server_->SetAuthCertificateCallback(
+      [&called](TlsAgent*, PRBool, PRBool) -> SECStatus {
+        called++;
+        return SECSuccess;
+      });
+  Connect();
+  EXPECT_EQ(0U, called);
+  EXPECT_EQ(nullptr, SSL_PeerCertificate(server_->ssl_fd()));
+  // Send 1st CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ReadBytes(50);
+  EXPECT_EQ(1U, called);
+  ScopedCERTCertificate cert1(SSL_PeerCertificate(server_->ssl_fd()));
+  ScopedCERTCertificate cert2(SSL_LocalCertificate(client_->ssl_fd()));
+  EXPECT_TRUE(SECITEM_ItemsAreEqual(&cert1->derCert, &cert2->derCert));
+  // Send 2nd CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_GetClientAuthDataHook(
+                            client_->ssl_fd(), GetClientAuthDataHook, nullptr));
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ReadBytes(50);
+  EXPECT_EQ(2U, called);
+  ScopedCERTCertificate cert3(SSL_PeerCertificate(server_->ssl_fd()));
+  ScopedCERTCertificate cert4(SSL_LocalCertificate(client_->ssl_fd()));
+  EXPECT_TRUE(SECITEM_ItemsAreEqual(&cert3->derCert, &cert4->derCert));
+  EXPECT_FALSE(SECITEM_ItemsAreEqual(&cert3->derCert, &cert1->derCert));
+}
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthConcurrent) {
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  Connect();
+  // Send 1st CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  // Send 2nd CertificateRequest.
+  EXPECT_EQ(SECFailure, SSL_SendCertificateRequest(server_->ssl_fd()));
+  EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError());
+}
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthMissingExtension) {
+  client_->SetupClientAuth();
+  Connect();
+  // Send CertificateRequest, should fail due to missing
+  // post_handshake_auth extension.
+  EXPECT_EQ(SECFailure, SSL_SendCertificateRequest(server_->ssl_fd()));
+  EXPECT_EQ(SSL_ERROR_MISSING_POST_HANDSHAKE_AUTH_EXTENSION, PORT_GetError());
+}
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthAfterClientAuth) {
+  client_->SetupClientAuth();
+  server_->RequestClientAuth(true);
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  size_t called = 0;
+  server_->SetAuthCertificateCallback(
+      [&called](TlsAgent*, PRBool, PRBool) -> SECStatus {
+        called++;
+        return SECSuccess;
+      });
+  Connect();
+  EXPECT_EQ(1U, called);
+  ScopedCERTCertificate cert1(SSL_PeerCertificate(server_->ssl_fd()));
+  ScopedCERTCertificate cert2(SSL_LocalCertificate(client_->ssl_fd()));
+  EXPECT_TRUE(SECITEM_ItemsAreEqual(&cert1->derCert, &cert2->derCert));
+  // Send CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_GetClientAuthDataHook(
+                            client_->ssl_fd(), GetClientAuthDataHook, nullptr));
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ReadBytes(50);
+  EXPECT_EQ(2U, called);
+  ScopedCERTCertificate cert3(SSL_PeerCertificate(server_->ssl_fd()));
+  ScopedCERTCertificate cert4(SSL_LocalCertificate(client_->ssl_fd()));
+  EXPECT_TRUE(SECITEM_ItemsAreEqual(&cert3->derCert, &cert4->derCert));
+  EXPECT_FALSE(SECITEM_ItemsAreEqual(&cert3->derCert, &cert1->derCert));
+}
+
+// Damages the request context in a CertificateRequest message.
+// We don't modify a Certificate message instead, so that the client
+// can compute CertificateVerify correctly.
+class TlsDamageCertificateRequestContextFilter : public TlsHandshakeFilter {
+ public:
+  TlsDamageCertificateRequestContextFilter(const std::shared_ptr<TlsAgent>& a)
+      : TlsHandshakeFilter(a, {kTlsHandshakeCertificateRequest}) {
+    EnableDecryption();
+  }
+
+ protected:
+  virtual PacketFilter::Action FilterHandshake(const HandshakeHeader& header,
+                                               const DataBuffer& input,
+                                               DataBuffer* output) {
+    *output = input;
+    assert(1 < output->len());
+    // The request context has a 1 octet length.
+    output->data()[1] ^= 73;
+    return CHANGE;
+  }
+};
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthContextMismatch) {
+  EnsureTlsSetup();
+  MakeTlsFilter<TlsDamageCertificateRequestContextFilter>(server_);
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  Connect();
+  // Send CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ExpectSendAlert(kTlsAlertIllegalParameter);
+  server_->ReadBytes(50);
+  EXPECT_EQ(SSL_ERROR_RX_MALFORMED_CERTIFICATE, PORT_GetError());
+  server_->ExpectReadWriteError();
+  server_->SendData(50);
+  client_->ExpectReceiveAlert(kTlsAlertIllegalParameter);
+  client_->ReadBytes(50);
+  EXPECT_EQ(SSL_ERROR_ILLEGAL_PARAMETER_ALERT, PORT_GetError());
+}
+
+// Replaces signature in a CertificateVerify message.
+class TlsDamageSignatureFilter : public TlsHandshakeFilter {
+ public:
+  TlsDamageSignatureFilter(const std::shared_ptr<TlsAgent>& a)
+      : TlsHandshakeFilter(a, {kTlsHandshakeCertificateVerify}) {
+    EnableDecryption();
+  }
+
+ protected:
+  virtual PacketFilter::Action FilterHandshake(const HandshakeHeader& header,
+                                               const DataBuffer& input,
+                                               DataBuffer* output) {
+    *output = input;
+    assert(2 < output->len());
+    // The signature follows a 2-octet signature scheme.
+    output->data()[2] ^= 73;
+    return CHANGE;
+  }
+};
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthBadSignature) {
+  EnsureTlsSetup();
+  MakeTlsFilter<TlsDamageSignatureFilter>(client_);
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  Connect();
+  // Send CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ExpectSendAlert(kTlsAlertDecodeError);
+  server_->ReadBytes(50);
+  EXPECT_EQ(SSL_ERROR_RX_MALFORMED_CERT_VERIFY, PORT_GetError());
+}
+
+TEST_F(TlsConnectStreamTls13, PostHandshakeAuthDecline) {
+  EnsureTlsSetup();
+  auto capture_cert_req = MakeTlsFilter<TlsCertificateRequestContextRecorder>(
+      server_, kTlsHandshakeCertificateRequest);
+  auto capture_certificate =
+      MakeTlsFilter<TlsCertificateRequestContextRecorder>(
+          client_, kTlsHandshakeCertificate);
+  client_->SetupClientAuth();
+  EXPECT_EQ(SECSuccess, SSL_OptionSet(client_->ssl_fd(),
+                                      SSL_ENABLE_POST_HANDSHAKE_AUTH, PR_TRUE));
+  // Client to decline the certificate request.
+  EXPECT_EQ(SECSuccess,
+            SSL_GetClientAuthDataHook(
+                client_->ssl_fd(),
+                [](void*, PRFileDesc*, CERTDistNames*, CERTCertificate**,
+                   SECKEYPrivateKey**) -> SECStatus { return SECFailure; },
+                nullptr));
+  size_t called = 0;
+  server_->SetAuthCertificateCallback(
+      [&called](TlsAgent*, PRBool, PRBool) -> SECStatus {
+        called++;
+        return SECSuccess;
+      });
+  Connect();
+  EXPECT_EQ(0U, called);
+  // Send CertificateRequest.
+  EXPECT_EQ(SECSuccess, SSL_SendCertificateRequest(server_->ssl_fd()))
+      << "Unexpected error: " << PORT_ErrorToName(PORT_GetError());
+  server_->SendData(50);
+  client_->ReadBytes(50);
+  client_->SendData(50);
+  server_->ReadBytes(50);
+  // AuthCertificateCallback is not called, because the client sends
+  // an empty certificate_list.
+  EXPECT_EQ(0U, called);
+  EXPECT_TRUE(capture_cert_req->filtered());
+  EXPECT_TRUE(capture_certificate->filtered());
+  // Check if a non-empty request context is generated and it is
+  // properly sent back.
+  EXPECT_LT(0U, capture_cert_req->buffer().len());
+  EXPECT_EQ(capture_cert_req->buffer().len(),
+            capture_certificate->buffer().len());
+  EXPECT_EQ(0, memcmp(capture_cert_req->buffer().data(),
+                      capture_certificate->buffer().data(),
+                      capture_cert_req->buffer().len()));
+}
+
 // In TLS 1.3, the client sends its cert rejection on the
 // second flight, and since it has already received the
 // server's Finished, it transitions to complete and
 // then gets an alert from the server. The test harness
 // doesn't handle this right yet.
 TEST_P(TlsConnectStream, DISABLED_ClientAuthRequiredRejected) {
   server_->RequestClientAuth(true);
   ConnectExpectFail();
@@ -268,19 +583,17 @@ TEST_P(TlsConnectTls12, ClientAuthBigRsa
 }
 
 // Replaces the signature scheme in a CertificateVerify message.
 class TlsReplaceSignatureSchemeFilter : public TlsHandshakeFilter {
  public:
   TlsReplaceSignatureSchemeFilter(const std::shared_ptr<TlsAgent>& a,
                                   SSLSignatureScheme scheme)
       : TlsHandshakeFilter(a, {kTlsHandshakeCertificateVerify}),
-        scheme_(scheme) {
-    EnableDecryption();
-  }
+        scheme_(scheme) {}
 
  protected:
   virtual PacketFilter::Action FilterHandshake(const HandshakeHeader& header,
                                                const DataBuffer& input,
                                                DataBuffer* output) {
     *output = input;
     output->Write(0, scheme_, 2);
     return CHANGE;
@@ -547,29 +860,32 @@ TEST_P(TlsConnectTls12, SignatureAlgorit
   MakeTlsFilter<TlsExtensionDropper>(client_, ssl_signature_algorithms_xtn);
   ConnectExpectAlert(server_, kTlsAlertDecryptError);
   client_->CheckErrorCode(SSL_ERROR_DECRYPT_ERROR_ALERT);
   server_->CheckErrorCode(SSL_ERROR_BAD_HANDSHAKE_HASH_VALUE);
 }
 
 TEST_P(TlsConnectTls13, UnsupportedSignatureSchemeAlert) {
   EnsureTlsSetup();
-  MakeTlsFilter<TlsReplaceSignatureSchemeFilter>(server_, ssl_sig_none);
+  auto filter =
+      MakeTlsFilter<TlsReplaceSignatureSchemeFilter>(server_, ssl_sig_none);
+  filter->EnableDecryption();
 
   ConnectExpectAlert(client_, kTlsAlertIllegalParameter);
   server_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
   client_->CheckErrorCode(SSL_ERROR_RX_MALFORMED_CERT_VERIFY);
 }
 
 TEST_P(TlsConnectTls13, InconsistentSignatureSchemeAlert) {
   EnsureTlsSetup();
 
   // This won't work because we use an RSA cert by default.
-  MakeTlsFilter<TlsReplaceSignatureSchemeFilter>(
+  auto filter = MakeTlsFilter<TlsReplaceSignatureSchemeFilter>(
       server_, ssl_sig_ecdsa_secp256r1_sha256);
+  filter->EnableDecryption();
 
   ConnectExpectAlert(client_, kTlsAlertIllegalParameter);
   server_->CheckErrorCode(SSL_ERROR_ILLEGAL_PARAMETER_ALERT);
   client_->CheckErrorCode(SSL_ERROR_INCORRECT_SIGNATURE_ALGORITHM);
 }
 
 TEST_P(TlsConnectTls12Plus, RequestClientAuthWithSha384) {
   server_->SetSignatureSchemes(kSignatureSchemeRsaSha384,
--- a/security/nss/gtests/ssl_gtest/ssl_damage_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/ssl_damage_unittest.cc
@@ -57,17 +57,16 @@ TEST_F(TlsConnectTest, DamageSecretHandl
   ConnectExpectAlert(client_, kTlsAlertDecryptError);
   client_->CheckErrorCode(SSL_ERROR_BAD_HANDSHAKE_HASH_VALUE);
 }
 
 TEST_P(TlsConnectGenericPre13, DamageServerSignature) {
   EnsureTlsSetup();
   auto filter = MakeTlsFilter<TlsLastByteDamager>(
       server_, kTlsHandshakeServerKeyExchange);
-  filter->EnableDecryption();
   ExpectAlert(client_, kTlsAlertDecryptError);
   ConnectExpectFail();
   client_->CheckErrorCode(SEC_ERROR_BAD_SIGNATURE);
   server_->CheckErrorCode(SSL_ERROR_DECRYPT_ERROR_ALERT);
 }
 
 TEST_P(TlsConnectTls13, DamageServerSignature) {
   EnsureTlsSetup();
@@ -79,17 +78,19 @@ TEST_P(TlsConnectTls13, DamageServerSign
 }
 
 TEST_P(TlsConnectGeneric, DamageClientSignature) {
   EnsureTlsSetup();
   client_->SetupClientAuth();
   server_->RequestClientAuth(true);
   auto filter = MakeTlsFilter<TlsLastByteDamager>(
       client_, kTlsHandshakeCertificateVerify);
-  filter->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    filter->EnableDecryption();
+  }
   server_->ExpectSendAlert(kTlsAlertDecryptError);
   // Do these handshakes by hand to avoid race condition on
   // the client processing the server's alert.
   StartConnect();
   client_->Handshake();
   server_->Handshake();
   client_->Handshake();
   server_->Handshake();
--- a/security/nss/gtests/ssl_gtest/ssl_drop_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/ssl_drop_unittest.cc
@@ -61,16 +61,48 @@ TEST_P(TlsConnectDatagramPre13, DropClie
 }
 
 // This drops the server's second flight three times.
 TEST_P(TlsConnectDatagramPre13, DropServerSecondFlightThrice) {
   server_->SetFilter(std::make_shared<SelectiveDropFilter>(0xe));
   Connect();
 }
 
+static void CheckAcks(const std::shared_ptr<TlsRecordRecorder>& acks,
+                      size_t index, std::vector<uint64_t> expected) {
+  ASSERT_LT(index, acks->count());
+  const DataBuffer& buf = acks->record(index).buffer;
+  size_t offset = 2;
+  uint64_t len;
+
+  EXPECT_EQ(2 + expected.size() * 8, buf.len());
+  ASSERT_TRUE(buf.Read(0, 2, &len));
+  ASSERT_EQ(static_cast<size_t>(len + 2), buf.len());
+  if ((2 + expected.size() * 8) != buf.len()) {
+    while (offset < buf.len()) {
+      uint64_t ack;
+      ASSERT_TRUE(buf.Read(offset, 8, &ack));
+      offset += 8;
+      std::cerr << "Ack=0x" << std::hex << ack << std::dec << std::endl;
+    }
+    return;
+  }
+
+  for (size_t i = 0; i < expected.size(); ++i) {
+    uint64_t a = expected[i];
+    uint64_t ack;
+    ASSERT_TRUE(buf.Read(offset, 8, &ack));
+    offset += 8;
+    if (a != ack) {
+      ADD_FAILURE() << "Wrong ack " << i << " expected=0x" << std::hex << a
+                    << " got=0x" << ack << std::dec;
+    }
+  }
+}
+
 class TlsDropDatagram13 : public TlsConnectDatagram13,
                           public ::testing::WithParamInterface<bool> {
  public:
   TlsDropDatagram13()
       : client_filters_(),
         server_filters_(),
         expected_client_acks_(0),
         expected_server_acks_(1) {}
@@ -134,47 +166,16 @@ class TlsDropDatagram13 : public TlsConn
     const TlsRecord& record(size_t i) const { return records_->record(i); }
 
     std::shared_ptr<TlsRecordRecorder> records_;
     std::shared_ptr<TlsRecordRecorder> ack_;
     std::shared_ptr<SelectiveRecordDropFilter> drop_;
     std::shared_ptr<PacketFilter> chain_;
   };
 
-  void CheckAcks(const DropAckChain& chain, size_t index,
-                 std::vector<uint64_t> acks) {
-    const DataBuffer& buf = chain.ack_->record(index).buffer;
-    size_t offset = 2;
-    uint64_t len;
-
-    EXPECT_EQ(2 + acks.size() * 8, buf.len());
-    ASSERT_TRUE(buf.Read(0, 2, &len));
-    ASSERT_EQ(static_cast<size_t>(len + 2), buf.len());
-    if ((2 + acks.size() * 8) != buf.len()) {
-      while (offset < buf.len()) {
-        uint64_t ack;
-        ASSERT_TRUE(buf.Read(offset, 8, &ack));
-        offset += 8;
-        std::cerr << "Ack=0x" << std::hex << ack << std::dec << std::endl;
-      }
-      return;
-    }
-
-    for (size_t i = 0; i < acks.size(); ++i) {
-      uint64_t a = acks[i];
-      uint64_t ack;
-      ASSERT_TRUE(buf.Read(offset, 8, &ack));
-      offset += 8;
-      if (a != ack) {
-        ADD_FAILURE() << "Wrong ack " << i << " expected=0x" << std::hex << a
-                      << " got=0x" << ack << std::dec;
-      }
-    }
-  }
-
   void CheckedHandshakeSendReceive() {
     Handshake();
     CheckPostHandshake();
   }
 
   void CheckPostHandshake() {
     CheckConnected();
     SendReceive();
@@ -194,55 +195,55 @@ class TlsDropDatagram13 : public TlsConn
 // Dropping complete first and second flights does not produce
 // ACKs
 TEST_P(TlsDropDatagram13, DropClientFirstFlightOnce) {
   client_filters_.drop_->Reset({0});
   StartConnect();
   client_->Handshake();
   server_->Handshake();
   CheckedHandshakeSendReceive();
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 TEST_P(TlsDropDatagram13, DropServerFirstFlightOnce) {
   server_filters_.drop_->Reset(0xff);
   StartConnect();
   client_->Handshake();
   // Send the first flight, all dropped.
   server_->Handshake();
   server_filters_.drop_->Disable();
   CheckedHandshakeSendReceive();
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // Dropping the server's first record also does not produce
 // an ACK because the next record is ignored.
 // TODO(ekr@rtfm.com): We should generate an empty ACK.
 TEST_P(TlsDropDatagram13, DropServerFirstRecordOnce) {
   server_filters_.drop_->Reset({0});
   StartConnect();
   client_->Handshake();
   server_->Handshake();
   Handshake();
   CheckedHandshakeSendReceive();
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // Dropping the second packet of the server's flight should
 // produce an ACK.
 TEST_P(TlsDropDatagram13, DropServerSecondRecordOnce) {
   server_filters_.drop_->Reset({1});
   StartConnect();
   client_->Handshake();
   server_->Handshake();
   HandshakeAndAck(client_);
   expected_client_acks_ = 1;
   CheckedHandshakeSendReceive();
-  CheckAcks(client_filters_, 0, {0});  // ServerHello
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(client_filters_.ack_, 0, {0});  // ServerHello
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // Drop the server ACK and verify that the client retransmits
 // the ClientHello.
 TEST_P(TlsDropDatagram13, DropServerAckOnce) {
   StartConnect();
   client_->Handshake();
   server_->Handshake();
@@ -260,36 +261,36 @@ TEST_P(TlsDropDatagram13, DropServerAckO
   server_->Handshake();  // Read the Finished and send an ACK.
   uint8_t buf[1];
   PRInt32 rv = PR_Read(client_->ssl_fd(), buf, sizeof(buf));
   expected_server_acks_ = 2;
   EXPECT_GT(0, rv);
   EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError());
   CheckPostHandshake();
   // There should be two copies of the finished ACK
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
-  CheckAcks(server_filters_, 1, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 1, {0x0002000000000000ULL});
 }
 
 // Drop the client certificate verify.
 TEST_P(TlsDropDatagram13, DropClientCertVerify) {
   StartConnect();
   client_->SetupClientAuth();
   server_->RequestClientAuth(true);
   client_->Handshake();
   server_->Handshake();
   // Have the client drop Cert Verify
   client_filters_.drop_->Reset({1});
   expected_server_acks_ = 2;
   CheckedHandshakeSendReceive();
   // Ack of the Cert.
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
   // Ack of the whole client handshake.
   CheckAcks(
-      server_filters_, 1,
+      server_filters_.ack_, 1,
       {0x0002000000000000ULL,    // CH (we drop everything after this on client)
        0x0002000000000003ULL,    // CT (2)
        0x0002000000000004ULL});  // FIN (2)
 }
 
 // Shrink the MTU down so that certs get split and drop the first piece.
 TEST_P(TlsDropDatagram13, DropFirstHalfOfServerCertificate) {
   server_filters_.drop_->Reset({2});
@@ -305,21 +306,21 @@ TEST_P(TlsDropDatagram13, DropFirstHalfO
   expected_client_acks_ = 1;
   HandshakeAndAck(client_);
   server_->Handshake();                               // Retransmit
   EXPECT_EQ(3UL, server_filters_.records_->count());  // CT2, CV, FIN
   // Check that the first record is CT1 (which is identical to the same
   // as the previous CT1).
   EXPECT_EQ(ct1_size, server_filters_.record(0).buffer.len());
   CheckedHandshakeSendReceive();
-  CheckAcks(client_filters_, 0,
+  CheckAcks(client_filters_.ack_, 0,
             {0,                        // SH
              0x0002000000000000ULL,    // EE
              0x0002000000000002ULL});  // CT2
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // Shrink the MTU down so that certs get split and drop the second piece.
 TEST_P(TlsDropDatagram13, DropSecondHalfOfServerCertificate) {
   server_filters_.drop_->Reset({3});
   StartConnect();
   ShrinkPostServerHelloMtu();
   client_->Handshake();
@@ -331,23 +332,23 @@ TEST_P(TlsDropDatagram13, DropSecondHalf
   server_filters_.records_->Clear();
   expected_client_acks_ = 1;
   HandshakeAndAck(client_);
   server_->Handshake();                               // Retransmit
   EXPECT_EQ(3UL, server_filters_.records_->count());  // CT1, CV, FIN
   // Check that the first record is CT1
   EXPECT_EQ(ct1_size, server_filters_.record(0).buffer.len());
   CheckedHandshakeSendReceive();
-  CheckAcks(client_filters_, 0,
+  CheckAcks(client_filters_.ack_, 0,
             {
                 0,                      // SH
                 0x0002000000000000ULL,  // EE
                 0x0002000000000001ULL,  // CT1
             });
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // In this test, the Certificate message is sent four times, we drop all or part
 // of the first three attempts:
 // 1. Without fragmentation so that we can see how big it is - we drop that.
 // 2. In two pieces - we drop half AND the resulting ACK.
 // 3. In three pieces - we drop the middle piece.
 //
@@ -387,28 +388,28 @@ class TlsFragmentationAndRecoveryTest : 
 
     CompleteHandshake(final_server_flight_count);
 
     // This is the ACK for the first attempt to send a whole certificate.
     std::vector<uint64_t> client_acks = {
         0,                     // SH
         0x0002000000000000ULL  // EE
     };
-    CheckAcks(client_filters_, 0, client_acks);
+    CheckAcks(client_filters_.ack_, 0, client_acks);
     // And from the second attempt for the half was kept (we delayed this ACK).
     client_acks.push_back(0x0002000000000000ULL + second_flight_count +
                           ~dropped_half % 2);
-    CheckAcks(client_filters_, 1, client_acks);
+    CheckAcks(client_filters_.ack_, 1, client_acks);
     // And the third attempt where the first and last thirds got through.
     client_acks.push_back(0x0002000000000000ULL + second_flight_count +
                           third_flight_count - 1);
     client_acks.push_back(0x0002000000000000ULL + second_flight_count +
                           third_flight_count + 1);
-    CheckAcks(client_filters_, 2, client_acks);
-    CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+    CheckAcks(client_filters_.ack_, 2, client_acks);
+    CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
   }
 
  private:
   void FirstFlightDropCertificate() {
     StartConnect();
     client_->Handshake();
 
     // Note: 1 << N is the Nth packet, starting from zero.
@@ -543,17 +544,17 @@ TEST_P(TlsDropDatagram13, NoDropsDuringZ
   server_->Set0RttEnabled(true);
   ExpectResumption(RESUME_TICKET);
   ZeroRttSendReceive(true, true);
   Handshake();
   ExpectEarlyDataAccepted(true);
   CheckConnected();
   SendReceive();
   EXPECT_EQ(0U, client_filters_.ack_->count());
-  CheckAcks(server_filters_, 0,
+  CheckAcks(server_filters_.ack_, 0,
             {0x0001000000000001ULL,    // EOED
              0x0002000000000000ULL});  // Finished
 }
 
 TEST_P(TlsDropDatagram13, DropEEDuringZeroRtt) {
   SetupForZeroRtt();
   SetFilters();
   std::cerr << "Starting second handshake" << std::endl;
@@ -562,18 +563,18 @@ TEST_P(TlsDropDatagram13, DropEEDuringZe
   ExpectResumption(RESUME_TICKET);
   server_filters_.drop_->Reset({1});
   ZeroRttSendReceive(true, true);
   HandshakeAndAck(client_);
   Handshake();
   ExpectEarlyDataAccepted(true);
   CheckConnected();
   SendReceive();
-  CheckAcks(client_filters_, 0, {0});
-  CheckAcks(server_filters_, 0,
+  CheckAcks(client_filters_.ack_, 0, {0});
+  CheckAcks(server_filters_.ack_, 0,
             {0x0001000000000002ULL,    // EOED
              0x0002000000000000ULL});  // Finished
 }
 
 class TlsReorderDatagram13 : public TlsDropDatagram13 {
  public:
   TlsReorderDatagram13() {}
 
@@ -603,64 +604,78 @@ TEST_P(TlsDropDatagram13, ReorderServerE
   StartConnect();
   client_->Handshake();
   server_->Handshake();
   // We dropped EE, now reinject.
   server_->SendRecordDirect(server_filters_.record(1));
   expected_client_acks_ = 1;
   HandshakeAndAck(client_);
   CheckedHandshakeSendReceive();
-  CheckAcks(client_filters_, 0,
+  CheckAcks(client_filters_.ack_, 0,
             {
                 0,                   // SH
                 0x0002000000000000,  // EE
             });
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 // The client sends an out of order non-handshake message
 // but with the handshake key.
 class TlsSendCipherSpecCapturer {
  public:
-  TlsSendCipherSpecCapturer(std::shared_ptr<TlsAgent>& agent)
-      : send_cipher_specs_() {
-    SSLInt_SetCipherSpecChangeFunc(agent->ssl_fd(), CipherSpecChanged,
-                                   (void*)this);
+  TlsSendCipherSpecCapturer(const std::shared_ptr<TlsAgent>& agent)
+      : agent_(agent), send_cipher_specs_() {
+    EXPECT_EQ(SECSuccess,
+              SSL_SecretCallback(agent_->ssl_fd(), SecretCallback, this));
   }
 
   std::shared_ptr<TlsCipherSpec> spec(size_t i) {
     if (i >= send_cipher_specs_.size()) {
       return nullptr;
     }
     return send_cipher_specs_[i];
   }
 
  private:
-  static void CipherSpecChanged(void* arg, PRBool sending,
-                                ssl3CipherSpec* newSpec) {
-    if (!sending) {
+  static void SecretCallback(PRFileDesc* fd, PRUint16 epoch,
+                             SSLSecretDirection dir, PK11SymKey* secret,
+                             void* arg) {
+    auto self = static_cast<TlsSendCipherSpecCapturer*>(arg);
+    std::cerr << self->agent_->role_str() << ": capture " << dir
+              << " secret for epoch " << epoch << std::endl;
+
+    if (dir == ssl_secret_read) {
       return;
     }
 
-    auto self = static_cast<TlsSendCipherSpecCapturer*>(arg);
+    SSLPreliminaryChannelInfo preinfo;
+    EXPECT_EQ(SECSuccess,
+              SSL_GetPreliminaryChannelInfo(self->agent_->ssl_fd(), &preinfo,
+                                            sizeof(preinfo)));
+    EXPECT_EQ(sizeof(preinfo), preinfo.length);
+    EXPECT_TRUE(preinfo.valuesSet & ssl_preinfo_cipher_suite);
 
-    auto spec = std::make_shared<TlsCipherSpec>();
-    bool ret = spec->Init(SSLInt_CipherSpecToEpoch(newSpec),
-                          SSLInt_CipherSpecToAlgorithm(newSpec),
-                          SSLInt_CipherSpecToKey(newSpec),
-                          SSLInt_CipherSpecToIv(newSpec));
-    EXPECT_EQ(true, ret);
+    SSLCipherSuiteInfo cipherinfo;
+    EXPECT_EQ(SECSuccess,
+              SSL_GetCipherSuiteInfo(preinfo.cipherSuite, &cipherinfo,
+                                     sizeof(cipherinfo)));
+    EXPECT_EQ(sizeof(cipherinfo), cipherinfo.length);
+
+    auto spec = std::make_shared<TlsCipherSpec>(true, epoch);
+    EXPECT_TRUE(spec->SetKeys(&cipherinfo, secret));
     self->send_cipher_specs_.push_back(spec);
   }
 
+  std::shared_ptr<TlsAgent> agent_;
   std::vector<std::shared_ptr<TlsCipherSpec>> send_cipher_specs_;
 };
 
-TEST_P(TlsDropDatagram13, SendOutOfOrderAppWithHandshakeKey) {
+TEST_F(TlsConnectDatagram13, SendOutOfOrderAppWithHandshakeKey) {
   StartConnect();
+  // Capturing secrets means that we can't use decrypting filters on the client.
   TlsSendCipherSpecCapturer capturer(client_);
   client_->Handshake();
   server_->Handshake();
   client_->Handshake();
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, client_->state());
   server_->Handshake();
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, server_->state());
   // After the client sends Finished, inject an app data record
@@ -675,39 +690,42 @@ TEST_P(TlsDropDatagram13, SendOutOfOrder
 
   // Now have the server consume the bogus message.
   server_->ExpectSendAlert(illegal_parameter, kTlsAlertFatal);
   server_->Handshake();
   EXPECT_EQ(TlsAgent::STATE_ERROR, server_->state());
   EXPECT_EQ(SSL_ERROR_RX_UNKNOWN_RECORD_TYPE, PORT_GetError());
 }
 
-TEST_P(TlsDropDatagram13, SendOutOfOrderHsNonsenseWithHandshakeKey) {
+TEST_F(TlsConnectDatagram13, SendOutOfOrderHsNonsenseWithHandshakeKey) {
   StartConnect();
   TlsSendCipherSpecCapturer capturer(client_);
+  auto acks = MakeTlsFilter<TlsRecordRecorder>(server_, ssl_ct_ack);
+  acks->EnableDecryption();
+
   client_->Handshake();
   server_->Handshake();
   client_->Handshake();
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, client_->state());
   server_->Handshake();
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, server_->state());
   // Inject a new bogus handshake record, which the server responds
   // to by just ACKing the original one (we ignore the contents).
   uint8_t buf[] = {'a', 'b', 'c'};
   auto spec = capturer.spec(0);
   ASSERT_NE(nullptr, spec.get());
   ASSERT_EQ(2, spec->epoch());
   ASSERT_TRUE(client_->SendEncryptedRecord(spec, 0x0002000000000002,
                                            ssl_ct_handshake,
                                            DataBuffer(buf, sizeof(buf))));
   server_->Handshake();
-  EXPECT_EQ(2UL, server_filters_.ack_->count());
+  EXPECT_EQ(2UL, acks->count());
   // The server acknowledges client Finished twice.
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
-  CheckAcks(server_filters_, 1, {0x0002000000000000ULL});
+  CheckAcks(acks, 0, {0x0002000000000000ULL});
+  CheckAcks(acks, 1, {0x0002000000000000ULL});
 }
 
 // Shrink the MTU down so that certs get split and then swap the first and
 // second pieces of the server certificate.
 TEST_P(TlsReorderDatagram13, ReorderServerCertificate) {
   StartConnect();
   ShrinkPostServerHelloMtu();
   client_->Handshake();
@@ -721,17 +739,17 @@ TEST_P(TlsReorderDatagram13, ReorderServ
   ReSend(TlsAgent::SERVER, std::vector<size_t>{0, 1, 3, 2, 4, 5});
   // Clear.
   server_filters_.drop_->Disable();
   server_filters_.records_->Clear();
   // Wait for client to send ACK.
   ShiftDtlsTimers();
   CheckedHandshakeSendReceive();
   EXPECT_EQ(2UL, server_filters_.records_->count());  // ACK + Data
-  CheckAcks(server_filters_, 0, {0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0, {0x0002000000000000ULL});
 }
 
 TEST_P(TlsReorderDatagram13, DataAfterEOEDDuringZeroRtt) {
   SetupForZeroRtt();
   SetFilters();
   std::cerr << "Starting second handshake" << std::endl;
   client_->Set0RttEnabled(true);
   server_->Set0RttEnabled(true);
@@ -756,17 +774,18 @@ TEST_P(TlsReorderDatagram13, DataAfterEO
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, client_->state());
   EXPECT_EQ(TlsAgent::STATE_CONNECTING, server_->state());
   // Now re-send the client's messages: EOED, data, FIN
   ReSend(TlsAgent::CLIENT, std::vector<size_t>({1, 0, 2}));
   server_->Handshake();
   CheckConnected();
   EXPECT_EQ(0U, client_filters_.ack_->count());
   // Acknowledgements for EOED and Finished.
-  CheckAcks(server_filters_, 0, {0x0001000000000002ULL, 0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0,
+            {0x0001000000000002ULL, 0x0002000000000000ULL});
   uint8_t buf[8];
   rv = PR_Read(server_->ssl_fd(), buf, sizeof(buf));
   EXPECT_EQ(-1, rv);
   EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError());
 }
 
 TEST_P(TlsReorderDatagram13, DataAfterFinDuringZeroRtt) {
   SetupForZeroRtt();
@@ -795,17 +814,18 @@ TEST_P(TlsReorderDatagram13, DataAfterFi
   EXPECT_EQ(TlsAgent::STATE_CONNECTED, client_->state());
   EXPECT_EQ(TlsAgent::STATE_CONNECTING, server_->state());
   // Now re-send the client's messages: EOED, FIN, Data
   ReSend(TlsAgent::CLIENT, std::vector<size_t>({1, 2, 0}));
   server_->Handshake();
   CheckConnected();
   EXPECT_EQ(0U, client_filters_.ack_->count());
   // Acknowledgements for EOED and Finished.
-  CheckAcks(server_filters_, 0, {0x0001000000000002ULL, 0x0002000000000000ULL});
+  CheckAcks(server_filters_.ack_, 0,
+            {0x0001000000000002ULL, 0x0002000000000000ULL});
   uint8_t buf[8];
   rv = PR_Read(server_->ssl_fd(), buf, sizeof(buf));
   EXPECT_EQ(-1, rv);
   EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError());
 }
 
 static void GetCipherAndLimit(uint16_t version, uint16_t* cipher,
                               uint64_t* limit = nullptr) {
--- a/security/nss/gtests/ssl_gtest/ssl_gtest.gyp
+++ b/security/nss/gtests/ssl_gtest/ssl_gtest.gyp
@@ -32,16 +32,17 @@
         'ssl_gather_unittest.cc',
         'ssl_gtest.cc',
         'ssl_hrr_unittest.cc',
         'ssl_keylog_unittest.cc',
         'ssl_keyupdate_unittest.cc',
         'ssl_loopback_unittest.cc',
         'ssl_misc_unittest.cc',
         'ssl_record_unittest.cc',
+        'ssl_recordsep_unittest.cc',
         'ssl_recordsize_unittest.cc',
         'ssl_resumption_unittest.cc',
         'ssl_renegotiation_unittest.cc',
         'ssl_skip_unittest.cc',
         'ssl_staticrsa_unittest.cc',
         'ssl_tls13compat_unittest.cc',
         'ssl_v2_client_hello_unittest.cc',
         'ssl_version_unittest.cc',
new file mode 100644
--- /dev/null
+++ b/security/nss/gtests/ssl_gtest/ssl_recordsep_unittest.cc
@@ -0,0 +1,518 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "secerr.h"
+#include "ssl.h"
+#include "sslerr.h"
+#include "sslproto.h"
+
+extern "C" {
+// This is not something that should make you happy.
+#include "libssl_internals.h"
+}
+
+#include <queue>
+#include "gtest_utils.h"
+#include "nss_scoped_ptrs.h"
+#include "tls_connect.h"
+#include "tls_filter.h"
+#include "tls_parser.h"
+
+namespace nss_test {
+
+class HandshakeSecretTracker {
+ public:
+  HandshakeSecretTracker(const std::shared_ptr<TlsAgent>& agent,
+                         uint16_t first_read_epoch, uint16_t first_write_epoch)
+      : agent_(agent),
+        next_read_epoch_(first_read_epoch),
+        next_write_epoch_(first_write_epoch) {
+    EXPECT_EQ(SECSuccess,
+              SSL_SecretCallback(agent_->ssl_fd(),
+                                 HandshakeSecretTracker::SecretCb, this));
+  }
+
+  void CheckComplete() const {
+    EXPECT_EQ(0, next_read_epoch_);
+    EXPECT_EQ(0, next_write_epoch_);
+  }
+
+ private:
+  static void SecretCb(PRFileDesc* fd, PRUint16 epoch, SSLSecretDirection dir,
+                       PK11SymKey* secret, void* arg) {
+    HandshakeSecretTracker* t = reinterpret_cast<HandshakeSecretTracker*>(arg);
+    t->SecretUpdated(epoch, dir, secret);
+  }
+
+  void SecretUpdated(PRUint16 epoch, SSLSecretDirection dir,
+                     PK11SymKey* secret) {
+    if (g_ssl_gtest_verbose) {
+      std::cerr << agent_->role_str() << ": secret callback for " << dir
+                << " epoch " << epoch << std::endl;
+    }
+
+    EXPECT_TRUE(secret);
+    uint16_t* p;
+    if (dir == ssl_secret_read) {
+      p = &next_read_epoch_;
+    } else {
+      ASSERT_EQ(ssl_secret_write, dir);
+      p = &next_write_epoch_;
+    }
+    EXPECT_EQ(*p, epoch);
+    switch (*p) {
+      case 1:  // 1 == 0-RTT, next should be handshake.
+      case 2:  // 2 == handshake, next should be application data.
+        (*p)++;
+        break;
+
+      case 3:  // 3 == application data, there should be no more.
+        // Use 0 as a sentinel value.
+        *p = 0;
+        break;
+
+      default:
+        ADD_FAILURE() << "Unexpected next epoch: " << *p;
+    }
+  }
+
+  std::shared_ptr<TlsAgent> agent_;
+  uint16_t next_read_epoch_;
+  uint16_t next_write_epoch_;
+};
+
+TEST_F(TlsConnectTest, HandshakeSecrets) {
+  ConfigureVersion(SSL_LIBRARY_VERSION_TLS_1_3);
+  EnsureTlsSetup();
+
+  HandshakeSecretTracker c(client_, 2, 2);
+  HandshakeSecretTracker s(server_, 2, 2);
+
+  Connect();
+  SendReceive();
+
+  c.CheckComplete();
+  s.CheckComplete();
+}
+
+TEST_F(TlsConnectTest, ZeroRttSecrets) {
+  SetupForZeroRtt();
+
+  HandshakeSecretTracker c(client_, 2, 1);
+  HandshakeSecretTracker s(server_, 1, 2);
+
+  client_->Set0RttEnabled(true);
+  server_->Set0RttEnabled(true);
+  ExpectResumption(RESUME_TICKET);
+  ZeroRttSendReceive(true, true);
+  Handshake();
+  ExpectEarlyDataAccepted(true);
+  CheckConnected();
+  SendReceive();
+
+  c.CheckComplete();
+  s.CheckComplete();
+}
+
+class KeyUpdateTracker {
+ public:
+  KeyUpdateTracker(const std::shared_ptr<TlsAgent>& agent,
+                   bool expect_read_secret)
+      : agent_(agent), expect_read_secret_(expect_read_secret), called_(false) {
+    EXPECT_EQ(SECSuccess, SSL_SecretCallback(agent_->ssl_fd(),
+                                             KeyUpdateTracker::SecretCb, this));
+  }
+
+  void CheckCalled() const { EXPECT_TRUE(called_); }
+
+ private:
+  static void SecretCb(PRFileDesc* fd, PRUint16 epoch, SSLSecretDirection dir,
+                       PK11SymKey* secret, void* arg) {
+    KeyUpdateTracker* t = reinterpret_cast<KeyUpdateTracker*>(arg);
+    t->SecretUpdated(epoch, dir, secret);
+  }
+
+  void SecretUpdated(PRUint16 epoch, SSLSecretDirection dir,
+                     PK11SymKey* secret) {
+    EXPECT_EQ(4U, epoch);
+    EXPECT_EQ(expect_read_secret_, dir == ssl_secret_read);
+    EXPECT_TRUE(secret);
+    called_ = true;
+  }
+
+  std::shared_ptr<TlsAgent> agent_;
+  bool expect_read_secret_;
+  bool called_;
+};
+
+TEST_F(TlsConnectTest, KeyUpdateSecrets) {
+  ConfigureVersion(SSL_LIBRARY_VERSION_TLS_1_3);
+  Connect();
+  // The update is to the client write secret; the server read secret.
+  KeyUpdateTracker c(client_, false);
+  KeyUpdateTracker s(server_, true);
+  EXPECT_EQ(SECSuccess, SSL_KeyUpdate(client_->ssl_fd(), PR_FALSE));
+  SendReceive(50);
+  SendReceive(60);
+  CheckEpochs(4, 3);
+  c.CheckCalled();
+  s.CheckCalled();
+}
+
+// BadPrSocket is an instance of a PR IO layer that crashes the test if it is
+// ever used for reading or writing.  It does that by failing to overwrite any
+// of the DummyIOLayerMethods, which all crash when invoked.
+class BadPrSocket : public DummyIOLayerMethods {
+ public:
+  BadPrSocket(std::shared_ptr<TlsAgent>& agent) : DummyIOLayerMethods() {
+    static PRDescIdentity bad_identity = PR_GetUniqueIdentity("bad NSPR id");
+    fd_ = DummyIOLayerMethods::CreateFD(bad_identity, this);
+
+    // This is terrible, but NSPR doesn't provide an easy way to replace the
+    // bottom layer of an IO stack.  Take the DummyPrSocket and replace its
+    // NSPR method vtable with the ones from this object.
+    dummy_layer_ =
+        PR_GetIdentitiesLayer(agent->ssl_fd(), DummyPrSocket::LayerId());
+    original_methods_ = dummy_layer_->methods;
+    original_secret_ = dummy_layer_->secret;
+    dummy_layer_->methods = fd_->methods;
+    dummy_layer_->secret = reinterpret_cast<PRFilePrivate*>(this);
+  }
+
+  // This will be destroyed before the agent, so we need to restore the state
+  // before we tampered with it.
+  virtual ~BadPrSocket() {
+    dummy_layer_->methods = original_methods_;
+    dummy_layer_->secret = original_secret_;
+  }
+
+ private:
+  ScopedPRFileDesc fd_;
+  PRFileDesc* dummy_layer_;
+  const PRIOMethods* original_methods_;
+  PRFilePrivate* original_secret_;
+};
+
+class StagedRecords {
+ public:
+  StagedRecords(std::shared_ptr<TlsAgent>& agent) : agent_(agent), records_() {
+    EXPECT_EQ(SECSuccess,
+              SSL_RecordLayerWriteCallback(
+                  agent_->ssl_fd(), StagedRecords::StageRecordData, this));
+  }
+
+  virtual ~StagedRecords() {
+    // Uninstall so that the callback doesn't fire during cleanup.
+    EXPECT_EQ(SECSuccess,
+              SSL_RecordLayerWriteCallback(agent_->ssl_fd(), nullptr, nullptr));
+  }
+
+  bool empty() const { return records_.empty(); }
+
+  void ForwardAll(std::shared_ptr<TlsAgent>& peer) {
+    EXPECT_NE(agent_, peer) << "can't forward to self";
+    for (auto r : records_) {
+      r.Forward(peer);
+    }
+    records_.clear();
+  }
+
+  // This forwards all saved data and checks the resulting state.
+  void ForwardAll(std::shared_ptr<TlsAgent>& peer,
+                  TlsAgent::State expected_state) {
+    ForwardAll(peer);
+    peer->Handshake();
+    EXPECT_EQ(expected_state, peer->state());
+  }
+
+  void ForwardPartial(std::shared_ptr<TlsAgent>& peer) {
+    if (records_.empty()) {
+      ADD_FAILURE() << "No records to slice";
+      return;
+    }
+    auto& last = records_.back();
+    auto tail = last.SliceTail();
+    ForwardAll(peer, TlsAgent::STATE_CONNECTING);
+    records_.push_back(tail);
+    EXPECT_EQ(TlsAgent::STATE_CONNECTING, peer->state());
+  }
+
+ private:
+  // A single record.
+  class StagedRecord {
+   public:
+    StagedRecord(const std::string role, uint16_t epoch, SSLContentType ct,
+                 const uint8_t* data, size_t len)
+        : role_(role), epoch_(epoch), content_type_(ct), data_(data, len) {
+      if (g_ssl_gtest_verbose) {
+        std::cerr << role_ << ": staged epoch " << epoch_ << " "
+                  << content_type_ << ": " << data_ << std::endl;
+      }
+    }
+
+    // This forwards staged data to the identified agent.
+    void Forward(std::shared_ptr<TlsAgent>& peer) {
+      // Now there should be staged data.
+      EXPECT_FALSE(data_.empty());
+      if (g_ssl_gtest_verbose) {
+        std::cerr << role_ << ": forward " << data_ << std::endl;
+      }
+      SECStatus rv = SSL_RecordLayerData(
+          peer->ssl_fd(), epoch_, content_type_, data_.data(),
+          static_cast<unsigned int>(data_.len()));
+      if (rv != SECSuccess) {
+        EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError());
+      }
+    }
+
+    // Slices the tail off this record and returns it.
+    StagedRecord SliceTail() {
+      size_t slice = 1;
+      if (data_.len() <= slice) {
+        ADD_FAILURE() << "record too small to slice in two";
+        slice = 0;
+      }
+      size_t keep = data_.len() - slice;
+      StagedRecord tail(role_, epoch_, content_type_, data_.data() + keep,
+                        slice);
+      data_.Truncate(keep);
+      return tail;
+    }
+
+   private:
+    std::string role_;
+    uint16_t epoch_;
+    SSLContentType content_type_;
+    DataBuffer data_;
+  };
+
+  // This is an SSLRecordWriteCallback that stages data.
+  static SECStatus StageRecordData(PRFileDesc* fd, PRUint16 epoch,
+                                   SSLContentType content_type,
+                                   const PRUint8* data, unsigned int len,
+                                   void* arg) {
+    auto stage = reinterpret_cast<StagedRecords*>(arg);
+    stage->records_.push_back(StagedRecord(stage->agent_->role_str(), epoch,
+                                           content_type, data,
+                                           static_cast<size_t>(len)));
+    return SECSuccess;
+  }
+
+  std::shared_ptr<TlsAgent>& agent_;
+  std::deque<StagedRecord> records_;
+};
+
+// Attempting to feed application data in before the handshake is complete
+// should be caught.
+static void RefuseApplicationData(std::shared_ptr<TlsAgent>& peer,
+                                  uint16_t epoch) {
+  static const uint8_t d[] = {1, 2, 3};
+  EXPECT_EQ(SECFailure,
+            SSL_RecordLayerData(peer->ssl_fd(), epoch, ssl_ct_application_data,
+                                d, static_cast<unsigned int>(sizeof(d))));
+  EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
+}
+
+static void SendForwardReceive(std::shared_ptr<TlsAgent>& sender,
+                               StagedRecords& sender_stage,
+                               std::shared_ptr<TlsAgent>& receiver) {
+  const size_t count = 10;
+  sender->SendData(count, count);
+  sender_stage.ForwardAll(receiver);
+  receiver->ReadBytes(count);
+}
+
+TEST_P(TlsConnectStream, ReplaceRecordLayer) {
+  StartConnect();
+  client_->SetServerKeyBits(server_->server_key_bits());
+
+  // BadPrSocket installs an IO layer that crashes when the SSL layer attempts
+  // to read or write.
+  BadPrSocket bad_layer_client(client_);
+  BadPrSocket bad_layer_server(server_);
+
+  // StagedRecords installs a handler for unprotected data from the socket, and
+  // captures that data.
+  StagedRecords client_stage(client_);
+  StagedRecords server_stage(server_);
+
+  // Both peers should refuse application data from epoch 0.
+  RefuseApplicationData(client_, 0);
+  RefuseApplicationData(server_, 0);
+
+  // This first call forwards nothing, but it causes the client to handshake,
+  // which starts things off.  This stages the ClientHello as a result.
+  server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTING);
+  // This processes the ClientHello and stages the first server flight.
+  client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTING);
+  RefuseApplicationData(server_, 1);
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    // Process the server flight and the client is done.
+    server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTED);
+    client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTED);
+  } else {
+    server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTING);
+    RefuseApplicationData(client_, 1);
+    client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTED);
+    server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTED);
+  }
+  CheckKeys();
+
+  // Reading and writing application data should work.
+  SendForwardReceive(client_, client_stage, server_);
+  SendForwardReceive(server_, server_stage, client_);
+}
+
+static SECStatus AuthCompleteBlock(TlsAgent*, PRBool, PRBool) {
+  return SECWouldBlock;
+}
+
+TEST_P(TlsConnectStream, ReplaceRecordLayerAsyncLateAuth) {
+  StartConnect();
+  client_->SetServerKeyBits(server_->server_key_bits());
+
+  BadPrSocket bad_layer_client(client_);
+  BadPrSocket bad_layer_server(server_);
+  StagedRecords client_stage(client_);
+  StagedRecords server_stage(server_);
+
+  client_->SetAuthCertificateCallback(AuthCompleteBlock);
+
+  server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTING);
+  client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTING);
+  server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTING);
+
+  // Prior to TLS 1.3, the client sends its second flight immediately.  But in
+  // TLS 1.3, a client won't send a Finished until it is happy with the server
+  // certificate.  So blocking certificate validation causes the client to send
+  // nothing.
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    ASSERT_TRUE(client_stage.empty());
+
+    // Client should have stopped reading when it saw the Certificate message,
+    // so it will be reading handshake epoch, and writing cleartext.
+    client_->CheckEpochs(2, 0);
+    // Server should be reading handshake, and writing application data.
+    server_->CheckEpochs(2, 3);
+
+    // Handshake again and the client will read the remainder of the server's
+    // flight, but it will remain blocked.
+    client_->Handshake();
+    ASSERT_TRUE(client_stage.empty());
+    EXPECT_EQ(TlsAgent::STATE_CONNECTING, client_->state());
+  } else {
+    // In prior versions, the client's second flight is always sent.
+    ASSERT_FALSE(client_stage.empty());
+  }
+
+  // Now declare the certificate good.
+  EXPECT_EQ(SECSuccess, SSL_AuthCertificateComplete(client_->ssl_fd(), 0));
+  client_->Handshake();
+  ASSERT_FALSE(client_stage.empty());
+
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    EXPECT_EQ(TlsAgent::STATE_CONNECTED, client_->state());
+    client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTED);
+  } else {
+    client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTED);
+    server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTED);
+  }
+  CheckKeys();
+
+  // Reading and writing application data should work.
+  SendForwardReceive(client_, client_stage, server_);
+}
+
+// This test ensures that data is correctly forwarded when the handshake is
+// resumed after asynchronous server certificate authentication, when
+// SSL_AuthCertificateComplete() is called.  The logic for resuming the
+// handshake involves a different code path than the usual one, so this test
+// exercises that code fully.
+TEST_F(TlsConnectStreamTls13, ReplaceRecordLayerAsyncEarlyAuth) {
+  StartConnect();
+  client_->SetServerKeyBits(server_->server_key_bits());
+
+  BadPrSocket bad_layer_client(client_);
+  BadPrSocket bad_layer_server(server_);
+  StagedRecords client_stage(client_);
+  StagedRecords server_stage(server_);
+
+  client_->SetAuthCertificateCallback(AuthCompleteBlock);
+
+  server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTING);
+  client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTING);
+
+  // Send a partial flight on to the client.
+  // This includes enough to trigger the certificate callback.
+  server_stage.ForwardPartial(client_);
+  EXPECT_TRUE(client_stage.empty());
+
+  // Declare the certificate good.
+  EXPECT_EQ(SECSuccess, SSL_AuthCertificateComplete(client_->ssl_fd(), 0));
+  client_->Handshake();
+  EXPECT_TRUE(client_stage.empty());
+
+  // Send the remainder of the server flight.
+  PRBool pending = PR_FALSE;
+  EXPECT_EQ(SECSuccess,
+            SSLInt_HasPendingHandshakeData(client_->ssl_fd(), &pending));
+  EXPECT_EQ(PR_TRUE, pending);
+  EXPECT_EQ(TlsAgent::STATE_CONNECTING, client_->state());
+  server_stage.ForwardAll(client_, TlsAgent::STATE_CONNECTED);
+  client_stage.ForwardAll(server_, TlsAgent::STATE_CONNECTED);
+  CheckKeys();
+
+  SendForwardReceive(server_, server_stage, client_);
+}
+
+TEST_P(TlsConnectStream, ForwardDataFromWrongEpoch) {
+  const uint8_t data[] = {1};
+  Connect();
+  uint16_t next_epoch;
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    EXPECT_EQ(SECFailure,
+              SSL_RecordLayerData(client_->ssl_fd(), 2, ssl_ct_application_data,
+                                  data, sizeof(data)));
+    EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError())
+        << "Passing data from an old epoch is rejected";
+    next_epoch = 4;
+  } else {
+    // Prior to TLS 1.3, the epoch is only updated once during the handshake.
+    next_epoch = 2;
+  }
+  EXPECT_EQ(SECFailure,
+            SSL_RecordLayerData(client_->ssl_fd(), next_epoch,
+                                ssl_ct_application_data, data, sizeof(data)));
+  EXPECT_EQ(PR_WOULD_BLOCK_ERROR, PORT_GetError())
+      << "Passing data from a future epoch blocks";
+}
+
+TEST_F(TlsConnectStreamTls13, ForwardInvalidData) {
+  const uint8_t data[1] = {0};
+
+  EnsureTlsSetup();
+  // Zero-length data.
+  EXPECT_EQ(SECFailure, SSL_RecordLayerData(client_->ssl_fd(), 0,
+                                            ssl_ct_application_data, data, 0));
+  EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
+
+  // NULL data.
+  EXPECT_EQ(SECFailure,
+            SSL_RecordLayerData(client_->ssl_fd(), 0, ssl_ct_application_data,
+                                nullptr, 1));
+  EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
+}
+
+TEST_F(TlsConnectDatagram13, ForwardDataDtls) {
+  EnsureTlsSetup();
+  const uint8_t data[1] = {0};
+  EXPECT_EQ(SECFailure,
+            SSL_RecordLayerData(client_->ssl_fd(), 0, ssl_ct_application_data,
+                                data, sizeof(data)));
+  EXPECT_EQ(SEC_ERROR_INVALID_ARGS, PORT_GetError());
+}
+
+}  // namespace nss_test
--- a/security/nss/gtests/ssl_gtest/ssl_recordsize_unittest.cc
+++ b/security/nss/gtests/ssl_gtest/ssl_recordsize_unittest.cc
@@ -118,60 +118,68 @@ TEST_P(TlsConnectGeneric, RecordSizeMaxi
   uint16_t max_record_size =
       (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) ? 16385 : 16384;
   size_t send_size = (version_ >= SSL_LIBRARY_VERSION_TLS_1_3)
                          ? max_record_size
                          : max_record_size + 1;
 
   EnsureTlsSetup();
   auto client_max = MakeTlsFilter<TlsRecordMaximum>(client_);
-  client_max->EnableDecryption();
   auto server_max = MakeTlsFilter<TlsRecordMaximum>(server_);
-  server_max->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    client_max->EnableDecryption();
+    server_max->EnableDecryption();
+  }
 
   Connect();
   client_->SendData(send_size, send_size);
   server_->SendData(send_size, send_size);
   server_->ReadBytes(send_size);
   client_->ReadBytes(send_size);
 
   CheckRecordSizes(client_, client_max, max_record_size);
   CheckRecordSizes(server_, server_max, max_record_size);
 }
 
 TEST_P(TlsConnectGeneric, RecordSizeMinimumClient) {
   EnsureTlsSetup();
   auto server_max = MakeTlsFilter<TlsRecordMaximum>(server_);
-  server_max->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    server_max->EnableDecryption();
+  }
 
   client_->SetOption(SSL_RECORD_SIZE_LIMIT, 64);
   Connect();
   SendReceive(127);  // Big enough for one record, allowing for 1+N splitting.
 
   CheckRecordSizes(server_, server_max, 64);
 }
 
 TEST_P(TlsConnectGeneric, RecordSizeMinimumServer) {
   EnsureTlsSetup();
   auto client_max = MakeTlsFilter<TlsRecordMaximum>(client_);
-  client_max->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    client_max->EnableDecryption();
+  }
 
   server_->SetOption(SSL_RECORD_SIZE_LIMIT, 64);
   Connect();
   SendReceive(127);
 
   CheckRecordSizes(client_, client_max, 64);
 }
 
 TEST_P(TlsConnectGeneric, RecordSizeAsymmetric) {
   EnsureTlsSetup();
   auto client_max = MakeTlsFilter<TlsRecordMaximum>(client_);
-  client_max->EnableDecryption();
   auto server_max = MakeTlsFilter<TlsRecordMaximum>(server_);
-  server_max->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    client_max->EnableDecryption();
+    server_max->EnableDecryption();
+  }
 
   client_->SetOption(SSL_RECORD_SIZE_LIMIT, 64);
   server_->SetOption(SSL_RECORD_SIZE_LIMIT, 100);
   Connect();
   SendReceive(127);
 
   CheckRecordSizes(client_, client_max, 100);
   CheckRecordSizes(server_, server_max, 64);
@@ -251,29 +259,31 @@ class TlsRecordPadder : public TlsRecord
  protected:
   PacketFilter::Action FilterRecord(const TlsRecordHeader& header,
                                     const DataBuffer& record, size_t* offset,
                                     DataBuffer* output) override {
     if (header.content_type() != ssl_ct_application_data) {
       return KEEP;
     }
 
+    uint16_t protection_epoch;
     uint8_t inner_content_type;
     DataBuffer plaintext;
-    if (!Unprotect(header, record, &inner_content_type, &plaintext)) {
+    if (!Unprotect(header, record, &protection_epoch, &inner_content_type,
+                   &plaintext)) {
       return KEEP;
     }
 
     if (inner_content_type != ssl_ct_application_data) {
       return KEEP;
     }
 
     DataBuffer ciphertext;
-    bool ok =
-        Protect(header, inner_content_type, plaintext, &ciphertext, padding_);
+    bool ok = Protect(spec(protection_epoch), header, inner_content_type,
+                      plaintext, &ciphertext, padding_);
     EXPECT_TRUE(ok);
     if (!ok) {
       return KEEP;
     }
     *offset = header.Write(output, *offset, ciphertext);
     return CHANGE;
   }
 
@@ -329,17 +339,19 @@ TEST_P(TlsConnectGeneric, RecordSizeGetV
 }
 
 // The value of the extension is capped by the maximum version of the client.
 TEST_P(TlsConnectGeneric, RecordSizeCapExtensionClient) {
   EnsureTlsSetup();
   client_->SetOption(SSL_RECORD_SIZE_LIMIT, 16385);
   auto capture =
       MakeTlsFilter<TlsExtensionCapture>(client_, ssl_record_size_limit_xtn);
-  capture->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    capture->EnableDecryption();
+  }
   Connect();
 
   uint64_t val = 0;
   EXPECT_TRUE(capture->extension().Read(0, 2, &val));
   if (version_ < SSL_LIBRARY_VERSION_TLS_1_3) {
     EXPECT_EQ(16384U, val) << "Extension should be capped";
   } else {
     EXPECT_EQ(16385U, val);
@@ -347,17 +359,19 @@ TEST_P(TlsConnectGeneric, RecordSizeCapE
 }
 
 // The value of the extension is capped by the maximum version of the server.
 TEST_P(TlsConnectGeneric, RecordSizeCapExtensionServer) {
   EnsureTlsSetup();
   server_->SetOption(SSL_RECORD_SIZE_LIMIT, 16385);
   auto capture =
       MakeTlsFilter<TlsExtensionCapture>(server_, ssl_record_size_limit_xtn);
-  capture->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    capture->EnableDecryption();
+  }
   Connect();
 
   uint64_t val = 0;
   EXPECT_TRUE(capture->extension().Read(0, 2, &val));
   if (version_ < SSL_LIBRARY_VERSION_TLS_1_3) {
     EXPECT_EQ(16384U, val) << "Extension should be capped";
   } else {
     EXPECT_EQ(16385U, val);
@@ -388,27 +402,31 @@ TEST_F(TlsConnectStreamTls13, RecordSize
 }
 
 TEST_P(TlsConnectGeneric, RecordSizeServerExtensionInvalid) {
   EnsureTlsSetup();
   server_->SetOption(SSL_RECORD_SIZE_LIMIT, 1000);
   static const uint8_t v[] = {0xf4, 0x1f};
   auto replace = MakeTlsFilter<TlsExtensionReplacer>(
       server_, ssl_record_size_limit_xtn, DataBuffer(v, sizeof(v)));
-  replace->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    replace->EnableDecryption();
+  }
   ConnectExpectAlert(client_, kTlsAlertIllegalParameter);
 }
 
 TEST_P(TlsConnectGeneric, RecordSizeServerExtensionExtra) {
   EnsureTlsSetup();
   server_->SetOption(SSL_RECORD_SIZE_LIMIT, 1000);
   static const uint8_t v[] = {0x01, 0x00, 0x00};
   auto replace = MakeTlsFilter<TlsExtensionReplacer>(
       server_, ssl_record_size_limit_xtn, DataBuffer(v, sizeof(v)));
-  replace->EnableDecryption();
+  if (version_ >= SSL_LIBRARY_VERSION_TLS_1_3) {
+    replace->EnableDecryption();
+  }
   ConnectExpectAlert(client_, kTlsAlertDecodeError);
 }
 
 class RecordSizeDefaultsTest : public ::testing::Test {
  public:
   void SetUp() {
     EXPECT_EQ(SECSuccess,
               SSL_OptionGetDefault(SSL_RECORD_SIZE_LIMIT, &default_));
--- a/security/nss/gtests/ssl_gtest/test_io.cc
+++ b/security/nss/gtests/ssl_gtest/test_io.cc
@@ -20,20 +20,23 @@ extern bool g_ssl_gtest_verbose;
 namespace nss_test {
 
 #define LOG(a) std::cerr << name_ << ": " << a << std::endl
 #define LOGV(a)                      \
   do {                               \
     if (g_ssl_gtest_verbose) LOG(a); \
   } while (false)
 
+PRDescIdentity DummyPrSocket::LayerId() {
+  static PRDescIdentity id = PR_GetUniqueIdentity("dummysocket");
+  return id;
+}
+
 ScopedPRFileDesc DummyPrSocket::CreateFD() {
-  static PRDescIdentity test_fd_identity =
-      PR_GetUniqueIdentity("testtransportadapter");
-  return DummyIOLayerMethods::CreateFD(test_fd_identity, this);
+  return DummyIOLayerMethods::CreateFD(DummyPrSocket::LayerId(), this);
 }
 
 void DummyPrSocket::Reset() {
   auto p = peer_.lock();
   peer_.reset();
   if (p) {
     p->peer_.reset();
     p->Reset();
@@ -131,29 +134,28 @@ int32_t DummyPrSocket::Write(PRFileDesc 
     return -1;
   }
 
   DataBuffer packet(static_cast<const uint8_t *>(buf),
                     static_cast<size_t>(length));
   DataBuffer filtered;
   PacketFilter::Action action = PacketFilter::KEEP;
   if (filter_) {
+    LOGV("Original packet: " << packet);
     action = filter_->Process(packet, &filtered);
   }
   switch (action) {
     case PacketFilter::CHANGE:
-      LOG("Original packet: " << packet);
       LOG("Filtered packet: " << filtered);
       dst->PacketReceived(filtered);
       break;
     case PacketFilter::DROP:
-      LOG("Droppped packet: " << packet);
+      LOG("Drop packet");
       break;
     case PacketFilter::KEEP:
-      LOGV("Packet: " << packet);
       dst->PacketReceived(packet);
       break;
   }
   // libssl can't handle it if this reports something other than the length
   // of what was passed in (or less, but we're not doing partial writes).
   return static_cast<int32_t>(packet.len());
 }
 
--- a/security/nss/gtests/ssl_gtest/test_io.h
+++ b/security/nss/gtests/ssl_gtest/test_io.h
@@ -63,16 +63,18 @@ class DummyPrSocket : public DummyIOLaye
       : name_(name),
         variant_(var),
         peer_(),
         input_(),
         filter_(nullptr),
         write_error_(0) {}
   virtual ~DummyPrSocket() {}
 
+  static PRDescIdentity LayerId();
+
   // Create a file descriptor that will reference this object.  The fd must not
   // live longer than this adapter; call PR_Close() before.
   ScopedPRFileDesc CreateFD();
 
   std::weak_ptr<DummyPrSocket>& peer() { return peer_; }
   void SetPeer(const std::shared_ptr<DummyPrSocket>& p) { peer_ = p; }
   void SetPacketFilter(const std::shared_ptr<PacketFilter>& filter) {
     filter_ = filter;
--- a/security/nss/gtests/ssl_gtest/tls_agent.cc
+++ b/security/nss/gtests/ssl_gtest/tls_agent.cc
@@ -635,16 +635,26 @@ void TlsAgent::CheckAlpn(SSLNextProtoSta
   if (alpn_state == SSL_NEXT_PROTO_NO_SUPPORT) {
     EXPECT_EQ("", expected);
   } else {
     EXPECT_NE("", expected);
     EXPECT_EQ(expected, std::string(chosen, chosen_len));
   }
 }
 
+void TlsAgent::CheckEpochs(uint16_t expected_read,
+                           uint16_t expected_write) const {
+  uint16_t read_epoch = 0;
+  uint16_t write_epoch = 0;
+  EXPECT_EQ(SECSuccess,
+            SSL_GetCurrentEpoch(ssl_fd(), &read_epoch, &write_epoch));
+  EXPECT_EQ(expected_read, read_epoch) << role_str() << " read epoch";
+  EXPECT_EQ(expected_write, write_epoch) << role_str() << " write epoch";
+}
+
 void TlsAgent::EnableSrtp() {
   EXPECT_TRUE(EnsureTlsSetup());
   const uint16_t ciphers[] = {SRTP_AES128_CM_HMAC_SHA1_80,
                               SRTP_AES128_CM_HMAC_SHA1_32};
   EXPECT_EQ(SECSuccess,
             SSL_SetSRTPCiphers(ssl_fd(), ciphers, PR_ARRAY_SIZE(ciphers)));
 }
 
--- a/security/nss/gtests/ssl_gtest/tls_agent.h
+++ b/security/nss/gtests/ssl_gtest/tls_agent.h
@@ -134,16 +134,17 @@ class TlsAgent : public PollTarget {
   void ExpectResumption();
   void SkipVersionChecks();
   void SetSignatureSchemes(const SSLSignatureScheme* schemes, size_t count);
   void EnableAlpn(const uint8_t* val, size_t len);
   void CheckAlpn(SSLNextProtoState expected_state,
                  const std::string& expected = "") const;
   void EnableSrtp();
   void CheckSrtp() const;
+  void CheckEpochs(uint16_t expected_read, uint16_t expected_write) const;
   void CheckErrorCode(int32_t expected) const;
   void WaitForErrorCode(int32_t expected, uint32_t delay) const;
   // Send data on the socket, encrypting it.
   void SendData(size_t bytes, size_t blocksize = 1024);
   void SendBuffer(const DataBuffer& buf);
   bool SendEncryptedRecord(const std::shared_ptr<TlsCipherSpec>& spec,
                            uint64_t seq, uint8_t ct, const DataBuffer& buf);
   // Send data directly to the underlying socket, skipping the TLS layer.
--- a/security/nss/gtests/ssl_gtest/tls_connect.cc
+++ b/security/nss/gtests/ssl_gtest/tls_connect.cc
@@ -162,28 +162,18 @@ void TlsConnectTestBase::CheckShares(
     check_group(group);
     ASSERT_TRUE(shares.Read(i + 2, 2, &tmp));
   }
   EXPECT_EQ(shares.len(), i);
 }
 
 void TlsConnectTestBase::CheckEpochs(uint16_t client_epoch,
                                      uint16_t server_epoch) const {
-  uint16_t read_epoch = 0;
-  uint16_t write_epoch = 0;
-
-  EXPECT_EQ(SECSuccess,
-            SSLInt_GetEpochs(client_->ssl_fd(), &read_epoch, &write_epoch));
-  EXPECT_EQ(server_epoch, read_epoch) << "client read epoch";
-  EXPECT_EQ(client_epoch, write_epoch) << "client write epoch";
-
-  EXPECT_EQ(SECSuccess,
-            SSLInt_GetEpochs(server_->ssl_fd(), &read_epoch, &write_epoch));
-  EXPECT_EQ(client_epoch, read_epoch) << "server read epoch";
-  EXPECT_EQ(server_epoch, write_epoch) << "server write epoch";
+  client_->CheckEpochs(server_epoch, client_epoch);
+  server_->CheckEpochs(client_epoch, server_epoch);
 }
 
 void TlsConnectTestBase::ClearStats() {
   // Clear statistics.
   SSL3Statistics* stats = SSL_GetStatistics();
   memset(stats, 0, sizeof(*stats));
 }
 
--- a/security/nss/gtests/ssl_gtest/tls_filter.cc
+++ b/security/nss/gtests/ssl_gtest/tls_filter.cc
@@ -40,107 +40,143 @@ void TlsVersioned::WriteStream(std::ostr
       stream << "1.3";
       break;
     default:
       stream << "Invalid version: " << version();
       break;
   }
 }
 
+TlsRecordFilter::TlsRecordFilter(const std::shared_ptr<TlsAgent>& a)
+    : agent_(a) {
+  cipher_specs_.emplace_back(a->variant() == ssl_variant_datagram, 0);
+}
+
 void TlsRecordFilter::EnableDecryption() {
-  SSLInt_SetCipherSpecChangeFunc(agent()->ssl_fd(), CipherSpecChanged,
-                                 (void*)this);
+  EXPECT_EQ(SECSuccess,
+            SSL_SecretCallback(agent()->ssl_fd(), SecretCallback, this));
+  decrypting_ = true;
 }
 
-void TlsRecordFilter::CipherSpecChanged(void* arg, PRBool sending,
-                                        ssl3CipherSpec* newSpec) {
+void TlsRecordFilter::SecretCallback(PRFileDesc* fd, PRUint16 epoch,
+                                     SSLSecretDirection dir, PK11SymKey* secret,
+                                     void* arg) {
   TlsRecordFilter* self = static_cast<TlsRecordFilter*>(arg);
-  PRBool isServer = self->agent()->role() == TlsAgent::SERVER;
-
   if (g_ssl_gtest_verbose) {
-    std::cerr << (isServer ? "server" : "client") << ": "
-              << (sending ? "send" : "receive")
-              << " cipher spec changed:  " << newSpec->epoch << " ("
-              << newSpec->phase << ")" << std::endl;
+    std::cerr << self->agent()->role_str() << ": " << dir
+              << " secret changed for epoch " << epoch << std::endl;
   }
-  if (!sending) {
+
+  if (dir == ssl_secret_read) {
     return;
   }
 
-  uint64_t seq_no;
-  if (self->agent()->variant() == ssl_variant_datagram) {
-    seq_no = static_cast<uint64_t>(SSLInt_CipherSpecToEpoch(newSpec)) << 48;
+  for (auto& spec : self->cipher_specs_) {
+    ASSERT_NE(spec.epoch(), epoch) << "duplicate spec for epoch " << epoch;
+  }
+
+  SSLPreliminaryChannelInfo preinfo;
+  EXPECT_EQ(SECSuccess,
+            SSL_GetPreliminaryChannelInfo(self->agent()->ssl_fd(), &preinfo,
+                                          sizeof(preinfo)));
+  EXPECT_EQ(sizeof(preinfo), preinfo.length);
+
+  // Check the version.
+  if (preinfo.valuesSet & ssl_preinfo_version) {
+    EXPECT_EQ(SSL_LIBRARY_VERSION_TLS_1_3, preinfo.protocolVersion);
   } else {
-    seq_no = 0;
+    EXPECT_EQ(1U, epoch);
   }
-  self->in_sequence_number_ = seq_no;
-  self->out_sequence_number_ = seq_no;
-  self->dropped_record_ = false;
-  self->cipher_spec_.reset(new TlsCipherSpec());
-  bool ret = self->cipher_spec_->Init(
-      SSLInt_CipherSpecToEpoch(newSpec), SSLInt_CipherSpecToAlgorithm(newSpec),
-      SSLInt_CipherSpecToKey(newSpec), SSLInt_CipherSpecToIv(newSpec));
-  EXPECT_EQ(true, ret);
+
+  uint16_t suite;
+  if (epoch == 1) {
+    // 0-RTT
+    EXPECT_TRUE(preinfo.valuesSet & ssl_preinfo_0rtt_cipher_suite);
+    suite = preinfo.zeroRttCipherSuite;
+  } else {
+    EXPECT_TRUE(preinfo.valuesSet & ssl_preinfo_cipher_suite);
+    suite = preinfo.cipherSuite;
+  }
+
+  SSLCipherSuiteInfo cipherinfo;
+  EXPECT_EQ(SECSuccess,
+            SSL_GetCipherSuiteInfo(suite, &cipherinfo, sizeof(cipherinfo)));
+  EXPECT_EQ(sizeof(cipherinfo), cipherinfo.length);
+
+  bool is_dtls = self->agent()->variant() == ssl_variant_datagram;
+  self->cipher_specs_.emplace_back(is_dtls, epoch);
+  EXPECT_TRUE(self->cipher_specs_.back().SetKeys(&cipherinfo, secret));
 }
 
 bool TlsRecordFilter::is_dtls13() const {
   if (agent()->variant() != ssl_variant_datagram) {
     return false;
   }
   if (agent()->state() == TlsAgent::STATE_CONNECTED) {
     return agent()->version() >= SSL_LIBRARY_VERSION_TLS_1_3;
   }
   SSLPreliminaryChannelInfo info;
   EXPECT_EQ(SECSuccess, SSL_GetPreliminaryChannelInfo(agent()->ssl_fd(), &info,
                                                       sizeof(info)));
   return (info.protocolVersion >= SSL_LIBRARY_VERSION_TLS_1_3) ||
          info.canSendEarlyData;
 }
 
+// Gets the cipher spec that matches the specified epoch.
+TlsCipherSpec& TlsRecordFilter::spec(uint16_t write_epoch) {
+  for (auto& sp : cipher_specs_) {
+    if (sp.epoch() == write_epoch) {
+      return sp;
+    }
+  }
+
+  // If we aren't decrypting, provide a cipher spec that does nothing other than
+  // count sequence numbers.
+  EXPECT_FALSE(decrypting_) << "No spec available for epoch " << write_epoch;
+  ;
+  bool is_dtls = agent()->variant() == ssl_variant_datagram;
+  cipher_specs_.emplace_back(is_dtls, write_epoch);
+  return cipher_specs_.back();
+}
+
 PacketFilter::Action TlsRecordFilter::Filter(const DataBuffer& input,
                                              DataBuffer* output) {
   // Disable during shutdown.
   if (!agent()) {
     return KEEP;
   }
 
   bool changed = false;
   size_t offset = 0U;
 
   output->Allocate(input.len());
   TlsParser parser(input);
 
+  // This uses the current write spec for the purposes of parsing the epoch and
+  // sequence number from the header.  This might be wrong because we can
+  // receive records from older specs, but guessing is good enough:
+  // - In DTLS, parsing the sequence number corrects any errors.
+  // - In TLS, we don't use the sequence number unless decrypting, where we use
+  //   trial decryption to get the right epoch.
+  uint16_t write_epoch = 0;
+  SECStatus rv = SSL_GetCurrentEpoch(agent()->ssl_fd(), nullptr, &write_epoch);
+  if (rv != SECSuccess) {
+    ADD_FAILURE() << "unable to read epoch";
+    return KEEP;
+  }
+  uint64_t guess_seqno = static_cast<uint64_t>(write_epoch) << 48;
+
   while (parser.remaining()) {
     TlsRecordHeader header;
     DataBuffer record;
-
-    if (!header.Parse(is_dtls13(), in_sequence_number_, &parser, &record)) {
+    if (!header.Parse(is_dtls13(), guess_seqno, &parser, &record)) {
       ADD_FAILURE() << "not a valid record";
       return KEEP;
     }
 
-    // Track the sequence number, which is necessary for stream mode when
-    // decrypting and for TLS 1.3 datagram to recover the sequence number.
-    //
-    // We reset the counter when the cipher spec changes, but that notification
-    // appears before a record is sent.  If multiple records are sent with
-    // different cipher specs, this would fail.  This filters out cleartext
-    // records, so we don't get confused by handshake messages that are sent at
-    // the same time as encrypted records.  Sequence numbers are therefore
-    // likely to be incorrect for cleartext records.
-    //
-    // This isn't perfectly robust: if there is a change from an active cipher
-    // spec to another active cipher spec (KeyUpdate for instance) AND writes
-    // are consolidated across that change, this code could use the wrong
-    // sequence numbers when re-encrypting records with the old keys.
-    if (header.content_type() == ssl_ct_application_data) {
-      in_sequence_number_ =
-          (std::max)(in_sequence_number_, header.sequence_number() + 1);
-    }
-
     if (FilterRecord(header, record, &offset, output) != KEEP) {
       changed = true;
     } else {
       offset = header.Write(output, offset, record);
     }
   }
   output->Truncate(offset);
 
@@ -154,62 +190,65 @@ PacketFilter::Action TlsRecordFilter::Fi
 }
 
 PacketFilter::Action TlsRecordFilter::FilterRecord(
     const TlsRecordHeader& header, const DataBuffer& record, size_t* offset,
     DataBuffer* output) {
   DataBuffer filtered;
   uint8_t inner_content_type;
   DataBuffer plaintext;
+  uint16_t protection_epoch = 0;
 
-  if (!Unprotect(header, record, &inner_content_type, &plaintext)) {
-    if (g_ssl_gtest_verbose) {
-      std::cerr << "unprotect failed: " << header << ":" << record << std::endl;
-    }
+  if (!Unprotect(header, record, &protection_epoch, &inner_content_type,
+                 &plaintext)) {
+    std::cerr << agent()->role_str() << ": unprotect failed: " << header << ":"
+              << record << std::endl;
     return KEEP;
   }
 
+  auto& protection_spec = spec(protection_epoch);
   TlsRecordHeader real_header(header.variant(), header.version(),
                               inner_content_type, header.sequence_number());
 
   PacketFilter::Action action = FilterRecord(real_header, plaintext, &filtered);
   // In stream mode, even if something doesn't change we need to re-encrypt if
   // previous packets were dropped.
   if (action == KEEP) {
-    if (header.is_dtls() || !dropped_record_) {
+    if (header.is_dtls() || !protection_spec.record_dropped()) {
+      // Count every outgoing packet.
+      protection_spec.RecordProtected();
       return KEEP;
     }
     filtered = plaintext;
   }
 
   if (action == DROP) {
     std::cerr << "record drop: " << header << ":" << record << std::endl;
-    dropped_record_ = true;
+    protection_spec.RecordDropped();
     return DROP;
   }
 
   EXPECT_GT(0x10000U, filtered.len());
   if (action != KEEP) {
     std::cerr << "record old: " << plaintext << std::endl;
     std::cerr << "record new: " << filtered << std::endl;
   }
 
-  uint64_t seq_num;
-  if (header.is_dtls() || !cipher_spec_ ||
-      header.content_type() != ssl_ct_application_data) {
-    seq_num = header.sequence_number();
-  } else {
-    seq_num = out_sequence_number_++;
+  uint64_t seq_num = protection_spec.next_out_seqno();
+  if (!decrypting_ && header.is_dtls()) {
+    // Copy over the epoch, which isn't tracked when not decrypting.
+    seq_num |= header.sequence_number() & (0xffffULL << 48);
   }
+
   TlsRecordHeader out_header(header.variant(), header.version(),
                              header.content_type(), seq_num);
 
   DataBuffer ciphertext;
-  bool rv = Protect(out_header, inner_content_type, filtered, &ciphertext);
-  EXPECT_TRUE(rv);
+  bool rv = Protect(protection_spec, out_header, inner_content_type, filtered,
+                    &ciphertext);
   if (!rv) {
     return KEEP;
   }
   *offset = out_header.Write(output, *offset, ciphertext);
   return CHANGE;
 }
 
 size_t TlsRecordHeader::header_length() const {
@@ -222,25 +261,30 @@ size_t TlsRecordHeader::header_length() 
   DataBuffer buf;
   return WriteHeader(&buf, 0, 0);
 }
 
 uint64_t TlsRecordHeader::RecoverSequenceNumber(uint64_t expected,
                                                 uint32_t partial,
                                                 size_t partial_bits) {
   EXPECT_GE(32U, partial_bits);
-  uint64_t mask = (1 << partial_bits) - 1;
+  uint64_t mask = (1ULL << partial_bits) - 1;
   // First we determine the highest possible value.  This is half the
-  // expressible range above the expected value.
-  uint64_t cap = expected + (1ULL << (partial_bits - 1));
+  // expressible range above the expected value, less 1.
+  //
+  // We subtract the extra 1 from the cap so that when given a choice between
+  // the equidistant expected+N and expected-N we want to chose the lower.  With
+  // 0-RTT, we sometimes have to recover an epoch of 1 when we expect an epoch
+  // of 3 and with 2 partial bits, the alternative result of 5 is wrong.
+  uint64_t cap = expected + (1ULL << (partial_bits - 1)) - 1;
   // Add the partial piece in.  e.g., xxxx789a and 1234 becomes xxxx1234.
   uint64_t seq_no = (cap & ~mask) | partial;
   // If the partial value is higher than the same partial piece from the cap,
   // then the real value has to be lower.  e.g., xxxx1234 can't become xxxx5678.
-  if (partial > (cap & mask)) {
+  if (partial > (cap & mask) && (seq_no >= (1ULL << partial_bits))) {
     seq_no -= 1ULL << partial_bits;
   }
   return seq_no;
 }
 
 // Determine the full epoch and sequence number from an expected and raw value.
 // The expected and output values are packed as they are in DTLS 1.2 and
 // earlier: with 16 bits of epoch and 48 bits of sequence number.
@@ -370,64 +414,101 @@ size_t TlsRecordHeader::Write(DataBuffer
                               const DataBuffer& body) const {
   offset = WriteHeader(buffer, offset, body.len());
   offset = buffer->Write(offset, body);
   return offset;
 }
 
 bool TlsRecordFilter::Unprotect(const TlsRecordHeader& header,
                                 const DataBuffer& ciphertext,
+                                uint16_t* protection_epoch,
                                 uint8_t* inner_content_type,
                                 DataBuffer* plaintext) {
-  if (!cipher_spec_ || header.content_type() != ssl_ct_application_data) {
+  if (!decrypting_ || header.content_type() != ssl_ct_application_data) {
+    // Maintain the epoch and sequence number for plaintext records.
+    uint16_t ep = 0;
+    if (agent()->variant() == ssl_variant_datagram) {
+      ep = static_cast<uint16_t>(header.sequence_number() >> 48);
+    }
+    spec(ep).RecordUnprotected(header.sequence_number());
+    *protection_epoch = ep;
     *inner_content_type = header.content_type();
     *plaintext = ciphertext;
     return true;
   }
 
-  if (!cipher_spec_->Unprotect(header, ciphertext, plaintext)) {
-    return false;
+  uint16_t ep = 0;
+  if (agent()->variant() == ssl_variant_datagram) {
+    ep = static_cast<uint16_t>(header.sequence_number() >> 48);
+    if (!spec(ep).Unprotect(header, ciphertext, plaintext)) {
+      return false;
+    }
+  } else {
+    // In TLS, records aren't clearly labelled with their epoch, and we
+    // can't just use the newest keys because the same flight of messages can
+    // contain multiple epochs. So... trial decrypt!
+    for (size_t i = cipher_specs_.size() - 1; i > 0; --i) {
+      if (cipher_specs_[i].Unprotect(header, ciphertext, plaintext)) {
+        ep = cipher_specs_[i].epoch();
+        break;
+      }
+    }
+    if (!ep) {
+      return false;
+    }
   }
 
   size_t len = plaintext->len();
   while (len > 0 && !plaintext->data()[len - 1]) {
     --len;
   }
   if (!len) {
     // Bogus padding.
     return false;
   }
 
+  *protection_epoch = ep;
   *inner_content_type = plaintext->data()[len - 1];
   plaintext->Truncate(len - 1);
   if (g_ssl_gtest_verbose) {
-    std::cerr << "unprotect: " << std::hex << header.sequence_number()
-              << std::dec << " type=" << static_cast<int>(*inner_content_type)
+    std::cerr << agent()->role_str() << ": unprotect: epoch=" << ep
+              << " seq=" << std::hex << header.sequence_number() << std::dec
               << " " << *plaintext << std::endl;
   }
 
   return true;
 }
 
-bool TlsRecordFilter::Protect(const TlsRecordHeader& header,
+bool TlsRecordFilter::Protect(TlsCipherSpec& protection_spec,
+                              const TlsRecordHeader& header,
                               uint8_t inner_content_type,
                               const DataBuffer& plaintext,
                               DataBuffer* ciphertext, size_t padding) {
-  if (!cipher_spec_ || header.content_type() != ssl_ct_application_data) {
+  if (!protection_spec.is_protected()) {
+    // Not protected, just keep the sequence numbers updated.
+    protection_spec.RecordProtected();
     *ciphertext = plaintext;
     return true;
   }
-  if (g_ssl_gtest_verbose) {
-    std::cerr << "protect: " << header.sequence_number() << std::endl;
-  }
+
   DataBuffer padded;
   padded.Allocate(plaintext.len() + 1 + padding);
   size_t offset = padded.Write(0, plaintext.data(), plaintext.len());
   padded.Write(offset, inner_content_type, 1);
-  return cipher_spec_->Protect(header, padded, ciphertext);
+
+  bool ok = protection_spec.Protect(header, padded, ciphertext);
+  if (!ok) {
+    ADD_FAILURE() << "protect fail";
+  } else if (g_ssl_gtest_verbose) {
+    std::cerr << agent()->role_str()
+              << ": protect: epoch=" << protection_spec.epoch()
+              << " seq=" << std::hex << header.sequence_number() << std::dec
+              << " " << *ciphertext << std::endl;
+  }
+  return ok;
 }
 
 bool IsHelloRetry(const DataBuffer& body) {
   static const uint8_t ssl_hello_retry_random[] = {
       0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C,
       0x02, 0x1E, 0x65, 0xB8, 0x91, 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB,
       0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C};
   return memcmp(body.data() + 2, ssl_hello_retry_random,
--- a/security/nss/gtests/ssl_gtest/tls_filter.h
+++ b/security/nss/gtests/ssl_gtest/tls_filter.h
@@ -92,41 +92,36 @@ inline std::shared_ptr<T> MakeTlsFilter(
   auto filter = std::make_shared<T>(agent, std::forward<Args>(args)...);
   agent->SetFilter(filter);
   return filter;
 }
 
 // Abstract filter that operates on entire (D)TLS records.
 class TlsRecordFilter : public PacketFilter {
  public:
-  TlsRecordFilter(const std::shared_ptr<TlsAgent>& a)
-      : agent_(a),
-        count_(0),
-        cipher_spec_(),
-        dropped_record_(false),
-        in_sequence_number_(0),
-        out_sequence_number_(0) {}
+  TlsRecordFilter(const std::shared_ptr<TlsAgent>& a);
 
   std::shared_ptr<TlsAgent> agent() const { return agent_.lock(); }
 
   // External interface. Overrides PacketFilter.
   PacketFilter::Action Filter(const DataBuffer& input, DataBuffer* output);
 
   // Report how many packets were altered by the filter.
   size_t filtered_packets() const { return count_; }
 
   // Enable decryption. This only works properly for TLS 1.3 and above.
   // Enabling it for lower version tests will cause undefined
   // behavior.
   void EnableDecryption();
   bool Unprotect(const TlsRecordHeader& header, const DataBuffer& cipherText,
-                 uint8_t* inner_content_type, DataBuffer* plaintext);
-  bool Protect(const TlsRecordHeader& header, uint8_t inner_content_type,
-               const DataBuffer& plaintext, DataBuffer* ciphertext,
-               size_t padding = 0);
+                 uint16_t* protection_epoch, uint8_t* inner_content_type,
+                 DataBuffer* plaintext);
+  bool Protect(TlsCipherSpec& protection_spec, const TlsRecordHeader& header,
+               uint8_t inner_content_type, const DataBuffer& plaintext,
+               DataBuffer* ciphertext, size_t padding = 0);
 
  protected:
   // There are two filter functions which can be overriden. Both are
   // called with the header and the record but the outer one is called
   // with a raw pointer to let you write into the buffer and lets you
   // do anything with this section of the stream. The inner one
   // just lets you change the record contents. By default, the
   // outer one calls the inner one, so if you override the outer
@@ -141,30 +136,27 @@ class TlsRecordFilter : public PacketFil
   // outparam with the new record contents if it chooses to CHANGE the record.
   virtual PacketFilter::Action FilterRecord(const TlsRecordHeader& header,
                                             const DataBuffer& data,
                                             DataBuffer* changed) {
     return KEEP;
   }
 
   bool is_dtls13() const;
+  TlsCipherSpec& spec(uint16_t epoch);
 
  private:
-  static void CipherSpecChanged(void* arg, PRBool sending,
-                                ssl3CipherSpec* newSpec);
+  static void SecretCallback(PRFileDesc* fd, PRUint16 epoch,
+                             SSLSecretDirection dir, PK11SymKey* secret,
+                             void* arg);
 
   std::weak_ptr<TlsAgent> agent_;
-  size_t count_;
-  std::unique_ptr<TlsCipherSpec> cipher_spec_;
-  // Whether we dropped a record since the cipher spec changed.
-  bool dropped_record_;
-  // The sequence number we use for reading records as they are written.
-  uint64_t in_sequence_number_;
-  // The sequence number we use for writing modified records.
-  uint64_t out_sequence_number_;
+  size_t count_ = 0;
+  std::vector<TlsCipherSpec> cipher_specs_;
+  bool decrypting_ = false;
 };
 
 inline std::ostream& operator<<(std::ostream& stream, const TlsVersioned& v) {
   v.WriteStream(stream);
   return stream;
 }
 
 inline std::ostream& operator<<(std::ostream& stream,
--- a/security/nss/gtests/ssl_gtest/tls_protect.cc
+++ b/security/nss/gtests/ssl_gtest/tls_protect.cc
@@ -2,151 +2,218 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "tls_protect.h"
 #include "tls_filter.h"
 
+// Do this to avoid having to re-implement HKDF.
+#include "tls13hkdf.h"
+
 namespace nss_test {
 
 AeadCipher::~AeadCipher() {
   if (key_) {
     PK11_FreeSymKey(key_);
   }
 }
 
-bool AeadCipher::Init(PK11SymKey *key, const uint8_t *iv) {
-  key_ = PK11_ReferenceSymKey(key);
+bool AeadCipher::Init(PK11SymKey* key, const uint8_t* iv) {
+  key_ = key;
   if (!key_) return false;
 
   memcpy(iv_, iv, sizeof(iv_));
+  if (g_ssl_gtest_verbose) {
+    EXPECT_EQ(SECSuccess, PK11_ExtractKeyValue(key_));
+    SECItem* raw_key = PK11_GetKeyData(key_);
+    std::cerr << "key: " << DataBuffer(raw_key->data, raw_key->len)
+              << std::endl;
+    std::cerr << "iv: " << DataBuffer(iv_, 12) << std::endl;
+  }
   return true;
 }
 
-void AeadCipher::FormatNonce(uint64_t seq, uint8_t *nonce) {
+void AeadCipher::FormatNonce(uint64_t seq, uint8_t* nonce) {
   memcpy(nonce, iv_, 12);
 
   for (size_t i = 0; i < 8; ++i) {
     nonce[12 - (i + 1)] ^= seq & 0xff;
     seq >>= 8;
   }
-
-  DataBuffer d(nonce, 12);
 }
 
-bool AeadCipher::AeadInner(bool decrypt, void *params, size_t param_length,
-                           const uint8_t *in, size_t inlen, uint8_t *out,
-                           size_t *outlen, size_t maxlen) {
+bool AeadCipher::AeadInner(bool decrypt, void* params, size_t param_length,
+                           const uint8_t* in, size_t inlen, uint8_t* out,
+                           size_t* outlen, size_t maxlen) {
   SECStatus rv;
   unsigned int uoutlen = 0;
   SECItem param = {
-      siBuffer, static_cast<unsigned char *>(params),
+      siBuffer, static_cast<unsigned char*>(params),
       static_cast<unsigned int>(param_length),
   };
 
   if (decrypt) {
     rv = PK11_Decrypt(key_, mech_, &param, out, &uoutlen, maxlen, in, inlen);
   } else {
     rv = PK11_Encrypt(key_, mech_, &param, out, &uoutlen, maxlen, in, inlen);
   }
   *outlen = (int)uoutlen;
 
   return rv == SECSuccess;
 }
 
-bool AeadCipherAesGcm::Aead(bool decrypt, const uint8_t *hdr, size_t hdr_len,
-                            uint64_t seq, const uint8_t *in, size_t inlen,
-                            uint8_t *out, size_t *outlen, size_t maxlen) {
+bool AeadCipherAesGcm::Aead(bool decrypt, const uint8_t* hdr, size_t hdr_len,
+                            uint64_t seq, const uint8_t* in, size_t inlen,
+                            uint8_t* out, size_t* outlen, size_t maxlen) {
   CK_GCM_PARAMS aeadParams;
   unsigned char nonce[12];
 
   memset(&aeadParams, 0, sizeof(aeadParams));
   aeadParams.pIv = nonce;
   aeadParams.ulIvLen = sizeof(nonce);
-  aeadParams.pAAD = const_cast<uint8_t *>(hdr);
+  aeadParams.pAAD = const_cast<uint8_t*>(hdr);
   aeadParams.ulAADLen = hdr_len;
   aeadParams.ulTagBits = 128;
 
   FormatNonce(seq, nonce);
-  return AeadInner(decrypt, (unsigned char *)&aeadParams, sizeof(aeadParams),
-                   in, inlen, out, outlen, maxlen);
+  return AeadInner(decrypt, (unsigned char*)&aeadParams, sizeof(aeadParams), in,
+                   inlen, out, outlen, maxlen);
 }
 
-bool AeadCipherChacha20Poly1305::Aead(bool decrypt, const uint8_t *hdr,
+bool AeadCipherChacha20Poly1305::Aead(bool decrypt, const uint8_t* hdr,
                                       size_t hdr_len, uint64_t seq,
-                                      const uint8_t *in, size_t inlen,
-                                      uint8_t *out, size_t *outlen,
+                                      const uint8_t* in, size_t inlen,
+                                      uint8_t* out, size_t* outlen,
                                       size_t maxlen) {
   CK_NSS_AEAD_PARAMS aeadParams;
   unsigned char nonce[12];
 
   memset(&aeadParams, 0, sizeof(aeadParams));
   aeadParams.pNonce = nonce;
   aeadParams.ulNonceLen = sizeof(nonce);
-  aeadParams.pAAD = const_cast<uint8_t *>(hdr);
+  aeadParams.pAAD = const_cast<uint8_t*>(hdr);
   aeadParams.ulAADLen = hdr_len;
   aeadParams.ulTagLen = 16;
 
   FormatNonce(seq, nonce);
-  return AeadInner(decrypt, (unsigned char *)&aeadParams, sizeof(aeadParams),
-                   in, inlen, out, outlen, maxlen);
+  return AeadInner(decrypt, (unsigned char*)&aeadParams, sizeof(aeadParams), in,
+                   inlen, out, outlen, maxlen);
+}
+
+static uint64_t FirstSeqno(bool dtls, uint16_t epoc) {
+  if (dtls) {
+    return static_cast<uint64_t>(epoc) << 48;
+  }
+  return 0;
 }
 
-bool TlsCipherSpec::Init(uint16_t epoc, SSLCipherAlgorithm cipher,
-                         PK11SymKey *key, const uint8_t *iv) {
-  epoch_ = epoc;
-  switch (cipher) {
+TlsCipherSpec::TlsCipherSpec(bool dtls, uint16_t epoc)
+    : dtls_(dtls),
+      epoch_(epoc),
+      in_seqno_(FirstSeqno(dtls, epoc)),
+      out_seqno_(FirstSeqno(dtls, epoc)) {}
+
+bool TlsCipherSpec::SetKeys(SSLCipherSuiteInfo* cipherinfo,
+                            PK11SymKey* secret) {
+  CK_MECHANISM_TYPE mech;
+  switch (cipherinfo->symCipher) {
     case ssl_calg_aes_gcm:
       aead_.reset(new AeadCipherAesGcm());
+      mech = CKM_AES_GCM;
       break;
     case ssl_calg_chacha20:
       aead_.reset(new AeadCipherChacha20Poly1305());
+      mech = CKM_NSS_CHACHA20_POLY1305;
       break;
     default:
       return false;
   }
 
+  PK11SymKey* key;
+  const std::string kPurposeKey = "key";
+  SECStatus rv = tls13_HkdfExpandLabel(
+      secret, cipherinfo->kdfHash, NULL, 0, kPurposeKey.c_str(),
+      kPurposeKey.length(), mech, cipherinfo->symKeyBits / 8, &key);
+  if (rv != SECSuccess) {
+    ADD_FAILURE() << "unable to derive key for epoch " << epoch_;
+    return false;
+  }
+
+  // No constant for IV length, but everything we know of uses 12.
+  uint8_t iv[12];
+  const std::string kPurposeIv = "iv";
+  rv = tls13_HkdfExpandLabelRaw(secret, cipherinfo->kdfHash, NULL, 0,
+                                kPurposeIv.c_str(), kPurposeIv.length(), iv,
+                                sizeof(iv));
+  if (rv != SECSuccess) {
+    ADD_FAILURE() << "unable to derive IV for epoch " << epoch_;
+    return false;
+  }
+
   return aead_->Init(key, iv);
 }
 
-bool TlsCipherSpec::Unprotect(const TlsRecordHeader &header,
-                              const DataBuffer &ciphertext,
-                              DataBuffer *plaintext) {
+bool TlsCipherSpec::Unprotect(const TlsRecordHeader& header,
+                              const DataBuffer& ciphertext,
+                              DataBuffer* plaintext) {
+  if (aead_ == nullptr) {
+    return false;
+  }
   // Make space.
   plaintext->Allocate(ciphertext.len());
 
   auto header_bytes = header.header();
   size_t len;
-  bool ret =
-      aead_->Aead(true, header_bytes.data(), header_bytes.len(),
-                  header.sequence_number(), ciphertext.data(), ciphertext.len(),
-                  plaintext->data(), &len, plaintext->len());
-  if (!ret) return false;
+  uint64_t seqno;
+  if (dtls_) {
+    seqno = header.sequence_number();
+  } else {
+    seqno = in_seqno_;
+  }
+  bool ret = aead_->Aead(true, header_bytes.data(), header_bytes.len(), seqno,
+                         ciphertext.data(), ciphertext.len(), plaintext->data(),
+                         &len, plaintext->len());
+  if (!ret) {
+    return false;
+  }
 
+  RecordUnprotected(seqno);
   plaintext->Truncate(len);
 
   return true;
 }
 
-bool TlsCipherSpec::Protect(const TlsRecordHeader &header,
-                            const DataBuffer &plaintext,
-                            DataBuffer *ciphertext) {
+bool TlsCipherSpec::Protect(const TlsRecordHeader& header,
+                            const DataBuffer& plaintext,
+                            DataBuffer* ciphertext) {
+  if (aead_ == nullptr) {
+    return false;
+  }
   // Make a padded buffer.
-
   ciphertext->Allocate(plaintext.len() +
                        32);  // Room for any plausible auth tag
   size_t len;
 
   DataBuffer header_bytes;
   (void)header.WriteHeader(&header_bytes, 0, plaintext.len() + 16);
-  bool ret =
-      aead_->Aead(false, header_bytes.data(), header_bytes.len(),
-                  header.sequence_number(), plaintext.data(), plaintext.len(),
-                  ciphertext->data(), &len, ciphertext->len());
-  if (!ret) return false;
+  uint64_t seqno;
+  if (dtls_) {
+    seqno = header.sequence_number();
+  } else {
+    seqno = out_seqno_;
+  }
+
+  bool ret = aead_->Aead(false, header_bytes.data(), header_bytes.len(), seqno,
+                         plaintext.data(), plaintext.len(), ciphertext->data(),
+                         &len, ciphertext->len());
+  if (!ret) {
+    return false;
+  }
+
+  RecordProtected();
   ciphertext->Truncate(len);
 
   return true;
 }
 
 }  // namespace nss_test
--- a/security/nss/gtests/ssl_gtest/tls_protect.h
+++ b/security/nss/gtests/ssl_gtest/tls_protect.h
@@ -17,66 +17,81 @@
 namespace nss_test {
 class TlsRecordHeader;
 
 class AeadCipher {
  public:
   AeadCipher(CK_MECHANISM_TYPE mech) : mech_(mech), key_(nullptr) {}
   virtual ~AeadCipher();
 
-  bool Init(PK11SymKey *key, const uint8_t *iv);
-  virtual bool Aead(bool decrypt, const uint8_t *hdr, size_t hdr_len,
-                    uint64_t seq, const uint8_t *in, size_t inlen, uint8_t *out,
-                    size_t *outlen, size_t maxlen) = 0;
+  bool Init(PK11SymKey* key, const uint8_t* iv);
+  virtual bool Aead(bool decrypt, const uint8_t* hdr, size_t hdr_len,
+                    uint64_t seq, const uint8_t* in, size_t inlen, uint8_t* out,
+                    size_t* outlen, size_t maxlen) = 0;
 
  protected:
-  void FormatNonce(uint64_t seq, uint8_t *nonce);
-  bool AeadInner(bool decrypt, void *params, size_t param_length,
-                 const uint8_t *in, size_t inlen, uint8_t *out, size_t *outlen,
+  void FormatNonce(uint64_t seq, uint8_t* nonce);
+  bool AeadInner(bool decrypt, void* params, size_t param_length,
+                 const uint8_t* in, size_t inlen, uint8_t* out, size_t* outlen,
                  size_t maxlen);
 
   CK_MECHANISM_TYPE mech_;
-  PK11SymKey *key_;
+  PK11SymKey* key_;
   uint8_t iv_[12];
 };
 
 class AeadCipherChacha20Poly1305 : public AeadCipher {
  public:
   AeadCipherChacha20Poly1305() : AeadCipher(CKM_NSS_CHACHA20_POLY1305) {}
 
  protected:
-  bool Aead(bool decrypt, const uint8_t *hdr, size_t hdr_len, uint64_t seq,
-            const uint8_t *in, size_t inlen, uint8_t *out, size_t *outlen,
+  bool Aead(bool decrypt, const uint8_t* hdr, size_t hdr_len, uint64_t seq,
+            const uint8_t* in, size_t inlen, uint8_t* out, size_t* outlen,
             size_t maxlen);
 };
 
 class AeadCipherAesGcm : public AeadCipher {
  public:
   AeadCipherAesGcm() : AeadCipher(CKM_AES_GCM) {}
 
  protected:
-  bool Aead(bool decrypt, const uint8_t *hdr, size_t hdr_len, uint64_t seq,
-            const uint8_t *in, size_t inlen, uint8_t *out, size_t *outlen,
+  bool Aead(bool decrypt, const uint8_t* hdr, size_t hdr_len, uint64_t seq,
+            const uint8_t* in, size_t inlen, uint8_t* out, size_t* outlen,
             size_t maxlen);
 };
 
 // Our analog of ssl3CipherSpec
 class TlsCipherSpec {
  public:
-  TlsCipherSpec() : epoch_(0), aead_() {}
+  TlsCipherSpec(bool dtls, uint16_t epoc);
+  bool SetKeys(SSLCipherSuiteInfo* cipherinfo, PK11SymKey* secret);
 
-  bool Init(uint16_t epoch, SSLCipherAlgorithm cipher, PK11SymKey *key,
-            const uint8_t *iv);
+  bool Protect(const TlsRecordHeader& header, const DataBuffer& plaintext,
+               DataBuffer* ciphertext);
+  bool Unprotect(const TlsRecordHeader& header, const DataBuffer& ciphertext,
+                 DataBuffer* plaintext);
 
-  bool Protect(const TlsRecordHeader &header, const DataBuffer &plaintext,
-               DataBuffer *ciphertext);
-  bool Unprotect(const TlsRecordHeader &header, const DataBuffer &ciphertext,
-                 DataBuffer *plaintext);
   uint16_t epoch() const { return epoch_; }
+  uint64_t next_in_seqno() const { return in_seqno_; }
+  void RecordUnprotected(uint64_t seqno) {
+    // Reordering happens, so don't let this go backwards.
+    in_seqno_ = (std::max)(in_seqno_, seqno + 1);
+  }
+  uint64_t next_out_seqno() { return out_seqno_; }
+  void RecordProtected() { out_seqno_++; }
+
+  void RecordDropped() { record_dropped_ = true; }
+  bool record_dropped() const { return record_dropped_; }
+
+  bool is_protected() const { return aead_ != nullptr; }
 
  private:
+  bool dtls_;
   uint16_t epoch_;
+  uint64_t in_seqno_;
+  uint64_t out_seqno_;
+  bool record_dropped_ = false;
   std::unique_ptr<AeadCipher> aead_;
 };
 
 }  // namespace nss_test
 
 #endif
--- a/security/nss/lib/pk11wrap/pk11akey.c
+++ b/security/nss/lib/pk11wrap/pk11akey.c
@@ -1672,16 +1672,102 @@ PK11_MakeKEAPubKey(unsigned char *keyDat
     rv = SECITEM_CopyItem(arena, &pubk->u.fortezza.KEAKey, &pkData);
     if (rv != SECSuccess) {
         PORT_FreeArena(arena, PR_FALSE);
         return NULL;
     }
     return pubk;
 }
 
+SECStatus
+SECKEY_SetPublicValue(SECKEYPrivateKey *privKey, SECItem *publicValue)
+{
+    SECStatus rv;
+    SECKEYPublicKey pubKey;
+    PLArenaPool *arena;
+    PK11SlotInfo *slot = privKey->pkcs11Slot;
+    CK_OBJECT_HANDLE privKeyID = privKey->pkcs11ID;
+
+    if (privKey == NULL || publicValue == NULL ||
+        publicValue->data == NULL || publicValue->len == 0) {
+        PORT_SetError(SEC_ERROR_INVALID_ARGS);
+        return SECFailure;
+    }
+
+    pubKey.arena = NULL;
+    pubKey.keyType = privKey->keyType;
+    pubKey.pkcs11Slot = NULL;
+    pubKey.pkcs11ID = CK_INVALID_HANDLE;
+    /* can't use PORT_InitCheapArena here becase SECKEY_DestroyPublic is used
+      * to free it, and it uses PORT_FreeArena which not only frees the 
+      * underlying arena, it also frees the allocated arena struct. */
+    arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE);
+    pubKey.arena = arena;
+    if (arena == NULL) {
+        return SECFailure;
+    }
+    rv = SECFailure;
+    switch (privKey->keyType) {
+        default:
+            /* error code already set to SECFailure */
+            break;
+        case rsaKey:
+            pubKey.u.rsa.modulus = *publicValue;
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_PUBLIC_EXPONENT,
+                                    arena, &pubKey.u.rsa.publicExponent);
+            break;
+        case dsaKey:
+            pubKey.u.dsa.publicValue = *publicValue;
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_PRIME,
+                                    arena, &pubKey.u.dsa.params.prime);
+            if (rv != SECSuccess) {
+                break;
+            }
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_SUBPRIME,
+                                    arena, &pubKey.u.dsa.params.subPrime);
+            if (rv != SECSuccess) {
+                break;
+            }
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_BASE,
+                                    arena, &pubKey.u.dsa.params.base);
+            break;
+        case dhKey:
+            pubKey.u.dh.publicValue = *publicValue;
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_PRIME,
+                                    arena, &pubKey.u.dh.prime);
+            if (rv != SECSuccess) {
+                break;
+            }
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_BASE,
+                                    arena, &pubKey.u.dh.base);
+            break;
+        case ecKey:
+            pubKey.u.ec.publicValue = *publicValue;
+            pubKey.u.ec.encoding = ECPoint_Undefined;
+            pubKey.u.ec.size = 0;
+            rv = PK11_ReadAttribute(slot, privKeyID, CKA_EC_PARAMS,
+                                    arena, &pubKey.u.ec.DEREncodedParams);
+            break;
+    }
+    if (rv == SECSuccess) {
+        rv = PK11_ImportPublicKey(slot, &pubKey, PR_TRUE);
+    }
+    /* Even though pubKey is stored on the stack, we've allocated
+     * some of it's data from the arena. SECKEY_DestroyPublicKey
+     * destroys keys by freeing the arena, so this will clean up all
+     * the data we allocated specifically for the key above. It will
+     * also free any slot references which we may have picked up in
+     * PK11_ImportPublicKey. It won't delete the underlying key if
+     * its a Token/Permanent key (which it will be if
+     * PK11_ImportPublicKey succeeds). */
+    SECKEY_DestroyPublicKey(&pubKey);
+
+    return rv;
+}
+
 /*
  * NOTE: This function doesn't return a SECKEYPrivateKey struct to represent
  * the new private key object.  If it were to create a session object that
  * could later be looked up by its nickname, it would leak a SECKEYPrivateKey.
  * So isPerm must be true.
  */
 SECStatus
 PK11_ImportEncryptedPrivateKeyInfo(PK11SlotInfo *slot,
@@ -1797,22 +1883,16 @@ try_faulty_3des:
 
     PORT_Assert(usage != NULL);
     PORT_Assert(usageCount != 0);
     privKey = PK11_UnwrapPrivKey(slot, key, cryptoMechType,
                                  crypto_param, &epki->encryptedData,
                                  nickname, publicValue, isPerm, isPrivate,
                                  key_type, usage, usageCount, wincx);
     if (privKey) {
-        if (privk) {
-            *privk = privKey;
-        } else {
-            SECKEY_DestroyPrivateKey(privKey);
-        }
-        privKey = NULL;
         rv = SECSuccess;
         goto done;
     }
 
     /* if we are unable to import the key and the pbeMechType is
      * CKM_NETSCAPE_PBE_SHA1_TRIPLE_DES_CBC, then it is possible that
      * the encrypted blob was created with a buggy key generation method
      * which is described in the PKCS 12 implementation notes.  So we
@@ -1832,16 +1912,35 @@ try_faulty_3des:
         faulty3DES = PR_TRUE;
         goto try_faulty_3des;
     }
 
     /* key import really did fail */
     rv = SECFailure;
 
 done:
+    if ((rv == SECSuccess) && isPerm) {
+        /* If we are importing a token object,
+         * create the corresponding public key.
+         * If this fails, just continue as the target
+         * token simply might not support persistant
+         * public keys. Such tokens are usable, but
+         * need to be authenticated before searching
+         * for user certs. */
+        (void)SECKEY_SetPublicValue(privKey, publicValue);
+    }
+
+    if (privKey) {
+        if (privk) {
+            *privk = privKey;
+        } else {
+            SECKEY_DestroyPrivateKey(privKey);
+        }
+        privKey = NULL;
+    }
     if (crypto_param != NULL) {
         SECITEM_ZfreeItem(crypto_param, PR_TRUE);
     }
 
     if (key != NULL) {
         PK11_FreeSymKey(key);
     }
 
--- a/security/nss/lib/softoken/pkcs11.c
+++ b/security/nss/lib/softoken/pkcs11.c
@@ -1810,29 +1810,33 @@ sftk_GetPubKey(SFTKObject *object, CK_KE
                  * Some curves are always pressumed to be non-DER.
                  */
                 if (pubKey->u.ec.publicValue.len == keyLen &&
                     (pubKey->u.ec.ecParams.fieldID.type == ec_field_plain ||
                      pubKey->u.ec.publicValue.data[0] == EC_POINT_FORM_UNCOMPRESSED)) {
                     break; /* key was not DER encoded, no need to unwrap */
                 }
 
-                PORT_Assert(pubKey->u.ec.ecParams.name != ECCurve25519);
-
                 /* handle the encoded case */
                 if ((pubKey->u.ec.publicValue.data[0] == SEC_ASN1_OCTET_STRING) &&
                     pubKey->u.ec.publicValue.len > keyLen) {
                     SECItem publicValue;
                     SECStatus rv;
 
                     rv = SEC_QuickDERDecodeItem(arena, &publicValue,
                                                 SEC_ASN1_GET(SEC_OctetStringTemplate),
                                                 &pubKey->u.ec.publicValue);
                     /* nope, didn't decode correctly */
-                    if ((rv != SECSuccess) || (publicValue.data[0] != EC_POINT_FORM_UNCOMPRESSED) || (publicValue.len != keyLen)) {
+                    if ((rv != SECSuccess) || (publicValue.len != keyLen)) {
+                        crv = CKR_ATTRIBUTE_VALUE_INVALID;
+                        break;
+                    }
+                    /* we don't handle compressed points except in the case of ECCurve25519 */
+                    if ((pubKey->u.ec.ecParams.fieldID.type != ec_field_plain) &&
+                        (publicValue.data[0] != EC_POINT_FORM_UNCOMPRESSED)) {
                         crv = CKR_ATTRIBUTE_VALUE_INVALID;
                         break;
                     }
                     /* replace our previous with the decoded key */
                     pubKey->u.ec.publicValue = publicValue;
                     break;
                 }
                 crv = CKR_ATTRIBUTE_VALUE_INVALID;
--- a/security/nss/lib/ssl/dtls13con.c
+++ b/security/nss/lib/ssl/dtls13con.c
@@ -477,17 +477,17 @@ dtls13_HandleAck(sslSocket *ss, sslBuffe
         ssl_ClearPRCList(&ss->ssl3.hs.dtlsSentHandshake, NULL);
         /* If the handshake is finished, and we're the client then
          * also clean up the handshake read cipher spec. Any ACKs
          * we receive will be with the application data cipher spec.
          * The server needs to keep the handshake cipher spec around
          * for the holddown period to process retransmitted Finisheds.
          */
         if (!ss->sec.isServer && (ss->ssl3.hs.ws == idle_handshake)) {
-            ssl_CipherSpecReleaseByEpoch(ss, CipherSpecRead,
+            ssl_CipherSpecReleaseByEpoch(ss, ssl_secret_read,
                                          TrafficKeyHandshake);
         }
     }
     return SECSuccess;
 }
 
 /* Clean up the read timer for the handshake cipher suites on the
  * server.
@@ -504,11 +504,11 @@ dtls13_HandleAck(sslSocket *ss, sslBuffe
  * After the holddown period, the server assumes the client is happy
  * and discards the handshake read cipher suite.
  */
 void
 dtls13_HolddownTimerCb(sslSocket *ss)
 {
     SSL_TRC(10, ("%d: SSL3[%d]: holddown timer fired",
                  SSL_GETPID(), ss->fd));
-    ssl_CipherSpecReleaseByEpoch(ss, CipherSpecRead, TrafficKeyHandshake);
+    ssl_CipherSpecReleaseByEpoch(ss, ssl_secret_read, TrafficKeyHandshake);
     ssl_ClearPRCList(&ss->ssl3.hs.dtlsRcvdHandshake, NULL);
 }
--- a/security/nss/lib/ssl/dtlscon.c
+++ b/security/nss/lib/ssl/dtlscon.c
@@ -537,17 +537,16 @@ dtls_QueueMessage(sslSocket *ss, SSLCont
         PR_APPEND_LINK(&msg->link, &ss->ssl3.hs.lastMessageFlight);
     }
 
     return rv;
 }
 
 /* Add DTLS handshake message to the pending queue
  * Empty the sendBuf buffer.
- * This function returns SECSuccess or SECFailure, never SECWouldBlock.
  * Always set sendBuf.len to 0, even when returning SECFailure.
  *
  * Called from:
  *              ssl3_AppendHandshakeHeader()
  *              dtls_FlushHandshake()
  */
 SECStatus
 dtls_StageHandshakeMessage(sslSocket *ss)
--- a/security/nss/lib/ssl/ssl.h
+++ b/security/nss/lib/ssl/ssl.h
@@ -294,16 +294,27 @@ SSL_IMPORT PRFileDesc *DTLS_ImportFD(PRF
 /* Enables the SSLv2-compatible ClientHello for servers. NSS does not support
  * SSLv2 and will never send an SSLv2-compatible ClientHello as a client.  An
  * NSS server with this option enabled will accept a ClientHello that is
  * v2-compatible as defined in Appendix E.1 of RFC 6101.
  *
  * This is disabled by default and will be removed in a future version. */
 #define SSL_ENABLE_V2_COMPATIBLE_HELLO 38
 
+/* Enables the post-handshake authentication in TLS 1.3.  If it is set
+ * to PR_TRUE, the client will send the "post_handshake_auth"
+ * extension to indicate that it will process CertificateRequest
+ * messages after handshake.
+ *
+ * This option applies only to clients.  For a server, the
+ * SSL_SendCertificateRequest can be used to request post-handshake
+ * authentication.
+ */
+#define SSL_ENABLE_POST_HANDSHAKE_AUTH 39
+
 #ifdef SSL_DEPRECATED_FUNCTION
 /* Old deprecated function names */
 SSL_IMPORT SECStatus SSL_Enable(PRFileDesc *fd, int option, PRIntn on);
 SSL_IMPORT SECStatus SSL_EnableDefault(int option, PRIntn on);
 #endif
 
 /* Set (and get) options for sockets and defaults for newly created sockets.
  *
--- a/security/nss/lib/ssl/ssl3con.c
+++ b/security/nss/lib/ssl/ssl3con.c
@@ -1389,40 +1389,40 @@ ssl3_ComputeDHKeyHash(sslSocket *ss, SSL
     return SECSuccess;
 
 loser:
     sslBuffer_Clear(&buf);
     return SECFailure;
 }
 
 static SECStatus
-ssl3_SetupPendingCipherSpec(sslSocket *ss, CipherSpecDirection direction,
+ssl3_SetupPendingCipherSpec(sslSocket *ss, SSLSecretDirection direction,
                             const ssl3CipherSuiteDef *suiteDef,
                             ssl3CipherSpec **specp)
 {
     ssl3CipherSpec *spec;
     const ssl3CipherSpec *prev;
 
-    prev = (direction == CipherSpecWrite) ? ss->ssl3.cwSpec : ss->ssl3.crSpec;
+    prev = (direction == ssl_secret_write) ? ss->ssl3.cwSpec : ss->ssl3.crSpec;
     if (prev->epoch == PR_UINT16_MAX) {
         PORT_SetError(SSL_ERROR_RENEGOTIATION_NOT_ALLOWED);
         return SECFailure;
     }
 
     spec = ssl_CreateCipherSpec(ss, direction);
     if (!spec) {
         return SECFailure;
     }
 
     spec->cipherDef = ssl_GetBulkCipherDef(suiteDef);
     spec->macDef = ssl_GetMacDef(ss, suiteDef);
 
     spec->epoch = prev->epoch + 1;
     spec->nextSeqNum = 0;
-    if (IS_DTLS(ss) && direction == CipherSpecRead) {
+    if (IS_DTLS(ss) && direction == ssl_secret_read) {
         dtls_InitRecvdRecords(&spec->recvdRecords);
     }
     ssl_SetSpecVersions(ss, spec);
 
     ssl_SaveCipherSpec(ss, spec);
     *specp = spec;
     return SECSuccess;
 }
@@ -1466,22 +1466,22 @@ ssl3_SetupBothPendingCipherSpecs(sslSock
     }
 
     ss->ssl3.hs.suite_def = suiteDef;
 
     kea = suiteDef->key_exchange_alg;
     ss->ssl3.hs.kea_def = &kea_defs[kea];
     PORT_Assert(ss->ssl3.hs.kea_def->kea == kea);
 
-    rv = ssl3_SetupPendingCipherSpec(ss, CipherSpecRead, suiteDef,
+    rv = ssl3_SetupPendingCipherSpec(ss, ssl_secret_read, suiteDef,
                                      &ss->ssl3.prSpec);
     if (rv != SECSuccess) {
         goto loser;
     }
-    rv = ssl3_SetupPendingCipherSpec(ss, CipherSpecWrite, suiteDef,
+    rv = ssl3_SetupPendingCipherSpec(ss, ssl_secret_write, suiteDef,
                                      &ss->ssl3.pwSpec);
     if (rv != SECSuccess) {
         goto loser;
     }
 
     if (ssl3_ExtensionNegotiated(ss, ssl_record_size_limit_xtn)) {
         ss->ssl3.prSpec->recordSizeLimit = PR_MIN(MAX_FRAGMENT_LENGTH,
                                                   ss->opt.recordSizeLimit);
@@ -1722,17 +1722,17 @@ ssl3_InitPendingContexts(sslSocket *ss, 
     */
     if (calg == ssl_calg_null) {
         spec->cipher = Null_Cipher;
         return SECSuccess;
     }
 
     spec->cipher = (SSLCipher)PK11_CipherOp;
     encMechanism = ssl3_Alg2Mech(calg);
-    encMode = (spec->direction == CipherSpecWrite) ? CKA_ENCRYPT : CKA_DECRYPT;
+    encMode = (spec->direction == ssl_secret_write) ? CKA_ENCRYPT : CKA_DECRYPT;
 
     /*
      * build the context
      */
     iv.data = spec->keyMaterial.iv;
     iv.len = spec->cipherDef->iv_size;
     spec->cipherContext = PK11_CreateContextBySymKey(encMechanism, encMode,
                                                      spec->keyMaterial.key,
@@ -2210,17 +2210,17 @@ ssl_InsertRecordHeader(const sslSocket *
 SECStatus
 ssl_ProtectRecord(sslSocket *ss, ssl3CipherSpec *cwSpec, SSLContentType ct,
                   const PRUint8 *pIn, PRUint32 contentLen, sslBuffer *wrBuf)
 {
     PRBool needsLength;
     unsigned int lenOffset;
     SECStatus rv;
 
-    PORT_Assert(cwSpec->direction == CipherSpecWrite);
+    PORT_Assert(cwSpec->direction == ssl_secret_write);
     PORT_Assert(SSL_BUFFER_LEN(wrBuf) == 0);
     PORT_Assert(cwSpec->cipherDef->max_records <= RECORD_SEQ_MAX);
 
     if (cwSpec->nextSeqNum >= cwSpec->cipherDef->max_records) {
         /* We should have automatically updated before here in TLS 1.3. */
         PORT_Assert(cwSpec->version < SSL_LIBRARY_VERSION_TLS_1_3);
         SSL_TRC(3, ("%d: SSL[-]: write sequence number at limit 0x%0llx",
                     SSL_GETPID(), cwSpec->nextSeqNum));
@@ -2309,18 +2309,18 @@ ssl_ProtectNextRecord(sslSocket *ss, ssl
     *written = contentLen;
     return SECSuccess;
 }
 
 /* Process the plain text before sending it.
  * Returns the number of bytes of plaintext that were successfully sent
  *  plus the number of bytes of plaintext that were copied into the
  *  output (write) buffer.
- * Returns SECFailure on a hard IO error, memory error, or crypto error.
- * Does NOT return SECWouldBlock.
+ * Returns -1 on an error.  PR_WOULD_BLOCK_ERROR is set if the error is blocking
+ *  and not terminal.
  *
  * Notes on the use of the private ssl flags:
  * (no private SSL flags)
  *    Attempt to make and send SSL records for all plaintext
  *    If non-blocking and a send gets WOULD_BLOCK,
  *    or if the pending (ciphertext) buffer is not empty,
  *    then buffer remaining bytes of ciphertext into pending buf,
  *    and continue to do that for all succssive records until all
@@ -2355,23 +2355,36 @@ ssl3_SendRecord(sslSocket *ss,
     if (ss->ssl3.fatalAlertSent) {
         SSL_TRC(3, ("%d: SSL3[%d] Suppress write, fatal alert already sent",
                     SSL_GETPID(), ss->fd));
         if (ct != ssl_ct_alert) {
             /* If we are sending an alert, then we already have an
              * error, so don't overwrite. */
             PORT_SetError(SSL_ERROR_HANDSHAKE_FAILED);
         }
-        return SECFailure;
+        return -1;
     }
 
     /* check for Token Presence */
     if (!ssl3_ClientAuthTokenPresent(ss->sec.ci.sid)) {
         PORT_SetError(SSL_ERROR_TOKEN_INSERTION_REMOVAL);
-        return SECFailure;
+        return -1;
+    }
+
+    if (ss->recordWriteCallback) {
+        PRUint16 epoch;
+        ssl_GetSpecReadLock(ss);
+        epoch = ss->ssl3.cwSpec->epoch;
+        ssl_ReleaseSpecReadLock(ss);
+        rv = ss->recordWriteCallback(ss->fd, epoch, ct, pIn, nIn,
+                                     ss->recordWriteCallbackArg);
+        if (rv != SECSuccess) {
+            return -1;
+        }
+        return nIn;
     }
 
     if (cwSpec) {
         /* cwSpec can only be set for retransmissions of the DTLS handshake. */
         PORT_Assert(IS_DTLS(ss) &&
                     (ct == ssl_ct_handshake ||
                      ct == ssl_ct_change_cipher_spec));
         spec = cwSpec;
@@ -2465,46 +2478,46 @@ loser:
     /* Don't leave bits of buffer lying around. */
     wrBuf->len = 0;
     return -1;
 }
 
 #define SSL3_PENDING_HIGH_WATER 1024
 
 /* Attempt to send the content of "in" in an SSL application_data record.
- * Returns "len" or SECFailure,   never SECWouldBlock, nor SECSuccess.
+ * Returns "len" or -1 on failure.
  */
 int
 ssl3_SendApplicationData(sslSocket *ss, const unsigned char *in,
                          PRInt32 len, PRInt32 flags)
 {
     PRInt32 totalSent = 0;
     PRInt32 discarded = 0;
     PRBool splitNeeded = PR_FALSE;
 
     PORT_Assert(ss->opt.noLocks || ssl_HaveXmitBufLock(ss));
     /* These flags for internal use only */
     PORT_Assert(!(flags & ssl_SEND_FLAG_NO_RETRANSMIT));
     if (len < 0 || !in) {
         PORT_SetError(PR_INVALID_ARGUMENT_ERROR);
-        return SECFailure;
+        return -1;
     }
 
     if (ss->pendingBuf.len > SSL3_PENDING_HIGH_WATER &&
         !ssl_SocketIsBlocking(ss)) {
         PORT_Assert(!ssl_SocketIsBlocking(ss));
         PORT_SetError(PR_WOULD_BLOCK_ERROR);
-        return SECFailure;
+        return -1;
     }
 
     if (ss->appDataBuffered && len) {
         PORT_Assert(in[0] == (unsigned char)(ss->appDataBuffered));
         if (in[0] != (unsigned char)(ss->appDataBuffered)) {
             PORT_SetError(PR_INVALID_ARGUMENT_ERROR);
-            return SECFailure;
+            return -1;
         }
         in++;
         len--;
         discarded = 1;
     }
 
     /* We will split the first byte of the record into its own record, as
      * explained in the documentation for SSL_CBC_RANDOM_IV in ssl.h.
@@ -2543,17 +2556,17 @@ ssl3_SendApplicationData(sslSocket *ss, 
          */
         sent = ssl3_SendRecord(ss, NULL, ssl_ct_application_data,
                                in + totalSent, toSend, flags);
         if (sent < 0) {
             if (totalSent > 0 && PR_GetError() == PR_WOULD_BLOCK_ERROR) {
                 PORT_Assert(ss->lastWriteBlocked);
                 break;
             }
-            return SECFailure; /* error code set by ssl3_SendRecord */
+            return -1; /* error code set by ssl3_SendRecord */
         }
         totalSent += sent;
         if (ss->pendingBuf.len) {
             /* must be a non-blocking socket */
             PORT_Assert(!ssl_SocketIsBlocking(ss));
             PORT_Assert(ss->lastWriteBlocked);
             break;
         }
@@ -2572,17 +2585,16 @@ ssl3_SendApplicationData(sslSocket *ss, 
         }
         return totalSent;
     }
     ss->appDataBuffered = 0;
     return totalSent + discarded;
 }
 
 /* Attempt to send buffered handshake messages.
- * This function returns SECSuccess or SECFailure, never SECWouldBlock.
  * Always set sendBuf.len to 0, even when returning SECFailure.
  *
  * Depending on whether we are doing DTLS or not, this either calls
  *
  * - ssl3_FlushHandshakeMessages if non-DTLS
  * - dtls_FlushHandshakeMessages if DTLS
  *
  * Called from SSL3_SendAlert(), ssl3_SendChangeCipherSpecs(),
@@ -2595,17 +2607,16 @@ ssl3_FlushHandshake(sslSocket *ss, PRInt
 {
     if (IS_DTLS(ss)) {
         return dtls_FlushHandshakeMessages(ss, flags);
     }
     return ssl3_FlushHandshakeMessages(ss, flags);
 }
 
 /* Attempt to send the content of sendBuf buffer in an SSL handshake record.
- * This function returns SECSuccess or SECFailure, never SECWouldBlock.
  * Always set sendBuf.len to 0, even when returning SECFailure.
  *
  * Called from ssl3_FlushHandshake
  */
 static SECStatus
 ssl3_FlushHandshakeMessages(sslSocket *ss, PRInt32 flags)
 {
     static const PRInt32 allowedFlags = ssl_SEND_FLAG_FORCE_INTO_BUFFER;
@@ -7377,16 +7388,19 @@ ssl3_CompleteHandleCertificateRequest(ss
                                       unsigned int signatureSchemeCount,
                                       CERTDistNames *ca_list)
 {
     SECStatus rv;
 
     if (ss->getClientAuthData != NULL) {
         PORT_Assert((ss->ssl3.hs.preliminaryInfo & ssl_preinfo_all) ==
                     ssl_preinfo_all);
+        PORT_Assert(ss->ssl3.clientPrivateKey == NULL);
+        PORT_Assert(ss->ssl3.clientCertificate == NULL);
+        PORT_Assert(ss->ssl3.clientCertChain == NULL);
         /* XXX Should pass cert_types and algorithms in this call!! */
         rv = (SECStatus)(*ss->getClientAuthData)(ss->getClientAuthDataArg,
                                                  ss->fd, ca_list,
                                                  &ss->ssl3.clientCertificate,
                                                  &ss->ssl3.clientPrivateKey);
     } else {
         rv = SECFailure; /* force it to send a no_certificate alert */
     }
@@ -7598,17 +7612,18 @@ ssl3_SendClientSecondRound(sslSocket *ss
         return SECFailure;
     }
     if (ss->ssl3.hs.authCertificatePending &&
         (sendClientCert || ss->ssl3.sendEmptyCert || ss->firstHsDone)) {
         SSL_TRC(3, ("%d: SSL3[%p]: deferring ssl3_SendClientSecondRound because"
                     " certificate authentication is still pending.",
                     SSL_GETPID(), ss->fd));
         ss->ssl3.hs.restartTarget = ssl3_SendClientSecondRound;
-        return SECWouldBlock;
+        PORT_SetError(PR_WOULD_BLOCK_ERROR);
+        return SECFailure;
     }
 
     ssl_GetXmitBufLock(ss); /*******************************/
 
     if (ss->ssl3.sendEmptyCert) {
         ss->ssl3.sendEmptyCert = PR_FALSE;
         rv = ssl3_SendEmptyCertificate(ss);
         /* Don't send verify */
@@ -10732,16 +10747,19 @@ ssl3_AuthCertificate(sslSocket *ss)
         }
 
         if (rv != SECSuccess) {
             ssl3_SendAlertForCertError(ss, errCode);
             goto loser;
         }
     }
 
+    if (ss->sec.ci.sid->peerCert) {
+        CERT_DestroyCertificate(ss->sec.ci.sid->peerCert);
+    }
     ss->sec.ci.sid->peerCert = CERT_DupCertificate(ss->sec.peerCert);
 
     if (!ss->sec.isServer) {
         CERTCertificate *cert = ss->sec.peerCert;
 
         /* set the server authentication type and size from the value
         ** in the cert. */
         SECKEYPublicKey *pubKey = CERT_ExtractPublicKey(cert);
@@ -10893,23 +10911,16 @@ ssl3_AuthCertificateComplete(sslSocket *
 
         if (target == ssl3_FinishHandshake) {
             SSL_TRC(3, ("%d: SSL3[%p]: certificate authentication lost the race"
                         " with peer's finished message",
                         SSL_GETPID(), ss->fd));
         }
 
         rv = target(ss);
-        /* Even if we blocked here, we have accomplished enough to claim
-         * success. Any remaining work will be taken care of by subsequent
-         * calls to SSL_ForceHandshake/PR_Send/PR_Read/etc.
-         */
-        if (rv == SECWouldBlock) {
-            rv = SECSuccess;
-        }
     } else {
         SSL_TRC(3, ("%d: SSL3[%p]: certificate authentication won the race with"
                     " peer's finished message",
                     SSL_GETPID(), ss->fd));
 
         PORT_Assert(!ss->ssl3.hs.isResuming);
         PORT_Assert(ss->ssl3.hs.ws != idle_handshake);
 
@@ -11440,17 +11451,18 @@ xmit_loser:
     if (ss->ssl3.hs.authCertificatePending) {
         if (ss->ssl3.hs.restartTarget) {
             PR_NOT_REACHED("ssl3_HandleFinished: unexpected restartTarget");
             PORT_SetError(SEC_ERROR_LIBRARY_FAILURE);
             return SECFailure;
         }
 
         ss->ssl3.hs.restartTarget = ssl3_FinishHandshake;
-        return SECWouldBlock;
+        PORT_SetError(PR_WOULD_BLOCK_ERROR);
+        return SECFailure;
     }
 
     rv = ssl3_FinishHandshake(ss);
     return rv;
 }
 
 SECStatus
 ssl3_FillInCachedSID(sslSocket *ss, sslSessionID *sid, PK11SymKey *secret)
@@ -11644,19 +11656,20 @@ ssl3_HandleHandshakeMessage(sslSocket *s
         /* If we negotiated the certificate_status extension then we deferred
          * certificate validation until we get the CertificateStatus messsage.
          * But the CertificateStatus message is optional. If the server did
          * not send it then we need to validate the certificate now. If the
          * server does send the CertificateStatus message then we will
          * authenticate the certificate in ssl3_HandleCertificateStatus.
          */
         rv = ssl3_AuthCertificate(ss); /* sets ss->ssl3.hs.ws */
-        PORT_Assert(rv != SECWouldBlock);
-        if (rv != SECSuccess) {
-            return rv;
+        if (rv != SECSuccess) {